1
This commit is contained in:
@@ -2,7 +2,7 @@ from datetime import datetime, timezone, timedelta
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
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 sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.dependencies import get_db, get_admin_user
|
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)
|
today_start = datetime.now(CST).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
|
date_start: datetime
|
||||||
|
date_end: datetime
|
||||||
try:
|
try:
|
||||||
if start_date:
|
if start_date:
|
||||||
date_start = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=CST)
|
date_start = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=CST)
|
||||||
else:
|
else:
|
||||||
date_start = today_start
|
date_start = today_start
|
||||||
if end_date:
|
if end_date:
|
||||||
date_end = datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=CST)
|
# 先构造完整的 naive 日期时刻,再一次性 attach tzinfo(避免分步 replace 丢 tzinfo)
|
||||||
date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999)
|
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:
|
else:
|
||||||
date_end = datetime.now(CST)
|
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_start = today_start
|
||||||
date_end = datetime.now(CST)
|
date_end = datetime.now(CST)
|
||||||
|
|
||||||
@@ -1889,11 +1898,17 @@ async def get_stats(
|
|||||||
)).scalar() or 0
|
)).scalar() or 0
|
||||||
|
|
||||||
# ── 每日各模块积分消耗(始终返回选中日期往前7天,便于图表展示)
|
# ── 每日各模块积分消耗(始终返回选中日期往前7天,便于图表展示)
|
||||||
|
# 把 timestamptz 按东八区(业务时区)偏移后再转 DATE,
|
||||||
|
# 直接手动 +8 小时再 CAST 成日期,简单稳妥,不依赖数据库时区名配置。
|
||||||
|
# 与代码中 CST = timezone(timedelta(hours=8)) 保持一致。
|
||||||
from sqlalchemy import Date, cast as sa_cast
|
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天
|
# 图表固定展示 [date_end - 6天, date_end] 共7天
|
||||||
_chart_end_dt = date_end
|
_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 = (
|
_inner = (
|
||||||
select(
|
select(
|
||||||
_day_expr.label('date'),
|
_day_expr.label('date'),
|
||||||
@@ -1904,7 +1919,7 @@ async def get_stats(
|
|||||||
CreditRecord.type == "consume",
|
CreditRecord.type == "consume",
|
||||||
real_credit_charge_filter,
|
real_credit_charge_filter,
|
||||||
CreditRecord.created_at >= _chart_start_dt,
|
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)
|
.group_by(_day_expr, CreditRecord.source_module)
|
||||||
.subquery()
|
.subquery()
|
||||||
@@ -1955,7 +1970,7 @@ async def get_stats(
|
|||||||
CreditRecord.team_id_snapshot.label('team_id'),
|
CreditRecord.team_id_snapshot.label('team_id'),
|
||||||
func.coalesce(
|
func.coalesce(
|
||||||
func.sum(
|
func.sum(
|
||||||
func.case(
|
case(
|
||||||
(CreditRecord.type == "consume", func.abs(CreditRecord.amount)),
|
(CreditRecord.type == "consume", func.abs(CreditRecord.amount)),
|
||||||
else_=0,
|
else_=0,
|
||||||
)
|
)
|
||||||
@@ -1963,7 +1978,7 @@ async def get_stats(
|
|||||||
).label("total_consume"),
|
).label("total_consume"),
|
||||||
func.coalesce(
|
func.coalesce(
|
||||||
func.sum(
|
func.sum(
|
||||||
func.case(
|
case(
|
||||||
(CreditRecord.type == "refund", func.abs(CreditRecord.amount)),
|
(CreditRecord.type == "refund", func.abs(CreditRecord.amount)),
|
||||||
else_=0,
|
else_=0,
|
||||||
)
|
)
|
||||||
@@ -1977,7 +1992,7 @@ async def get_stats(
|
|||||||
CreditRecord.created_at <= date_end,
|
CreditRecord.created_at <= date_end,
|
||||||
)
|
)
|
||||||
.group_by(CreditRecord.team_id_snapshot, CreditRecord.team_name_snapshot)
|
.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()
|
)).all()
|
||||||
credits_by_team = [
|
credits_by_team = [
|
||||||
TeamCreditOut(
|
TeamCreditOut(
|
||||||
|
|||||||
Reference in New Issue
Block a user