diff --git a/alembic/versions/ops_rename_tables.py b/alembic/versions/ops_rename_tables.py new file mode 100644 index 0000000..bb19023 --- /dev/null +++ b/alembic/versions/ops_rename_tables.py @@ -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') diff --git a/app/admin/main.py b/app/admin/main.py index 2cdb21c..74a06bb 100644 --- a/app/admin/main.py +++ b/app/admin/main.py @@ -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) diff --git a/app/admin/routers/marquee_seed.py b/app/admin/routers/ops_marquee_seed.py similarity index 67% rename from app/admin/routers/marquee_seed.py rename to app/admin/routers/ops_marquee_seed.py index df4a6f8..53084ff 100644 --- a/app/admin/routers/marquee_seed.py +++ b/app/admin/routers/ops_marquee_seed.py @@ -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() diff --git a/app/admin/routers/dashboard_display.py b/app/admin/routers/ops_stat_config.py similarity index 68% rename from app/admin/routers/dashboard_display.py rename to app/admin/routers/ops_stat_config.py index 69ce72d..f422ae6 100644 --- a/app/admin/routers/dashboard_display.py +++ b/app/admin/routers/ops_stat_config.py @@ -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) diff --git a/app/admin/schemas/marquee_seed.py b/app/admin/schemas/ops_marquee_seed.py similarity index 80% rename from app/admin/schemas/marquee_seed.py rename to app/admin/schemas/ops_marquee_seed.py index b144020..6095318 100644 --- a/app/admin/schemas/marquee_seed.py +++ b/app/admin/schemas/ops_marquee_seed.py @@ -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] diff --git a/app/admin/schemas/dashboard_display.py b/app/admin/schemas/ops_stat_config.py similarity index 95% rename from app/admin/schemas/dashboard_display.py rename to app/admin/schemas/ops_stat_config.py index 7913135..0a3a473 100644 --- a/app/admin/schemas/dashboard_display.py +++ b/app/admin/schemas/ops_stat_config.py @@ -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 diff --git a/app/api/v1/platform.py b/app/api/v1/platform.py index 4d17bb5..5c3c9d1 100644 --- a/app/api/v1/platform.py +++ b/app/api/v1/platform.py @@ -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") diff --git a/app/models/__init__.py b/app/models/__init__.py index 43772fb..ca2d6d1 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -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 diff --git a/app/models/marquee_seed.py b/app/models/ops_marquee_seed.py similarity index 94% rename from app/models/marquee_seed.py rename to app/models/ops_marquee_seed.py index bbc7970..2e64eb8 100644 --- a/app/models/marquee_seed.py +++ b/app/models/ops_marquee_seed.py @@ -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"" + return f"" diff --git a/app/models/platform_stat.py b/app/models/ops_stat_config.py similarity index 96% rename from app/models/platform_stat.py rename to app/models/ops_stat_config.py index b7d5fcd..1681de9 100644 --- a/app/models/platform_stat.py +++ b/app/models/ops_stat_config.py @@ -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"" + return f"" diff --git a/app/repositories/marquee.py b/app/repositories/ops_marquee.py similarity index 92% rename from app/repositories/marquee.py rename to app/repositories/ops_marquee.py index 999a53c..6eb0c27 100644 --- a/app/repositories/marquee.py +++ b/app/repositories/ops_marquee.py @@ -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) diff --git a/app/repositories/platform_stat.py b/app/repositories/ops_stat.py similarity index 95% rename from app/repositories/platform_stat.py rename to app/repositories/ops_stat.py index 3c537a0..5dfd7f9 100644 --- a/app/repositories/platform_stat.py +++ b/app/repositories/ops_stat.py @@ -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, diff --git a/docs/api/admin-dashboard-display.md b/docs/api/admin-dashboard-display.md index c753b03..70463b2 100644 --- a/docs/api/admin-dashboard-display.md +++ b/docs/api/admin-dashboard-display.md @@ -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` 取最新展示值。 diff --git a/docs/api/admin-marquee-seeds.md b/docs/api/admin-marquee-seeds.md index b130785..a0b9566 100644 --- a/docs/api/admin-marquee-seeds.md +++ b/docs/api/admin-marquee-seeds.md @@ -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 时生效。 diff --git a/docs/api/platform-savings-feed.md b/docs/api/platform-savings-feed.md index 2aeb544..156fa1e 100644 --- a/docs/api/platform-savings-feed.md +++ b/docs/api/platform-savings-feed.md @@ -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)。 diff --git a/docs/api/platform-stats.md b/docs/api/platform-stats.md index 9c154f9..25ced25 100644 --- a/docs/api/platform-stats.md +++ b/docs/api/platform-stats.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 元),保证前后展示一致。 diff --git a/docs/database/README.md b/docs/database/README.md index 1c0a8de..9220687 100644 --- a/docs/database/README.md +++ b/docs/database/README.md @@ -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/`,独立鉴权) | 表 | 用途 | 模型 | 文档 | diff --git a/docs/database/marquee_seed.md b/docs/database/ops_marquee_seed.md similarity index 78% rename from docs/database/marquee_seed.md rename to docs/database/ops_marquee_seed.md index badca3d..639ec2e 100644 --- a/docs/database/marquee_seed.md +++ b/docs/database/ops_marquee_seed.md @@ -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 模型 `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}`,带审计。