From 5de637ab6bad2b39e15e56160d8f2ff6423b1d35 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Tue, 4 Aug 2026 18:41:50 +0800 Subject: [PATCH] 1 --- video-gen-api/app/api/v1/admin.py | 35 ++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index 81acb14a..a031f11f 100644 --- a/video-gen-api/app/api/v1/admin.py +++ b/video-gen-api/app/api/v1/admin.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone, timedelta import json from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy import delete, func, or_, select, update +from sqlalchemy import case, delete, func, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_db, get_admin_user @@ -1749,17 +1749,26 @@ async def get_stats( ): today_start = datetime.now(CST).replace(hour=0, minute=0, second=0, microsecond=0) + date_start: datetime + date_end: datetime try: if start_date: date_start = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=CST) else: date_start = today_start if end_date: - date_end = datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=CST) - date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999) + # 先构造完整的 naive 日期时刻,再一次性 attach tzinfo(避免分步 replace 丢 tzinfo) + naive_end = datetime.strptime(end_date, "%Y-%m-%d").replace( + hour=23, minute=59, second=59, microsecond=999999, + ) + date_end = naive_end.replace(tzinfo=CST) else: date_end = datetime.now(CST) - except: + # 合法性:end >= start + if date_end < date_start: + date_end = date_start.replace(hour=23, minute=59, second=59, microsecond=999999) + except (ValueError, TypeError): + # 只拦截日期解析错误,不吞掉 SQL/运行时异常(原裸 except 会吞所有错误导致用户看不到报错) date_start = today_start date_end = datetime.now(CST) @@ -1889,11 +1898,17 @@ async def get_stats( )).scalar() or 0 # ── 每日各模块积分消耗(始终返回选中日期往前7天,便于图表展示) + # 把 timestamptz 按东八区(业务时区)偏移后再转 DATE, + # 直接手动 +8 小时再 CAST 成日期,简单稳妥,不依赖数据库时区名配置。 + # 与代码中 CST = timezone(timedelta(hours=8)) 保持一致。 from sqlalchemy import Date, cast as sa_cast - _day_expr = sa_cast(CreditRecord.created_at, Date) + _day_expr = sa_cast(CreditRecord.created_at + timedelta(hours=8), Date) # 图表固定展示 [date_end - 6天, date_end] 共7天 _chart_end_dt = date_end - _chart_start_dt = _chart_end_dt - timedelta(days=6) + _chart_start_dt = datetime( + _chart_end_dt.year, _chart_end_dt.month, _chart_end_dt.day, 0, 0, 0, 0, tzinfo=CST, + ) - timedelta(days=6) + _chart_end_dt_inclusive = _chart_end_dt.replace(hour=23, minute=59, second=59, microsecond=999999) _inner = ( select( _day_expr.label('date'), @@ -1904,7 +1919,7 @@ async def get_stats( CreditRecord.type == "consume", real_credit_charge_filter, CreditRecord.created_at >= _chart_start_dt, - CreditRecord.created_at <= _chart_end_dt, + CreditRecord.created_at <= _chart_end_dt_inclusive, ) .group_by(_day_expr, CreditRecord.source_module) .subquery() @@ -1955,7 +1970,7 @@ async def get_stats( CreditRecord.team_id_snapshot.label('team_id'), func.coalesce( func.sum( - func.case( + case( (CreditRecord.type == "consume", func.abs(CreditRecord.amount)), else_=0, ) @@ -1963,7 +1978,7 @@ async def get_stats( ).label("total_consume"), func.coalesce( func.sum( - func.case( + case( (CreditRecord.type == "refund", func.abs(CreditRecord.amount)), else_=0, ) @@ -1977,7 +1992,7 @@ async def get_stats( CreditRecord.created_at <= date_end, ) .group_by(CreditRecord.team_id_snapshot, CreditRecord.team_name_snapshot) - .order_by(func.coalesce(func.sum(func.case((CreditRecord.type == "consume", func.abs(CreditRecord.amount)), else_=0)), 0).desc()) + .order_by(func.coalesce(func.sum(case((CreditRecord.type == "consume", func.abs(CreditRecord.amount)), else_=0)), 0).desc()) )).all() credits_by_team = [ TeamCreditOut(