"""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)