From ceb930bd5a4f1148a7af54ebd2af9b624ffdd22d Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Wed, 5 Aug 2026 10:17:12 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E4=BF=AE=E5=A4=8D=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E6=95=B0=E6=8D=AE=E6=A6=82=E8=A7=88=E7=9A=84?= =?UTF-8?q?=E5=9B=A2=E9=98=9F=E7=A7=AF=E5=88=86=E6=B6=88=E8=80=97=E6=8E=92?= =?UTF-8?q?=E8=A1=8C=E6=83=85=E5=86=B5=202=E3=80=81=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=89=8D=E5=8F=B0=E5=9B=A2=E9=98=9F=E7=9A=84=E9=80=80=E6=AC=BE?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-api/app/api/v1/admin.py | 35 ++++++++----------- .../services/admin_credit_record_service.py | 7 +++- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index a031f11f..413de56c 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 case, delete, func, or_, select, update +from sqlalchemy import and_, case, delete, func, or_, select, update from sqlalchemy.ext.asyncio import AsyncSession from app.dependencies import get_db, get_admin_user @@ -1963,36 +1963,31 @@ async def get_stats( ] # ── 各团队积分消耗(有团队 vs 无团队,使用流水中的团队快照) - # 消耗 = 扣除金额 - 退回金额(净消耗) + # 净消耗 = 实际消费(charge_action IS NULL 或 charge) - 退款(charge_action=refund 不限制) + # 注意:real_credit_charge_filter 仅应限制 consume 类型(预扣 hold 释放不算真实消费); + # refund 类型的 charge_action = 'refund',不能套用同一个 filter, + # 所以把该 filter 下移到 CASE WHEN 内而不是写在 WHERE 中,避免退款被误过滤。 + _consume_filter = and_( + CreditRecord.type == "consume", + real_credit_charge_filter, + ) + _consume_expr = case((_consume_filter, func.abs(CreditRecord.amount)), else_=0) + _refund_expr = case((CreditRecord.type == "refund", func.abs(CreditRecord.amount)), else_=0) team_credit_rows = (await db.execute( select( func.coalesce(CreditRecord.team_name_snapshot, '未分配团队').label('team_name'), CreditRecord.team_id_snapshot.label('team_id'), - func.coalesce( - func.sum( - case( - (CreditRecord.type == "consume", func.abs(CreditRecord.amount)), - else_=0, - ) - ), 0 - ).label("total_consume"), - func.coalesce( - func.sum( - case( - (CreditRecord.type == "refund", func.abs(CreditRecord.amount)), - else_=0, - ) - ), 0 - ).label("total_refund"), + func.coalesce(func.sum(_consume_expr), 0).label("total_consume"), + func.coalesce(func.sum(_refund_expr), 0).label("total_refund"), ) .where( CreditRecord.type.in_(["consume", "refund"]), - real_credit_charge_filter, CreditRecord.created_at >= date_start, CreditRecord.created_at <= date_end, ) .group_by(CreditRecord.team_id_snapshot, CreditRecord.team_name_snapshot) - .order_by(func.coalesce(func.sum(case((CreditRecord.type == "consume", func.abs(CreditRecord.amount)), else_=0)), 0).desc()) + # 按"净消耗 = 总消费 - 总退款"倒序排序(排行榜) + .order_by((func.coalesce(func.sum(_consume_expr), 0) - func.coalesce(func.sum(_refund_expr), 0)).desc()) )).all() credits_by_team = [ TeamCreditOut( diff --git a/video-gen-api/app/services/admin_credit_record_service.py b/video-gen-api/app/services/admin_credit_record_service.py index a67d7ccc..1cbc7790 100644 --- a/video-gen-api/app/services/admin_credit_record_service.py +++ b/video-gen-api/app/services/admin_credit_record_service.py @@ -295,10 +295,15 @@ async def list_admin_credit_records( deleted_map = await _load_deleted_map(db, records) items = [_record_to_item(record, user, deleted_map) for record, user in rows] + # 说明: + # - consume / team_internal:amount 是负数(扣减积分),统计用 abs() 保证为正值 + # - refund:amount 是正数(退回积分),但为兼容旧数据/边缘场景也用 abs() 保证统计值恒正 + # 避免前台筛选"退款"类型时总退款显示为 0/负数导致用户误以为没有统计 + # - recharge:amount 是正数(充值增加),金额直接求和,不需要 abs summary_query = select( func.coalesce(func.sum(case((CreditRecord.type == "recharge", CreditRecord.amount), else_=0)), 0), func.coalesce(func.sum(case((and_(CreditRecord.type.in_(["consume", "team_internal"]), (or_(CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge"))), func.abs(CreditRecord.amount)), else_=0)), 0), - func.coalesce(func.sum(case((CreditRecord.type == "refund", CreditRecord.amount), else_=0)), 0), + func.coalesce(func.sum(case((CreditRecord.type == "refund", func.abs(CreditRecord.amount)), else_=0)), 0), func.count(CreditRecord.id), func.count(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.type == "consume", (or_(CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge"))), func.concat(CreditRecord.owner_type, ":", CreditRecord.owner_id)), else_=None))), func.count(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.type == "consume", (or_(CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge"))), 1), else_=None)),