a2270ee1b2
新手引导视频:运营后台上传 MP4(上限 100MB,魔数校验只认 ISO BMFF), App 端在领券等候浮层前 N 次以引导视频替代广告。新增 guide_video 的 model/schema/repository/router(App 侧 + 后台侧)与播放记录表迁移。 美团券:首页「销量最高 / 智能推荐」两个 tab 改游标分页,配套两条 (city_id, dedup_key, 排序键 DESC) 复合索引,让 Postgres 顺着索引流式 去重,免掉每翻一页重排整城券的开销。美团 CPS client 在 lifespan 预热 并在关闭时释放连接池。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
"""admin 新手引导视频配置:读 / 改开关次数金币 / 上传视频 / 删视频(带审计)。
|
|
|
|
整份配置存通用 app_config 表(见 app/repositories/guide_video.py),App 领券等候浮层
|
|
每次展示前调 POST /api/v1/guide-video/start 同步。权限:operator 可改(运营维护),
|
|
super 恒可;读为只读(任意已登录 admin)。
|
|
|
|
⚠️ 视频上限 100MB(settings.GUIDE_VIDEO_MAX_BYTES),已在 admin nginx 为本接口单独放宽
|
|
client_max_body_size,见 shaguabijia-admin-web/deploy/nginx/admin.shaguabijia.com.conf。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Annotated
|
|
|
|
from fastapi import APIRouter, Depends, File, HTTPException, Request, UploadFile
|
|
|
|
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.guide_video import GuideVideoConfigOut, GuideVideoConfigUpdate
|
|
from app.core import media
|
|
from app.models.admin import AdminUser
|
|
from app.repositories import guide_video
|
|
|
|
router = APIRouter(
|
|
prefix="/admin/api/guide-video",
|
|
tags=["admin-guide-video"],
|
|
dependencies=[Depends(get_current_admin)],
|
|
)
|
|
|
|
|
|
def _out(db: AdminDb) -> GuideVideoConfigOut:
|
|
"""配置 + 播放统计合成响应(四个写接口都以最新状态返回,前端一次同步到位)。"""
|
|
return GuideVideoConfigOut(**guide_video.get_config(db), **guide_video.play_stats(db))
|
|
|
|
|
|
@router.get("", response_model=GuideVideoConfigOut, summary="新手引导视频配置(领券浮层)")
|
|
def get_config(db: AdminDb) -> GuideVideoConfigOut:
|
|
return _out(db)
|
|
|
|
|
|
@router.patch("", response_model=GuideVideoConfigOut, summary="改开关/次数/金币(带审计)")
|
|
def update_config(
|
|
body: GuideVideoConfigUpdate,
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> GuideVideoConfigOut:
|
|
before, after = guide_video.update_config(
|
|
db,
|
|
enabled=body.enabled,
|
|
max_plays=body.max_plays,
|
|
reward_coin=body.reward_coin,
|
|
admin_id=admin.id,
|
|
commit=False,
|
|
)
|
|
write_audit(
|
|
db, admin, action="guide_video.update", target_type="guide_video", target_id=None,
|
|
detail={"before": before, "after": after}, ip=get_client_ip(request), commit=False,
|
|
)
|
|
db.commit()
|
|
return _out(db)
|
|
|
|
|
|
@router.post("/video", response_model=GuideVideoConfigOut, summary="上传新手引导视频(MP4,带审计)")
|
|
async def upload_video(
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
file: UploadFile = File(...),
|
|
) -> GuideVideoConfigOut:
|
|
data = await file.read()
|
|
try:
|
|
url = media.save_guide_video(data)
|
|
except media.MediaError as e:
|
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
before, after = guide_video.set_video(db, url, admin_id=admin.id, commit=False)
|
|
write_audit(
|
|
db, admin, action="guide_video.set_video", target_type="guide_video", target_id=None,
|
|
detail={"before": before.get("video_url"), "after": url, "bytes": len(data)},
|
|
ip=get_client_ip(request), commit=False,
|
|
)
|
|
db.commit()
|
|
# 提交成功后再删旧片,避免新片没落库就把旧片丢了
|
|
media.delete_guide_video(before.get("video_url"))
|
|
return _out(db)
|
|
|
|
|
|
@router.delete("/video", response_model=GuideVideoConfigOut, summary="移除新手引导视频(带审计)")
|
|
def delete_video(
|
|
request: Request,
|
|
admin: Annotated[AdminUser, Depends(require_role("operator"))],
|
|
db: AdminDb,
|
|
) -> GuideVideoConfigOut:
|
|
"""移除后 /guide-video/start 一律返回 should_play=false,领券浮层回到「只放广告」。"""
|
|
before, after = guide_video.set_video(db, None, admin_id=admin.id, commit=False)
|
|
write_audit(
|
|
db, admin, action="guide_video.delete_video", target_type="guide_video", target_id=None,
|
|
detail={"before": before.get("video_url")}, ip=get_client_ip(request), commit=False,
|
|
)
|
|
db.commit()
|
|
media.delete_guide_video(before.get("video_url"))
|
|
return _out(db)
|