"""admin「领券数据」看板:发起/完成数 + 领券耗时(均值 + P5/P50/P95/P99)+ 按天趋势 + 逐条明细。 任意已登录 admin 可看(只读)。聚合逻辑在 app/admin/repositories/coupon_data.py。 数据源 coupon_session(客户端 /api/v1/coupon/session 两段上报)。 """ from __future__ import annotations from datetime import date as _date from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query from app.admin.deps import AdminDb, get_current_admin from app.admin.repositories import coupon_data from app.admin.schemas.coupon_data import ( CouponDataDaily, CouponDataHourly, CouponDataOut, CouponDataRow, CouponDataSummary, CouponUserRecordsOut, ) from app.core.rewards import cn_today router = APIRouter( prefix="/admin/api/coupon-data", tags=["admin-coupon-data"], dependencies=[Depends(get_current_admin)], ) # 区间最大跨度(天);超出拒绝,避免一次拉过多天拖垮接口(对齐广告收益报表)。 _MAX_RANGE_DAYS = 92 def _parse_day(value: str | None, *, field: str, default: _date) -> _date: if value is None: return default try: return _date.fromisoformat(value) except ValueError as e: raise HTTPException(status_code=422, detail=f"{field} 需为 YYYY-MM-DD") from e @router.get( "", response_model=CouponDataOut, summary="领券数据看板(发起/完成数 + 耗时分位 + 按天趋势 + 逐条明细)", ) def get_coupon_data( db: AdminDb, date_from: Annotated[str | None, Query(description="起始日 北京 YYYY-MM-DD,默认今天")] = None, date_to: Annotated[str | None, Query(description="结束日 北京 YYYY-MM-DD,闭区间,默认=date_from")] = None, user: Annotated[str | None, Query(description="用户手机号/昵称模糊搜;不传=全部")] = None, app_env: Annotated[str, Query(description="prod(默认) / dev / all(全部环境)")] = "prod", granularity: Annotated[ str, Query(description="day=按天 / hour=按小时(北京);区间>1 天建议 day") ] = "day", limit: Annotated[int, Query(ge=1, le=1000, description="每页条数(分页大小)")] = 500, offset: Annotated[int, Query(ge=0, description="分页偏移(已跳过条数)=(页码-1)×每页条数")] = 0, sort: Annotated[ str, Query(description="排序:time=发起时间倒序(默认) / elapsed=耗时倒序") ] = "time", ) -> CouponDataOut: today = cn_today() d_from = _parse_day(date_from, field="date_from", default=today) d_to = _parse_day(date_to, field="date_to", default=d_from) if d_to < d_from: raise HTTPException(status_code=422, detail="date_to 不能早于 date_from") if (d_to - d_from).days + 1 > _MAX_RANGE_DAYS: raise HTTPException(status_code=422, detail=f"区间最长 {_MAX_RANGE_DAYS} 天") # 报表默认只看 prod(对齐广告报表防串台口径);app_env=all 时不过滤、看全部环境。 env = None if app_env == "all" else app_env result = coupon_data.coupon_data_report( db, date_from=d_from.isoformat(), date_to=d_to.isoformat(), user=user, app_env=env, granularity=granularity, limit=limit, offset=offset, sort=sort, ) return CouponDataOut( date_from=d_from.isoformat(), date_to=d_to.isoformat(), summary=CouponDataSummary(**result["summary"]), daily=[CouponDataDaily(**d) for d in result["daily"]], hourly=[CouponDataHourly(**h) for h in result["hourly"]], total=result["total"], items=[CouponDataRow(**r) for r in result["items"]], ) @router.get( "/user-records", response_model=CouponUserRecordsOut, summary="某用户全部领券记录(点手机号抽屉:领券次数 + 记录列表)", ) def get_user_coupon_records( db: AdminDb, user_id: Annotated[int, Query(description="用户 id")], limit: Annotated[int, Query(ge=1, le=500, description="最多返回条数")] = 100, sort_by: Annotated[str, Query(description="兼容 UserRecordsDrawer 参数;固定按发起时间倒序")] = "created_at", sort_order: Annotated[str, Query(description="兼容参数,忽略")] = "desc", ) -> CouponUserRecordsOut: result = coupon_data.coupon_user_records(db, user_id=user_id, limit=limit) return CouponUserRecordsOut( items=[CouponDataRow(**r) for r in result["items"]], total=result["total"], )