c9578f5ba2
轮播 feed(marquee_seed,全新): - 新增 marquee_seed 表 + admin CRUD/批量生成/预览;真实比价记录(success 且 0<省额≤300元、按 user 去重)优先,种子兜底混播 - 种子为「生成规则」:用户名可空(空则按脱敏格式随机合成、避开同屏撞名),金额改 [min,max] 区间随机,feed 公平随机抽取(不看 sort_order) - 新增 GET /api/v1/platform/savings-feed(无鉴权);展示时间统一刷新为相对当前的最近时刻 三统计配置(platform_stat_display)增强: - 自增长新增「绝对增量」方式(random_kind=add,每周期 +[step_min,step_max]) - 真实值模式加基数偏移(real_offset,展示=真实+偏移) - 只增不减护栏(allow_decrease 默认关,real/manual 不回退防门面缩水) - 累计节省 real 口径只计 0<单条≤300元防虚高;倍率上限 5.0→1.5;PATCH 支持 apply_now 立即更新 迁移:marquee_seed_table / marquee_seed_range / platform_stat_growth_offset(均可逆,已验证) 文档:API/DB 对应文档同步更新 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
161 lines
5.2 KiB
Python
161 lines
5.2 KiB
Python
"""admin 首页轮播种子管理:列出 / 新增 / 改 / 删 / 批量生成 / 预览(带审计)。
|
|
|
|
种子用于首页轮播「真实+种子混播」的兜底(见 app/repositories/marquee.py)。
|
|
权限:operator 可改,super 恒可;预览为只读(任意已登录 admin)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
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.models.admin import AdminUser
|
|
from app.models.marquee_seed import MarqueeSeed
|
|
from app.repositories import marquee
|
|
|
|
router = APIRouter(
|
|
prefix="/admin/api/marquee-seeds",
|
|
tags=["admin-marquee-seeds"],
|
|
dependencies=[Depends(get_current_admin)],
|
|
)
|
|
|
|
|
|
def _out(seed: MarqueeSeed) -> MarqueeSeedOut:
|
|
return MarqueeSeedOut(
|
|
id=seed.id,
|
|
masked_user=seed.masked_user,
|
|
min_cents=seed.min_cents,
|
|
max_cents=seed.max_cents,
|
|
enabled=seed.enabled,
|
|
sort_order=seed.sort_order,
|
|
created_at=seed.created_at.isoformat() if seed.created_at else None,
|
|
)
|
|
|
|
|
|
@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("/preview", response_model=SavingsFeedPreviewOut, summary="预览实际混播 feed")
|
|
def preview_feed(
|
|
db: AdminDb,
|
|
limit: Annotated[int, Query(ge=1, le=30)] = 8,
|
|
) -> SavingsFeedPreviewOut:
|
|
"""返回客户端实际会看到的 feed(真实记录会插队、种子随机抽取/金额随机/名字合成),供运营对效果。"""
|
|
return SavingsFeedPreviewOut(items=marquee.get_feed(db, limit=limit))
|
|
|
|
|
|
@router.post("", response_model=MarqueeSeedOut, summary="新增轮播种子(带审计)")
|
|
def create_seed(
|
|
body: MarqueeSeedCreate,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> MarqueeSeedOut:
|
|
try:
|
|
seed = marquee.create_seed(
|
|
db,
|
|
masked_user=body.masked_user,
|
|
min_cents=body.min_cents,
|
|
max_cents=body.max_cents,
|
|
enabled=body.enabled,
|
|
sort_order=body.sort_order,
|
|
commit=False,
|
|
)
|
|
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",
|
|
target_id=None, detail=body.model_dump(), ip=get_client_ip(request), commit=False,
|
|
)
|
|
db.commit()
|
|
db.refresh(seed)
|
|
return _out(seed)
|
|
|
|
|
|
@router.post("/bulk", response_model=list[MarqueeSeedOut], summary="批量生成种子(带审计)")
|
|
def bulk_generate(
|
|
body: MarqueeSeedBulkCreate,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> list[MarqueeSeedOut]:
|
|
try:
|
|
seeds = marquee.bulk_generate(
|
|
db,
|
|
count=body.count,
|
|
min_cents=body.min_cents,
|
|
max_cents=body.max_cents,
|
|
enabled=body.enabled,
|
|
commit=False,
|
|
)
|
|
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",
|
|
target_id=None, detail=body.model_dump(), ip=get_client_ip(request), commit=False,
|
|
)
|
|
db.commit()
|
|
for s in seeds:
|
|
db.refresh(s)
|
|
return [_out(s) for s in seeds]
|
|
|
|
|
|
@router.patch("/{seed_id}", response_model=MarqueeSeedOut, summary="改轮播种子(带审计)")
|
|
def update_seed(
|
|
seed_id: int,
|
|
body: MarqueeSeedUpdate,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> MarqueeSeedOut:
|
|
try:
|
|
seed = marquee.update_seed(
|
|
db,
|
|
seed_id,
|
|
masked_user=body.masked_user,
|
|
min_cents=body.min_cents,
|
|
max_cents=body.max_cents,
|
|
enabled=body.enabled,
|
|
sort_order=body.sort_order,
|
|
commit=False,
|
|
)
|
|
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",
|
|
target_id=seed_id, detail=body.model_dump(exclude_none=True),
|
|
ip=get_client_ip(request), commit=False,
|
|
)
|
|
db.commit()
|
|
db.refresh(seed)
|
|
return _out(seed)
|
|
|
|
|
|
@router.delete("/{seed_id}", summary="删轮播种子(带审计)")
|
|
def delete_seed(
|
|
seed_id: int,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> dict:
|
|
ok = 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",
|
|
target_id=seed_id, detail=None, ip=get_client_ip(request), commit=False,
|
|
)
|
|
db.commit()
|
|
return {"deleted": True}
|