refactor(ops): 运营可配代码统一 ops_ 前缀(模型/表/repo/admin schema·router)
把首页门面数据的「运营可配」部分代码统一加 ops_ 前缀,与运营语义对齐: - 模型 PlatformStatDisplay→OpsStatConfig(表 platform_stat_display→ops_stat_config)、 MarqueeSeed→OpsMarqueeSeed(表 marquee_seed→ops_marquee_seed) - repo platform_stat→ops_stat、marquee→ops_marquee - admin schema/router dashboard_display→ops_stat_config、marquee_seed→ops_marquee_seed(schema 类 Ops*) - 审计 action/target_type 同步成 ops_* - 引用方(models/__init__、admin/main、api/v1/platform)更 import;platform 用户侧门面保留命名 - 迁移 opsrename01 仅改表名(可逆);路由 URL 与 API 文档文件名不变(前端/客户端无感) - 文档:db 文档重命名 + API 文档/索引同步 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
"""运营可配表加 ops_ 前缀:platform_stat_display→ops_stat_config / marquee_seed→ops_marquee_seed
|
||||
|
||||
把首页门面数据的两张「运营可配」表重命名,与代码侧 ops_ 前缀(模型 OpsStatConfig/OpsMarqueeSeed、
|
||||
repo ops_stat/ops_marquee、admin ops_stat_config/ops_marquee_seed)对齐。仅改表名,不动列与数据。
|
||||
|
||||
Revision ID: opsrename01
|
||||
Revises: coin_reward_phase1
|
||||
Create Date: 2026-06-07 16:00:00.000000
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
|
||||
|
||||
revision: str = 'opsrename01'
|
||||
down_revision: Union[str, Sequence[str], None] = 'coin_reward_phase1'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.rename_table('platform_stat_display', 'ops_stat_config')
|
||||
op.rename_table('marquee_seed', 'ops_marquee_seed')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.rename_table('ops_stat_config', 'platform_stat_display')
|
||||
op.rename_table('ops_marquee_seed', 'marquee_seed')
|
||||
+4
-4
@@ -18,9 +18,9 @@ from app.admin.routers.audit import router as audit_router
|
||||
from app.admin.routers.auth import router as auth_router
|
||||
from app.admin.routers.config import router as config_router
|
||||
from app.admin.routers.dashboard import router as dashboard_router
|
||||
from app.admin.routers.dashboard_display import router as dashboard_display_router
|
||||
from app.admin.routers.ops_stat_config import router as ops_stat_config_router
|
||||
from app.admin.routers.feedback import router as feedback_router
|
||||
from app.admin.routers.marquee_seed import router as marquee_seed_router
|
||||
from app.admin.routers.ops_marquee_seed import router as ops_marquee_seed_router
|
||||
from app.admin.routers.users import router as users_router
|
||||
from app.admin.routers.wallet import router as wallet_router
|
||||
from app.admin.routers.withdraw import router as withdraw_router
|
||||
@@ -75,8 +75,8 @@ def health() -> dict[str, str]:
|
||||
|
||||
admin_app.include_router(auth_router)
|
||||
admin_app.include_router(dashboard_router)
|
||||
admin_app.include_router(dashboard_display_router)
|
||||
admin_app.include_router(marquee_seed_router)
|
||||
admin_app.include_router(ops_stat_config_router)
|
||||
admin_app.include_router(ops_marquee_seed_router)
|
||||
admin_app.include_router(users_router)
|
||||
admin_app.include_router(wallet_router)
|
||||
admin_app.include_router(withdraw_router)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""admin 首页轮播种子管理:列出 / 新增 / 改 / 删 / 批量生成 / 预览(带审计)。
|
||||
|
||||
种子用于首页轮播「真实+种子混播」的兜底(见 app/repositories/marquee.py)。
|
||||
种子用于首页轮播「真实+种子混播」的兜底(见 app/repositories/ops_marquee.py)。
|
||||
权限:operator 可改,super 恒可;预览为只读(任意已登录 admin)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -11,16 +11,16 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
|
||||
from app.admin.audit import write_audit
|
||||
from app.admin.deps import AdminDb, get_client_ip, get_current_admin, require_role
|
||||
from app.admin.schemas.marquee_seed import (
|
||||
MarqueeSeedBulkCreate,
|
||||
MarqueeSeedCreate,
|
||||
MarqueeSeedOut,
|
||||
MarqueeSeedUpdate,
|
||||
SavingsFeedPreviewOut,
|
||||
from app.admin.schemas.ops_marquee_seed import (
|
||||
OpsMarqueeSeedBulkCreate,
|
||||
OpsMarqueeSeedCreate,
|
||||
OpsMarqueeSeedOut,
|
||||
OpsMarqueeSeedUpdate,
|
||||
OpsSavingsFeedPreviewOut,
|
||||
)
|
||||
from app.models.admin import AdminUser
|
||||
from app.models.marquee_seed import MarqueeSeed
|
||||
from app.repositories import marquee
|
||||
from app.models.ops_marquee_seed import OpsMarqueeSeed
|
||||
from app.repositories import ops_marquee
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/admin/api/marquee-seeds",
|
||||
@@ -29,8 +29,8 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
def _out(seed: MarqueeSeed) -> MarqueeSeedOut:
|
||||
return MarqueeSeedOut(
|
||||
def _out(seed: OpsMarqueeSeed) -> OpsMarqueeSeedOut:
|
||||
return OpsMarqueeSeedOut(
|
||||
id=seed.id,
|
||||
masked_user=seed.masked_user,
|
||||
min_cents=seed.min_cents,
|
||||
@@ -41,29 +41,29 @@ def _out(seed: MarqueeSeed) -> MarqueeSeedOut:
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[MarqueeSeedOut], summary="轮播种子列表")
|
||||
def list_seeds(db: AdminDb) -> list[MarqueeSeedOut]:
|
||||
return [_out(s) for s in marquee.list_seeds(db)]
|
||||
@router.get("", response_model=list[OpsMarqueeSeedOut], summary="轮播种子列表")
|
||||
def list_seeds(db: AdminDb) -> list[OpsMarqueeSeedOut]:
|
||||
return [_out(s) for s in ops_marquee.list_seeds(db)]
|
||||
|
||||
|
||||
@router.get("/preview", response_model=SavingsFeedPreviewOut, summary="预览实际混播 feed")
|
||||
@router.get("/preview", response_model=OpsSavingsFeedPreviewOut, summary="预览实际混播 feed")
|
||||
def preview_feed(
|
||||
db: AdminDb,
|
||||
limit: Annotated[int, Query(ge=1, le=30)] = 8,
|
||||
) -> SavingsFeedPreviewOut:
|
||||
) -> OpsSavingsFeedPreviewOut:
|
||||
"""返回客户端实际会看到的 feed(真实记录会插队、种子随机抽取/金额随机/名字合成),供运营对效果。"""
|
||||
return SavingsFeedPreviewOut(items=marquee.get_feed(db, limit=limit))
|
||||
return OpsSavingsFeedPreviewOut(items=ops_marquee.get_feed(db, limit=limit))
|
||||
|
||||
|
||||
@router.post("", response_model=MarqueeSeedOut, summary="新增轮播种子(带审计)")
|
||||
@router.post("", response_model=OpsMarqueeSeedOut, summary="新增轮播种子(带审计)")
|
||||
def create_seed(
|
||||
body: MarqueeSeedCreate,
|
||||
body: OpsMarqueeSeedCreate,
|
||||
request: Request,
|
||||
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||
db: AdminDb,
|
||||
) -> MarqueeSeedOut:
|
||||
) -> OpsMarqueeSeedOut:
|
||||
try:
|
||||
seed = marquee.create_seed(
|
||||
seed = ops_marquee.create_seed(
|
||||
db,
|
||||
masked_user=body.masked_user,
|
||||
min_cents=body.min_cents,
|
||||
@@ -75,7 +75,7 @@ def create_seed(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
write_audit(
|
||||
db, admin, action="marquee_seed.create", target_type="marquee_seed",
|
||||
db, admin, action="ops_marquee_seed.create", target_type="ops_marquee_seed",
|
||||
target_id=None, detail=body.model_dump(), ip=get_client_ip(request), commit=False,
|
||||
)
|
||||
db.commit()
|
||||
@@ -83,15 +83,15 @@ def create_seed(
|
||||
return _out(seed)
|
||||
|
||||
|
||||
@router.post("/bulk", response_model=list[MarqueeSeedOut], summary="批量生成种子(带审计)")
|
||||
@router.post("/bulk", response_model=list[OpsMarqueeSeedOut], summary="批量生成种子(带审计)")
|
||||
def bulk_generate(
|
||||
body: MarqueeSeedBulkCreate,
|
||||
body: OpsMarqueeSeedBulkCreate,
|
||||
request: Request,
|
||||
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||
db: AdminDb,
|
||||
) -> list[MarqueeSeedOut]:
|
||||
) -> list[OpsMarqueeSeedOut]:
|
||||
try:
|
||||
seeds = marquee.bulk_generate(
|
||||
seeds = ops_marquee.bulk_generate(
|
||||
db,
|
||||
count=body.count,
|
||||
min_cents=body.min_cents,
|
||||
@@ -102,7 +102,7 @@ def bulk_generate(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
write_audit(
|
||||
db, admin, action="marquee_seed.bulk_generate", target_type="marquee_seed",
|
||||
db, admin, action="ops_marquee_seed.bulk_generate", target_type="ops_marquee_seed",
|
||||
target_id=None, detail=body.model_dump(), ip=get_client_ip(request), commit=False,
|
||||
)
|
||||
db.commit()
|
||||
@@ -111,16 +111,16 @@ def bulk_generate(
|
||||
return [_out(s) for s in seeds]
|
||||
|
||||
|
||||
@router.patch("/{seed_id}", response_model=MarqueeSeedOut, summary="改轮播种子(带审计)")
|
||||
@router.patch("/{seed_id}", response_model=OpsMarqueeSeedOut, summary="改轮播种子(带审计)")
|
||||
def update_seed(
|
||||
seed_id: int,
|
||||
body: MarqueeSeedUpdate,
|
||||
body: OpsMarqueeSeedUpdate,
|
||||
request: Request,
|
||||
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||
db: AdminDb,
|
||||
) -> MarqueeSeedOut:
|
||||
) -> OpsMarqueeSeedOut:
|
||||
try:
|
||||
seed = marquee.update_seed(
|
||||
seed = ops_marquee.update_seed(
|
||||
db,
|
||||
seed_id,
|
||||
masked_user=body.masked_user,
|
||||
@@ -133,7 +133,7 @@ def update_seed(
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=404 if "不存在" in str(e) else 400, detail=str(e)) from e
|
||||
write_audit(
|
||||
db, admin, action="marquee_seed.update", target_type="marquee_seed",
|
||||
db, admin, action="ops_marquee_seed.update", target_type="ops_marquee_seed",
|
||||
target_id=seed_id, detail=body.model_dump(exclude_none=True),
|
||||
ip=get_client_ip(request), commit=False,
|
||||
)
|
||||
@@ -149,11 +149,11 @@ def delete_seed(
|
||||
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||
db: AdminDb,
|
||||
) -> dict:
|
||||
ok = marquee.delete_seed(db, seed_id, commit=False)
|
||||
ok = ops_marquee.delete_seed(db, seed_id, commit=False)
|
||||
if not ok:
|
||||
raise HTTPException(status_code=404, detail="种子不存在")
|
||||
write_audit(
|
||||
db, admin, action="marquee_seed.delete", target_type="marquee_seed",
|
||||
db, admin, action="ops_marquee_seed.delete", target_type="ops_marquee_seed",
|
||||
target_id=seed_id, detail=None, ip=get_client_ip(request), commit=False,
|
||||
)
|
||||
db.commit()
|
||||
@@ -1,6 +1,6 @@
|
||||
"""admin 首页三统计展示配置:列出三指标配置(+ 真实值预览)+ 改某指标(带审计 + 校验)。
|
||||
|
||||
每指标可独立选 real/manual/random。计算逻辑见 app/repositories/platform_stat.py。
|
||||
每指标可独立选 real/manual/random。计算逻辑见 app/repositories/ops_stat.py。
|
||||
权限:operator 可改(运营调门面数字),super 恒可。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -11,12 +11,12 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from app.admin.audit import write_audit
|
||||
from app.admin.deps import AdminDb, get_client_ip, get_current_admin, require_role
|
||||
from app.admin.schemas.dashboard_display import (
|
||||
PlatformStatItemOut,
|
||||
PlatformStatUpdateRequest,
|
||||
from app.admin.schemas.ops_stat_config import (
|
||||
OpsStatItemOut,
|
||||
OpsStatUpdateRequest,
|
||||
)
|
||||
from app.models.admin import AdminUser
|
||||
from app.repositories import platform_stat
|
||||
from app.repositories import ops_stat
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/admin/api/dashboard-display",
|
||||
@@ -25,23 +25,23 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@router.get("", response_model=list[PlatformStatItemOut], summary="首页三统计配置 + 真实值预览")
|
||||
def list_display(db: AdminDb) -> list[PlatformStatItemOut]:
|
||||
return [PlatformStatItemOut(**item) for item in platform_stat.get_config(db)]
|
||||
@router.get("", response_model=list[OpsStatItemOut], summary="首页三统计配置 + 真实值预览")
|
||||
def list_display(db: AdminDb) -> list[OpsStatItemOut]:
|
||||
return [OpsStatItemOut(**item) for item in ops_stat.get_config(db)]
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{metric}", response_model=PlatformStatItemOut, summary="改某指标展示配置(带审计)"
|
||||
"/{metric}", response_model=OpsStatItemOut, summary="改某指标展示配置(带审计)"
|
||||
)
|
||||
def update_display(
|
||||
metric: str,
|
||||
body: PlatformStatUpdateRequest,
|
||||
body: OpsStatUpdateRequest,
|
||||
request: Request,
|
||||
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
||||
db: AdminDb,
|
||||
) -> PlatformStatItemOut:
|
||||
) -> OpsStatItemOut:
|
||||
try:
|
||||
before, after = platform_stat.update_config(
|
||||
before, after = ops_stat.update_config(
|
||||
db,
|
||||
metric,
|
||||
mode=body.mode,
|
||||
@@ -64,11 +64,11 @@ def update_display(
|
||||
raise HTTPException(status_code=400, detail=str(e)) from e
|
||||
|
||||
write_audit(
|
||||
db, admin, action="dashboard_display.set", target_type="platform_stat",
|
||||
db, admin, action="ops_stat_config.set", target_type="ops_stat_config",
|
||||
target_id=metric, detail={"before": before, "after": after},
|
||||
ip=get_client_ip(request), commit=False,
|
||||
)
|
||||
db.commit()
|
||||
|
||||
item = next(it for it in platform_stat.get_config(db) if it["metric"] == metric)
|
||||
return PlatformStatItemOut(**item)
|
||||
item = next(it for it in ops_stat.get_config(db) if it["metric"] == metric)
|
||||
return OpsStatItemOut(**item)
|
||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class MarqueeSeedOut(BaseModel):
|
||||
class OpsMarqueeSeedOut(BaseModel):
|
||||
id: int
|
||||
masked_user: str | None = None
|
||||
min_cents: int
|
||||
@@ -17,7 +17,7 @@ class MarqueeSeedOut(BaseModel):
|
||||
created_at: str | None = None
|
||||
|
||||
|
||||
class MarqueeSeedCreate(BaseModel):
|
||||
class OpsMarqueeSeedCreate(BaseModel):
|
||||
masked_user: str | None = None # 空 / 不传 → feed 随机合成脱敏名
|
||||
min_cents: int
|
||||
max_cents: int
|
||||
@@ -25,7 +25,7 @@ class MarqueeSeedCreate(BaseModel):
|
||||
sort_order: int = 0
|
||||
|
||||
|
||||
class MarqueeSeedUpdate(BaseModel):
|
||||
class OpsMarqueeSeedUpdate(BaseModel):
|
||||
# masked_user:不传=不改;传空串=清空(改回随机合成);传值=固定该名
|
||||
masked_user: str | None = None
|
||||
min_cents: int | None = None
|
||||
@@ -34,7 +34,7 @@ class MarqueeSeedUpdate(BaseModel):
|
||||
sort_order: int | None = None
|
||||
|
||||
|
||||
class MarqueeSeedBulkCreate(BaseModel):
|
||||
class OpsMarqueeSeedBulkCreate(BaseModel):
|
||||
"""批量生成 count 条自动合成名种子(冷启动铺量)。"""
|
||||
count: int
|
||||
min_cents: int
|
||||
@@ -42,12 +42,12 @@ class MarqueeSeedBulkCreate(BaseModel):
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
class SavingsFeedPreviewItem(BaseModel):
|
||||
class OpsSavingsFeedPreviewItem(BaseModel):
|
||||
masked_user: str
|
||||
saved_amount_cents: int
|
||||
time: str
|
||||
|
||||
|
||||
class SavingsFeedPreviewOut(BaseModel):
|
||||
class OpsSavingsFeedPreviewOut(BaseModel):
|
||||
"""运营预览:实际混播出来的 feed(真实记录会插队,与客户端一致)。"""
|
||||
items: list[SavingsFeedPreviewItem]
|
||||
items: list[OpsSavingsFeedPreviewItem]
|
||||
@@ -7,7 +7,7 @@ from __future__ import annotations
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class PlatformStatItemOut(BaseModel):
|
||||
class OpsStatItemOut(BaseModel):
|
||||
metric: str # help_users / total_compares / total_saved
|
||||
label: str # 帮助用户 / 完成比价 / 累计节省
|
||||
unit: str # 人 / 次 / 分
|
||||
@@ -28,7 +28,7 @@ class PlatformStatItemOut(BaseModel):
|
||||
updated_at: str | None = None
|
||||
|
||||
|
||||
class PlatformStatUpdateRequest(BaseModel):
|
||||
class OpsStatUpdateRequest(BaseModel):
|
||||
"""改某指标配置(均可选,只改传了的字段)。"""
|
||||
|
||||
mode: str | None = None
|
||||
@@ -3,7 +3,7 @@
|
||||
路由前缀 `/api/v1/platform`:
|
||||
GET /stats 首页三统计(帮助用户 / 完成比价 / 累计节省),按运营后台配的模式算。
|
||||
|
||||
展示模式(real/manual/random,每指标独立)与计算逻辑见 app/repositories/platform_stat.py。
|
||||
展示模式(real/manual/random,每指标独立)与计算逻辑见 app/repositories/ops_stat.py。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -12,8 +12,8 @@ import logging
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from app.api.deps import DbSession
|
||||
from app.repositories import marquee as marquee_crud
|
||||
from app.repositories import platform_stat as crud
|
||||
from app.repositories import ops_marquee as marquee_crud
|
||||
from app.repositories import ops_stat as crud
|
||||
from app.schemas.platform import PlatformStatsOut, SavingsFeedItem, SavingsFeedOut
|
||||
|
||||
logger = logging.getLogger("shagua.platform")
|
||||
|
||||
@@ -8,8 +8,8 @@ from app.models.app_config import AppConfig # noqa: F401
|
||||
from app.models.comparison import ComparisonRecord # noqa: F401
|
||||
from app.models.comparison_milestone import ComparisonMilestoneClaim # noqa: F401
|
||||
from app.models.feedback import Feedback # noqa: F401
|
||||
from app.models.marquee_seed import MarqueeSeed # noqa: F401
|
||||
from app.models.platform_stat import PlatformStatDisplay # noqa: F401
|
||||
from app.models.ops_marquee_seed import OpsMarqueeSeed # noqa: F401
|
||||
from app.models.ops_stat_config import OpsStatConfig # noqa: F401
|
||||
from app.models.price_observation import PriceObservation # noqa: F401
|
||||
from app.models.price_report import PriceReport # noqa: F401
|
||||
from app.models.savings import SavingsRecord # noqa: F401
|
||||
|
||||
@@ -21,8 +21,8 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class MarqueeSeed(Base):
|
||||
__tablename__ = "marquee_seed"
|
||||
class OpsMarqueeSeed(Base):
|
||||
__tablename__ = "ops_marquee_seed"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
# 脱敏用户名,如「用户********a52」;留空(NULL)→ feed 展示时按脱敏格式随机合成(避开撞名)
|
||||
@@ -39,4 +39,4 @@ class MarqueeSeed(Base):
|
||||
)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"<MarqueeSeed id={self.id} user={self.masked_user}>"
|
||||
return f"<OpsMarqueeSeed id={self.id} user={self.masked_user}>"
|
||||
@@ -26,8 +26,8 @@ from sqlalchemy.orm import Mapped, mapped_column
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class PlatformStatDisplay(Base):
|
||||
__tablename__ = "platform_stat_display"
|
||||
class OpsStatConfig(Base):
|
||||
__tablename__ = "ops_stat_config"
|
||||
|
||||
# help_users / total_compares / total_saved
|
||||
metric: Mapped[str] = mapped_column(String(32), primary_key=True)
|
||||
@@ -67,4 +67,4 @@ class PlatformStatDisplay(Base):
|
||||
)
|
||||
|
||||
def __repr__(self) -> str: # pragma: no cover
|
||||
return f"<PlatformStatDisplay metric={self.metric} mode={self.mode}>"
|
||||
return f"<OpsStatConfig metric={self.metric} mode={self.mode}>"
|
||||
@@ -1,6 +1,6 @@
|
||||
"""首页轮播 feed:全平台真实比价记录 + 种子兜底「混播」,以及种子表 CRUD。
|
||||
|
||||
feed 取最近的成功且省到钱的比价记录(脱敏用户名);不足 N 条时用 marquee_seed 的种子补齐,
|
||||
feed 取最近的成功且省到钱的比价记录(脱敏用户名);不足 N 条时用 ops_marquee_seed 的种子补齐,
|
||||
保证轮播永不空。真实够时种子不出现。
|
||||
|
||||
种子是「生成规则」:用户名可空(空→随机合成脱敏名,避开同屏撞名)、金额是区间(每次随机取值)、
|
||||
@@ -17,7 +17,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.rewards import CN_TZ
|
||||
from app.models.comparison import ComparisonRecord
|
||||
from app.models.marquee_seed import MarqueeSeed
|
||||
from app.models.ops_marquee_seed import OpsMarqueeSeed
|
||||
|
||||
# feed 运行时随机源(每次请求结果不同 = 轮播想要的「鲜活感」)
|
||||
_rng = random.Random()
|
||||
@@ -99,7 +99,7 @@ def get_feed(db: Session, limit: int = 8) -> list[dict]:
|
||||
need = limit - len(items)
|
||||
if need > 0:
|
||||
seeds = db.execute(
|
||||
select(MarqueeSeed).where(MarqueeSeed.enabled.is_(True))
|
||||
select(OpsMarqueeSeed).where(OpsMarqueeSeed.enabled.is_(True))
|
||||
).scalars().all()
|
||||
# 公平随机抽取 need 个(池子够大则不放回抽样;否则全用并打散),让所有启用种子都有机会露出。
|
||||
chosen = _rng.sample(seeds, need) if len(seeds) > need else list(seeds)
|
||||
@@ -126,10 +126,10 @@ def get_feed(db: Session, limit: int = 8) -> list[dict]:
|
||||
|
||||
|
||||
# ===== 运营侧:种子 CRUD =====
|
||||
def list_seeds(db: Session) -> list[MarqueeSeed]:
|
||||
def list_seeds(db: Session) -> list[OpsMarqueeSeed]:
|
||||
return list(
|
||||
db.execute(
|
||||
select(MarqueeSeed).order_by(MarqueeSeed.sort_order, MarqueeSeed.id)
|
||||
select(OpsMarqueeSeed).order_by(OpsMarqueeSeed.sort_order, OpsMarqueeSeed.id)
|
||||
).scalars().all()
|
||||
)
|
||||
|
||||
@@ -143,9 +143,9 @@ def create_seed(
|
||||
enabled: bool = True,
|
||||
sort_order: int = 0,
|
||||
commit: bool = True,
|
||||
) -> MarqueeSeed:
|
||||
) -> OpsMarqueeSeed:
|
||||
_validate_amount(min_cents, max_cents)
|
||||
seed = MarqueeSeed(
|
||||
seed = OpsMarqueeSeed(
|
||||
masked_user=(masked_user.strip() or None) if masked_user else None,
|
||||
min_cents=min_cents,
|
||||
max_cents=max_cents,
|
||||
@@ -171,9 +171,9 @@ def update_seed(
|
||||
enabled: bool | None = None,
|
||||
sort_order: int | None = None,
|
||||
commit: bool = True,
|
||||
) -> MarqueeSeed:
|
||||
) -> OpsMarqueeSeed:
|
||||
"""改种子。masked_user 约定:不传(None)=不改;传空串=清空(改回随机合成);传值=固定该名。"""
|
||||
seed = db.get(MarqueeSeed, seed_id)
|
||||
seed = db.get(OpsMarqueeSeed, seed_id)
|
||||
if seed is None:
|
||||
raise ValueError("种子不存在")
|
||||
if masked_user is not None:
|
||||
@@ -204,7 +204,7 @@ def bulk_generate(
|
||||
max_cents: int,
|
||||
enabled: bool = True,
|
||||
commit: bool = True,
|
||||
) -> list[MarqueeSeed]:
|
||||
) -> list[OpsMarqueeSeed]:
|
||||
"""批量生成 count 条「自动合成名」种子(masked_user=NULL),金额区间 [min,max]。
|
||||
冷启动铺量用;sort_order 续在当前最大值之后。
|
||||
"""
|
||||
@@ -212,11 +212,11 @@ def bulk_generate(
|
||||
raise ValueError("数量需在 1~200 之间")
|
||||
_validate_amount(min_cents, max_cents)
|
||||
base = db.execute(
|
||||
select(func.coalesce(func.max(MarqueeSeed.sort_order), 0))
|
||||
select(func.coalesce(func.max(OpsMarqueeSeed.sort_order), 0))
|
||||
).scalar_one()
|
||||
created: list[MarqueeSeed] = []
|
||||
created: list[OpsMarqueeSeed] = []
|
||||
for i in range(1, count + 1):
|
||||
seed = MarqueeSeed(
|
||||
seed = OpsMarqueeSeed(
|
||||
masked_user=None,
|
||||
min_cents=min_cents,
|
||||
max_cents=max_cents,
|
||||
@@ -235,7 +235,7 @@ def bulk_generate(
|
||||
|
||||
|
||||
def delete_seed(db: Session, seed_id: int, *, commit: bool = True) -> bool:
|
||||
seed = db.get(MarqueeSeed, seed_id)
|
||||
seed = db.get(OpsMarqueeSeed, seed_id)
|
||||
if seed is None:
|
||||
return False
|
||||
db.delete(seed)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""首页三统计(帮助用户 / 完成比价 / 累计节省)展示值计算 + 运营配置读写。
|
||||
|
||||
三种模式见 app/models/platform_stat.py。统一「定时刷新」:展示值 random_current 只在跨过
|
||||
三种模式见 app/models/ops_stat_config.py。统一「定时刷新」:展示值 random_current 只在跨过
|
||||
「北京钟点边界」时刷新一次(real 快照查库 / manual 取固定值 / random 增长)。
|
||||
|
||||
random 自增长支持两种方式(random_kind):
|
||||
@@ -23,7 +23,7 @@ from sqlalchemy import func, select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.comparison import ComparisonRecord
|
||||
from app.models.platform_stat import PlatformStatDisplay
|
||||
from app.models.ops_stat_config import OpsStatConfig
|
||||
|
||||
# 指标顺序 + 展示元信息(label/unit 给运营后台用;total_saved 单位是分,客户端 ÷100 显示元)
|
||||
METRICS = ("help_users", "total_compares", "total_saved")
|
||||
@@ -87,13 +87,13 @@ def _real_value(db: Session, metric: str) -> int:
|
||||
|
||||
|
||||
# ===== 行管理 =====
|
||||
def _ensure_rows(db: Session) -> dict[str, PlatformStatDisplay]:
|
||||
def _ensure_rows(db: Session) -> dict[str, OpsStatConfig]:
|
||||
"""保证三指标行都存在(migration 已播种;这里是缺行兜底,默认 manual + 种子值)。"""
|
||||
rows = {r.metric: r for r in db.execute(select(PlatformStatDisplay)).scalars().all()}
|
||||
rows = {r.metric: r for r in db.execute(select(OpsStatConfig)).scalars().all()}
|
||||
created = False
|
||||
for metric in METRICS:
|
||||
if metric not in rows:
|
||||
row = PlatformStatDisplay(
|
||||
row = OpsStatConfig(
|
||||
metric=metric, mode="manual", manual_value=_SEED_VALUE[metric]
|
||||
)
|
||||
db.add(row)
|
||||
@@ -111,7 +111,7 @@ def _boundary_index(epoch_utc: int, interval: int, anchor_sec: int) -> int:
|
||||
return (bj - anchor_sec) // interval
|
||||
|
||||
|
||||
def _current_target(db: Session, row: PlatformStatDisplay) -> int:
|
||||
def _current_target(db: Session, row: OpsStatConfig) -> int:
|
||||
"""某模式此刻「应有的展示值」(用于播种 / real·manual 的定时快照)。"""
|
||||
if row.mode == "manual":
|
||||
return max(0, int(row.manual_value or 0))
|
||||
@@ -122,14 +122,14 @@ def _current_target(db: Session, row: PlatformStatDisplay) -> int:
|
||||
return max(0, int(_real_value(db, row.metric)))
|
||||
|
||||
|
||||
def _monotonic(row: PlatformStatDisplay, new_val: int) -> int:
|
||||
def _monotonic(row: OpsStatConfig, new_val: int) -> int:
|
||||
"""只增不减护栏:未开 allow_decrease 且已有展示值时,新值不得低于当前值(否则保持当前)。"""
|
||||
if not row.allow_decrease and row.random_current is not None and new_val < row.random_current:
|
||||
return row.random_current
|
||||
return new_val
|
||||
|
||||
|
||||
def _grow(row: PlatformStatDisplay, val: int, periods: int) -> int:
|
||||
def _grow(row: OpsStatConfig, val: int, periods: int) -> int:
|
||||
"""random 自增长:按 random_kind 走 periods 个周期(add=逐周期加随机增量 / mult=逐周期乘随机倍率)。"""
|
||||
if row.random_kind == "add":
|
||||
lo = max(0, row.random_step_min)
|
||||
@@ -144,7 +144,7 @@ def _grow(row: PlatformStatDisplay, val: int, periods: int) -> int:
|
||||
return val
|
||||
|
||||
|
||||
def _refresh(db: Session, row: PlatformStatDisplay) -> int:
|
||||
def _refresh(db: Session, row: OpsStatConfig) -> int:
|
||||
"""统一「定时刷新」:展示值 random_current 只在跨过「北京钟点边界」时刷新一次。
|
||||
real→重新快照(真实+偏移,受只增不减护栏)/ manual→取固定值(受护栏)/ random→按方式增长。
|
||||
无初值则按模式播种。返回当前展示值(基础单位)。
|
||||
@@ -183,7 +183,7 @@ def get_display_values(db: Session) -> dict[str, int]:
|
||||
|
||||
|
||||
# ===== 运营侧:配置读写 =====
|
||||
def _snapshot(row: PlatformStatDisplay) -> dict:
|
||||
def _snapshot(row: OpsStatConfig) -> dict:
|
||||
"""审计 / 返回用的行快照。"""
|
||||
return {
|
||||
"mode": row.mode,
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 所属:Admin 组(前缀 `/admin/api/dashboard-display`) | 鉴权:Admin Bearer(改需 operator/super) | [← 返回 API 索引](./README.md)
|
||||
|
||||
配置客户端首页三个门面数字(帮助用户 / 完成比价 / 累计节省)的展示模式。每个指标可独立选 real/manual/random。用户侧读取见 [platform-stats](./platform-stats.md);表见 [platform_stat_display](../database/platform_stat_display.md)。
|
||||
配置客户端首页三个门面数字(帮助用户 / 完成比价 / 累计节省)的展示模式。每个指标可独立选 real/manual/random。用户侧读取见 [platform-stats](./platform-stats.md);表见 [ops_stat_config](../database/ops_stat_config.md)。
|
||||
|
||||
> **单位约定**:倍率用**千分比整数**(1.000→`1000`、1.100→`1100`)。`total_saved` 的 `manual_value` / `random_current` / `random_initial` / `random_step_min` / `random_step_max` / `real_offset` 单位都是**分**(前端展示时 ÷100 取元);两个计数指标是个数。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
## GET /admin/api/dashboard-display — 三指标配置 + 真实值预览
|
||||
|
||||
### 出参
|
||||
响应 `200`:`list[PlatformStatItemOut]`(按 help_users / total_compares / total_saved 顺序)
|
||||
响应 `200`:`list[OpsStatItemOut]`(按 help_users / total_compares / total_saved 顺序)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
`metric` 路径参数取 `help_users` / `total_compares` / `total_saved`。
|
||||
|
||||
### 入参 `PlatformStatUpdateRequest`(字段均可选,只改传了的)
|
||||
### 入参 `OpsStatUpdateRequest`(字段均可选,只改传了的)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `mode` | string | `real` / `manual` / `random` |
|
||||
@@ -58,7 +58,7 @@
|
||||
| `apply_now` | bool | 立即更新:不等更新钟点,保存后马上刷新一次(random 走一档,real/manual 取应有值,均经护栏) |
|
||||
|
||||
### 出参
|
||||
响应 `200`:更新后的 `PlatformStatItemOut`(同上)。
|
||||
响应 `200`:更新后的 `OpsStatItemOut`(同上)。
|
||||
|
||||
### 错误
|
||||
- `400`:mode/kind 非法 / 倍率越界(<1000 或 >1500)/ 上限<下限 / 增量负值或上限<下限 / 偏移负值 / tick<60 / 更新时间越界 / manual 负值。
|
||||
@@ -69,5 +69,5 @@
|
||||
- **统一定时刷新**:三模式的展示值(`random_current`)只在「更新时间(按更新间隔)」对齐的北京钟点刷新一次——real 快照查库(+偏移)/ manual 取固定值 / random 按 `random_kind` 走一档(`mult` ×倍率 / `add` +增量)。`random_tick_seconds`+`random_anchor_minutes` 三模式通用。
|
||||
- **只增不减护栏**(`allow_decrease=false`,默认):real/manual 刷新时新目标值低于当前展示值则保持不降;要下调须显式传 `allow_decrease=true`。
|
||||
- 改 mode/manual/倍率/增量/偏移等到**下个更新钟点**才在客户端生效(展示值不立即变);`random_initial` 立即设起点并重置;`apply_now=true` 立即刷新一次。首次无展示值时按当前模式播种。
|
||||
- 每次改动写 `admin_audit_log`(action=`dashboard_display.set`,detail 含改前/改后快照)。
|
||||
- 每次改动写 `admin_audit_log`(action=`ops_stat_config.set`,detail 含改前/改后快照)。
|
||||
- 客户端在**进首页 / 回前台时**重拉 `/stats` 取最新展示值。
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
> 所属:Admin 组(前缀 `/admin/api/marquee-seeds`) | 鉴权:Admin Bearer(改需 operator/super) | [← 返回 API 索引](./README.md)
|
||||
|
||||
管理首页轮播「真实+种子混播」的兜底种子。种子是「生成规则」:`masked_user` 可空(空→feed 随机合成名)、金额是 `[min_cents, max_cents]` 区间(feed 每次随机取值)。用户侧 feed 见 [platform-savings-feed](./platform-savings-feed.md);表见 [marquee_seed](../database/marquee_seed.md)。金额单位:分(前端 ÷100 显示元)。
|
||||
管理首页轮播「真实+种子混播」的兜底种子。种子是「生成规则」:`masked_user` 可空(空→feed 随机合成名)、金额是 `[min_cents, max_cents]` 区间(feed 每次随机取值)。用户侧 feed 见 [platform-savings-feed](./platform-savings-feed.md);表见 [ops_marquee_seed](../database/ops_marquee_seed.md)。金额单位:分(前端 ÷100 显示元)。
|
||||
|
||||
## 复用结构 MarqueeSeedOut
|
||||
## 复用结构 OpsMarqueeSeedOut
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `id` | int | |
|
||||
@@ -16,7 +16,7 @@
|
||||
| `created_at` | string \| null | 创建时间(ISO) |
|
||||
|
||||
## GET /admin/api/marquee-seeds — 种子列表
|
||||
出参 `200`:`list[MarqueeSeedOut]`(按 `sort_order,id`)。
|
||||
出参 `200`:`list[OpsMarqueeSeedOut]`(按 `sort_order,id`)。
|
||||
|
||||
## GET /admin/api/marquee-seeds/preview — 预览实际混播 feed
|
||||
预览客户端实际会看到的轮播(真实记录会插队、种子随机抽取 / 金额随机 / 名字合成),供运营对效果。**含随机,每次结果不同**。
|
||||
@@ -24,22 +24,22 @@
|
||||
- 出参 `200`:`{"items": [{masked_user, saved_amount_cents, time}]}`(条目同 [platform-savings-feed](./platform-savings-feed.md))
|
||||
|
||||
## POST /admin/api/marquee-seeds — 新增(带审计)
|
||||
入参 `MarqueeSeedCreate`:`masked_user`(可选,空 / 不传 → 随机合成)、`min_cents`(必填,≥0)、`max_cents`(必填,≥min,≤1000 元)、`enabled`(默认 true)、`sort_order`(默认 0)。出参:新建的 `MarqueeSeedOut`。`400`=金额非法。
|
||||
入参 `OpsMarqueeSeedCreate`:`masked_user`(可选,空 / 不传 → 随机合成)、`min_cents`(必填,≥0)、`max_cents`(必填,≥min,≤1000 元)、`enabled`(默认 true)、`sort_order`(默认 0)。出参:新建的 `OpsMarqueeSeedOut`。`400`=金额非法。
|
||||
|
||||
## POST /admin/api/marquee-seeds/bulk — 批量生成(带审计)
|
||||
冷启动铺量:一次生成 `count` 条「自动合成名」(masked_user=null)种子,金额均为给定区间,`sort_order` 续在当前最大值之后。
|
||||
- 入参 `MarqueeSeedBulkCreate`:`count`(1~200)、`min_cents`、`max_cents`、`enabled`(默认 true)
|
||||
- 出参 `200`:`list[MarqueeSeedOut]`(新建的 count 条)。`400`=数量 / 金额非法。
|
||||
- 入参 `OpsMarqueeSeedBulkCreate`:`count`(1~200)、`min_cents`、`max_cents`、`enabled`(默认 true)
|
||||
- 出参 `200`:`list[OpsMarqueeSeedOut]`(新建的 count 条)。`400`=数量 / 金额非法。
|
||||
|
||||
## PATCH /admin/api/marquee-seeds/{seed_id} — 改(带审计)
|
||||
入参 `MarqueeSeedUpdate`(均可选):`masked_user` / `min_cents` / `max_cents` / `enabled` / `sort_order`。
|
||||
入参 `OpsMarqueeSeedUpdate`(均可选):`masked_user` / `min_cents` / `max_cents` / `enabled` / `sort_order`。
|
||||
- `masked_user` 约定:**不传**=不改;**传空串**=清空(改回随机合成);**传值**=固定该名。
|
||||
- 只改单边 `min_cents` / `max_cents` 时,与现有的另一边一起做「上限≥下限」校验。
|
||||
出参:更新后的 `MarqueeSeedOut`。`404`=种子不存在;`400`=金额非法。
|
||||
出参:更新后的 `OpsMarqueeSeedOut`。`404`=种子不存在;`400`=金额非法。
|
||||
|
||||
## DELETE /admin/api/marquee-seeds/{seed_id} — 删(带审计)
|
||||
出参 `200`:`{"deleted": true}`。`404`=种子不存在。
|
||||
|
||||
## 说明
|
||||
- 写操作需 operator(super 恒可),均写 `admin_audit_log`(action=`marquee_seed.create` / `update` / `delete` / `bulk_generate`)。预览为只读,任意已登录 admin 可用。
|
||||
- 写操作需 operator(super 恒可),均写 `admin_audit_log`(action=`ops_marquee_seed.create` / `update` / `delete` / `bulk_generate`)。预览为只读,任意已登录 admin 可用。
|
||||
- 改动客户端**进首页 / 回前台**重拉 feed 时生效。
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 所属:Platform 组(前缀 `/api/v1/platform`) | 鉴权:**无** | [← 返回 API 索引](./README.md)
|
||||
|
||||
客户端首页顶部「用户****xxx 比价后节省 xx 元」滚动条数据源。全平台真实比价记录优先;不足时用运营配的种子([marquee_seed](../database/marquee_seed.md))补齐到 `limit` 条「混播」,保证轮播不空。
|
||||
客户端首页顶部「用户****xxx 比价后节省 xx 元」滚动条数据源。全平台真实比价记录优先;不足时用运营配的种子([ops_marquee_seed](../database/ops_marquee_seed.md))补齐到 `limit` 条「混播」,保证轮播不空。
|
||||
|
||||
## 入参
|
||||
| 参数 | 类型 | 说明 |
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
## 说明
|
||||
- 真实条:`comparison_record` 中 `status='success'` 且 `0 < saved_amount_cents ≤ 300 元` 的近期记录(金额超 300 元视为异常 / bug 值剔除,防「节省 999 元」穿帮),**按 `user_id` 去重**(同一用户只取最新一条,避免单人刷屏);用户名按 `user_id` 哈希脱敏(`用户********`+3 位)。
|
||||
- 种子条:`marquee_seed`(`enabled=true`),仅在真实去重后不足 `limit` 时补齐;**从启用种子中公平随机抽取**(不再固定取前 N,所有种子都有机会露出)。每条种子:用户名留空则按脱敏格式**随机合成并避开同屏撞名**,金额在 `[min_cents, max_cents]` 区间**随机取值**(固定金额则 min==max)。
|
||||
- 种子条:`ops_marquee_seed`(`enabled=true`),仅在真实去重后不足 `limit` 时补齐;**从启用种子中公平随机抽取**(不再固定取前 N,所有种子都有机会露出)。每条种子:用户名留空则按脱敏格式**随机合成并避开同屏撞名**,金额在 `[min_cents, max_cents]` 区间**随机取值**(固定金额则 min==max)。
|
||||
- **`time` 为合成的「最近」时间**(从当前北京时间往前递减,首条约 20 秒前,其余每条 2~6 分钟):社会证明轮播保证永远像刚发生,不受旧测试数据 / 低谷期记录影响。真实的用户/金额不变,只换展示时间。
|
||||
- 因含随机(抽取 / 金额 / 合成名),**每次请求结果都不同**——这是轮播想要的鲜活感。
|
||||
- 逻辑见 `app/repositories/marquee.py`;运营管理种子见 [admin-marquee-seeds](./admin-marquee-seeds.md)。
|
||||
- 逻辑见 `app/repositories/ops_marquee.py`;运营管理种子见 [admin-marquee-seeds](./admin-marquee-seeds.md)。
|
||||
|
||||
@@ -23,6 +23,6 @@
|
||||
- **random**:在上次展示值上按方式增长——`mult` ×一个 [1.0,1.5] 随机倍率 / `add` +一个 [下限,上限] 随机绝对增量(跨几个钟点走几次)。
|
||||
- **只增不减**:默认门面数字不回退(real/manual 新值低于当前则保持),除非运营开「允许下降」。
|
||||
|
||||
计算逻辑见 `app/repositories/platform_stat.py`,模型见 `app/models/platform_stat.py`(表 [platform_stat_display](../database/platform_stat_display.md))。
|
||||
计算逻辑见 `app/repositories/ops_stat.py`,模型见 `app/models/ops_stat_config.py`(表 [ops_stat_config](../database/ops_stat_config.md))。
|
||||
|
||||
上线初始播种为 manual + 客户端原写死门面值(12847 / 86532 / 37621.40 元),保证前后展示一致。
|
||||
|
||||
@@ -44,8 +44,8 @@
|
||||
### 首页门面数据
|
||||
| 表 | 用途 | 模型 | 文档 |
|
||||
|---|---|---|---|
|
||||
| `platform_stat_display` | 首页三统计展示配置(real/manual/random) | `models/platform_stat.py` | [详情](./platform_stat_display.md) |
|
||||
| `marquee_seed` | 首页轮播种子(真实不足时兜底混播) | `models/marquee_seed.py` | [详情](./marquee_seed.md) |
|
||||
| `ops_stat_config` | 首页三统计展示配置(real/manual/random) | `models/ops_stat_config.py` | [详情](./ops_stat_config.md) |
|
||||
| `ops_marquee_seed` | 首页轮播种子(真实不足时兜底混播) | `models/ops_marquee_seed.py` | [详情](./ops_marquee_seed.md) |
|
||||
|
||||
### 运营后台 admin(独立子应用 `app/admin/`,独立鉴权)
|
||||
| 表 | 用途 | 模型 | 文档 |
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# marquee_seed — 首页轮播种子(生成规则)
|
||||
# ops_marquee_seed — 首页轮播种子(生成规则)
|
||||
|
||||
> 模型 `app/models/marquee_seed.py` | 关联接口 [platform-savings-feed](../api/platform-savings-feed.md) / [admin-marquee-seeds](../api/admin-marquee-seeds.md) | [← 表索引](./README.md)
|
||||
> 模型 `app/models/ops_marquee_seed.py` | 关联接口 [platform-savings-feed](../api/platform-savings-feed.md) / [admin-marquee-seeds](../api/admin-marquee-seeds.md) | [← 表索引](./README.md)
|
||||
|
||||
首页「用户****xxx 比价后节省 xx 元」轮播的兜底假数据。轮播真实数据源是全平台比价记录,真实不足 N 条时用本表启用的种子补齐「混播」,保证轮播不空。种子是「生成规则」而非死记录:用户名可空(→随机合成)、金额是区间(→随机取值)。运营后台增删改。逻辑见 `app/repositories/marquee.py`。
|
||||
首页「用户****xxx 比价后节省 xx 元」轮播的兜底假数据。轮播真实数据源是全平台比价记录,真实不足 N 条时用本表启用的种子补齐「混播」,保证轮播不空。种子是「生成规则」而非死记录:用户名可空(→随机合成)、金额是区间(→随机取值)。运营后台增删改。逻辑见 `app/repositories/ops_marquee.py`。
|
||||
|
||||
## 字段
|
||||
| 列 | 类型 | 约束 / 默认 | 说明 |
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
## 初始数据(migration 播种)
|
||||
6 条 = 客户端原写死的轮播条目(金额转分、`min==max` 固定值):a52/18.60、k89/42.40、m71/7.90、p46/156.30、r93/23.50、c28/89.40。
|
||||
(`marquee_seed02` 迁移把旧单值列 `saved_amount_cents` 拆成 `min_cents=max_cents`,并把 `masked_user` 改为可空。)
|
||||
(`marquee_seed02` 迁移把旧单值列 `saved_amount_cents` 拆成 `min_cents=max_cents`,并把 `masked_user` 改为可空;`opsrename01` 迁移把表名由 `marquee_seed` 改为 `ops_marquee_seed`,与代码 ops_ 前缀对齐,路由 `/admin/api/marquee-seeds` 不变。)
|
||||
|
||||
## 说明
|
||||
- feed 取真实(`comparison_record` success 且 `0<saved≤300 元`,按 user 去重)优先,不足用本表启用种子**公平随机抽取**补齐(不再固定取前 N,所有种子都有机会露出)。种子用户名空则随机合成、金额区间随机取值。展示时间统一刷新成「相对现在的最近时刻」(详见 [platform-savings-feed](../api/platform-savings-feed.md))。
|
||||
@@ -1,8 +1,8 @@
|
||||
# platform_stat_display — 首页三统计展示配置
|
||||
# ops_stat_config — 首页三统计展示配置
|
||||
|
||||
> 模型 `app/models/platform_stat.py` | 关联接口 [platform-stats](../api/platform-stats.md) / [admin-dashboard-display](../api/admin-dashboard-display.md) | [← 表索引](./README.md)
|
||||
> 模型 `app/models/ops_stat_config.py` | 关联接口 [platform-stats](../api/platform-stats.md) / [admin-dashboard-display](../api/admin-dashboard-display.md) | [← 表索引](./README.md)
|
||||
|
||||
客户端首页三个门面数字(帮助用户 / 完成比价 / 累计节省)的展示配置。一行一指标(主键=`metric`),每指标可独立选 real/manual/random 三种模式。计算逻辑见 `app/repositories/platform_stat.py`。
|
||||
客户端首页三个门面数字(帮助用户 / 完成比价 / 累计节省)的展示配置。一行一指标(主键=`metric`),每指标可独立选 real/manual/random 三种模式。计算逻辑见 `app/repositories/ops_stat.py`。
|
||||
|
||||
> **单位**:倍率用**千分比整数**(1.000→`1000`);`total_saved` 指标的 `manual_value` / `random_current` 单位是**分**,两个计数指标是**个数**。
|
||||
|
||||
@@ -50,4 +50,5 @@
|
||||
## 说明
|
||||
- real 口径仅统计 `comparison_record` 中 `status='success'` 的记录(去重用户数 / 记录数)。`total_saved` 的求和**只计入 `0 < saved_amount_cents ≤ 300 元` 的条目**(剔除 bug 异常大值,与轮播口径一致,防一条异常配合「只增不减」永久撑高门面)。
|
||||
- `倍率上限收紧为 1.500`(单次 ×1.5,原 5.0 太大一跳就假)。
|
||||
- 表名 `ops_stat_config`(`opsrename01` 迁移由 `platform_stat_display` 改名,与代码 ops_ 前缀对齐;路由 `/admin/api/dashboard-display` 不变)。
|
||||
- 改配置走 admin `PATCH /admin/api/dashboard-display/{metric}`,带审计。
|
||||
Reference in New Issue
Block a user