From a55d4d649c1e2b5f9fc022fce92e3cd9818abb02 Mon Sep 17 00:00:00 2001 From: wwwwwwwww <526125649@qq.com> Date: Wed, 5 Aug 2026 11:59:27 +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=90=8E=E5=8F=B0=E4=BA=A4=E6=98=93=E6=B5=81=E6=B0=B4=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/AdminCreditRecords.tsx | 87 +++++++++++++++--- video-gen-admin/src/types/index.ts | 16 ++++ video-gen-api/app/api/v1/admin.py | 81 +++++++++++----- video-gen-api/app/schemas/admin.py | 13 ++- .../services/admin_credit_record_service.py | 92 ++++++++++++++++--- 5 files changed, 236 insertions(+), 53 deletions(-) diff --git a/video-gen-admin/src/pages/AdminCreditRecords.tsx b/video-gen-admin/src/pages/AdminCreditRecords.tsx index 312d65e2..bc1db51d 100644 --- a/video-gen-admin/src/pages/AdminCreditRecords.tsx +++ b/video-gen-admin/src/pages/AdminCreditRecords.tsx @@ -3,7 +3,7 @@ import { Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography, } from 'antd'; import { - ArrowDownOutlined, ArrowUpOutlined, DownloadOutlined, ReloadOutlined, RollbackOutlined, WalletOutlined, + ArrowDownOutlined, ArrowUpOutlined, DollarOutlined, DownloadOutlined, ReloadOutlined, RollbackOutlined, WalletOutlined, } from '@ant-design/icons'; import { exportStyledExcel, type StyledExcelColumn } from '../utils/excelExport'; import dayjs from 'dayjs'; @@ -17,6 +17,11 @@ const DEFAULT_SUMMARY: AdminCreditRecordSummary = { totalRecharge: 0, totalConsume: 0, totalRefund: 0, + totalCharge: 0, + totalHold: 0, + totalRefundReal: 0, + totalHoldRelease: 0, + netConsume: 0, transactionCount: 0, generationCount: 0, generationAttemptCount: 0, @@ -310,17 +315,22 @@ const AdminCreditRecords: React.FC = () => { ], summaryRows: [ ['总充值', exportSummary.totalRecharge], - ['总消费', exportSummary.totalConsume], - ['总回退', exportSummary.totalRefund], + ['总消费(真实扣费 + 预扣占用)', exportSummary.totalConsume], + [' · 真实扣费(独立统计:type=消费 & action=charge/NULL)', exportSummary.totalCharge], + [' · 预扣占用(独立统计:type=消费 & action=hold)', exportSummary.totalHold], + ['总回退(真实退款 + 预扣释放)', exportSummary.totalRefund], + [' · 真实退款(独立统计:type=回退 & action=refund/NULL)', exportSummary.totalRefundReal], + [' · 预扣释放(独立统计:type=回退 & action=hold_release)', exportSummary.totalHoldRelease], + ['净消耗(总消费 − 总回退,≥ 0)', exportSummary.netConsume], ['交易笔数', exportSummary.transactionCount], ['生成条数', exportSummary.generationCount], ['生成尝试次数', exportSummary.generationAttemptCount], ['图片生成条数', exportSummary.imageGenerationCount], ['视频生成条数', exportSummary.videoGenerationCount], - ['图片消费积分', exportSummary.imageConsume], - ['视频消费积分', exportSummary.videoConsume], - ['提词消费积分', exportSummary.textConsume], - ['视频分析积分', exportSummary.analysisConsume], + ['图片消费积分(仅真实扣费)', exportSummary.imageConsume], + ['视频消费积分(仅真实扣费)', exportSummary.videoConsume], + ['提词消费积分(仅真实扣费)', exportSummary.textConsume], + ['视频分析积分(仅真实扣费)', exportSummary.analysisConsume], ['总 Token', exportSummary.totalTokens], ['输入 Token', exportSummary.inputTokens], ['输出 Token', exportSummary.outputTokens], @@ -358,11 +368,64 @@ const AdminCreditRecords: React.FC = () => { return (
-
-
总充值
+{n(summary.totalRecharge)}
-
总消费
-{n(summary.totalConsume)}
-
总回退
+{n(summary.totalRefund)}
-
交易 / 生成
{n(summary.transactionCount)} / {n(summary.generationCount)}
+
+ + +
+
总充值
+
+{n(summary.totalRecharge)}
+
+
+
+ + +
+
+ 总消费 + (真实扣费 + 预扣占用) +
+
-{n(summary.totalConsume)}
+
+ 真实 {n(summary.totalCharge)} + | + 预扣 {n(summary.totalHold)} +
+
+
+
+ + +
+
+ 总回退 + (真实退款 + 预扣释放) +
+
+{n(summary.totalRefund)}
+
+ 退款 {n(summary.totalRefundReal)} + | + 释放 {n(summary.totalHoldRelease)} +
+
+
+
+ + +
+
净消耗(实际用掉)
+
{n(summary.netConsume)}
+
总消费 − 总回退(≥ 0)
+
+
+
+ + +
+
交易 / 生成
+
{n(summary.transactionCount)} / {n(summary.generationCount)}
+
+
+
diff --git a/video-gen-admin/src/types/index.ts b/video-gen-admin/src/types/index.ts index 00a92680..dad13673 100644 --- a/video-gen-admin/src/types/index.ts +++ b/video-gen-admin/src/types/index.ts @@ -868,16 +868,32 @@ export interface VideoPromptSchemaPreviewOut { export interface AdminCreditRecordSummary { totalRecharge: number; + /** 总消费(仅 type=consume,不含团队内部转账)= 真实扣费 + 预扣占用 */ totalConsume: number; + /** 总回退(仅 type=refund)= 真实退款 + 预扣释放 */ totalRefund: number; + /** 独立统计列:真实扣费 charge(含历史 NULL),对应"筛选类型=消费 & action=charge/NULL"求和 */ + totalCharge: number; + /** 独立统计列:预扣占用 hold */ + totalHold: number; + /** 独立统计列:真实退款 refund(含历史 NULL) */ + totalRefundReal: number; + /** 独立统计列:预扣释放 hold_release(type=refund, action=hold_release) */ + totalHoldRelease: number; + /** 净消耗 = max(totalConsume - totalRefund, 0),即真正"用掉了"的积分 */ + netConsume: number; transactionCount: number; generationCount: number; generationAttemptCount: number; imageGenerationCount: number; videoGenerationCount: number; + /** 子分类消费(图片)仅真实扣费 charge 口径 */ imageConsume: number; + /** 子分类消费(视频)仅真实扣费 charge 口径 */ videoConsume: number; + /** 子分类消费(提词)仅真实扣费 charge 口径 */ textConsume: number; + /** 子分类消费(分析)仅真实扣费 charge 口径 */ analysisConsume: number; totalTokens: number; inputTokens: number; diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index 413de56c..8b089f01 100644 --- a/video-gen-api/app/api/v1/admin.py +++ b/video-gen-api/app/api/v1/admin.py @@ -1811,20 +1811,45 @@ async def get_stats( ) )).scalar() or 0 - # 预扣占用不是实际消费;历史流水 charge_action 为空时仍按真实扣费兼容。 + # 消费类(真实扣费 + 预扣占用):charge_action 为空时仍按真实扣费兼容;hold 为预扣占用。 + credit_charge_action_filter = or_( + CreditRecord.charge_action.is_(None), + CreditRecord.charge_action == "charge", + CreditRecord.charge_action == "hold", + ) + # 「仅真实扣费」filter 用于图表、模型使用次数等需要按实际产出(非预扣)统计的场景。 real_credit_charge_filter = or_( CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge", ) - credits_consumed = (await db.execute( - select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where( - CreditRecord.type == "consume", - real_credit_charge_filter, + # 核心数据「消耗积分」= 净消耗 = 真实消费 + 预扣占用 - 真实退款 - 预扣释放。 + # 说明: + # hold(预扣占用):type=consume,charge_action='hold',amount<0 + # hold_release(预扣释放退回):type=refund,charge_action='hold_release',amount>0 + # (账本 L256 强校验:hold_release.type 必须是 'refund',不是 consume) + # charge(真实扣费):type=consume,charge_action='charge' 或 NULL(历史),amount<0 + # refund(真实退款):type=refund,charge_action='refund' 或 NULL(历史兼容),amount>0 + # 因此 type=refund 天然包含「真实退款 + 预扣释放退回」两类子流水。 + _stats_real_and_hold = case( + (and_(CreditRecord.type == "consume", credit_charge_action_filter), func.abs(CreditRecord.amount)), + else_=0, + ) + _stats_refund_and_release = case( + (CreditRecord.type == "refund", func.abs(CreditRecord.amount)), + else_=0, + ) + _net_row = (await db.execute( + select( + func.coalesce(func.sum(_stats_real_and_hold), 0), + func.coalesce(func.sum(_stats_refund_and_release), 0), + ).where( + CreditRecord.type.in_(["consume", "refund"]), CreditRecord.created_at >= date_start, CreditRecord.created_at <= date_end, ) - )).scalar() or 0 + )).one() + credits_consumed = round(max(float(_net_row[0] or 0) - float(_net_row[1] or 0), 0.0), 2) alipay_revenue = (await db.execute( select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where( @@ -1888,14 +1913,19 @@ async def get_stats( ) )).scalar() or 0 - last_period_credits_consumed = (await db.execute( - select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where( - CreditRecord.type == "consume", - real_credit_charge_filter, + last_period_net_row = (await db.execute( + select( + func.coalesce(func.sum(_stats_real_and_hold), 0), + func.coalesce(func.sum(_stats_refund_and_release), 0), + ).where( + CreditRecord.type.in_(["consume", "refund"]), CreditRecord.created_at >= last_period_start, CreditRecord.created_at <= last_period_end, ) - )).scalar() or 0 + )).one() + last_period_credits_consumed = round( + max(float(last_period_net_row[0] or 0) - float(last_period_net_row[1] or 0), 0.0), 2, + ) # ── 每日各模块积分消耗(始终返回选中日期往前7天,便于图表展示) # 把 timestamptz 按东八区(业务时区)偏移后再转 DATE, @@ -1963,22 +1993,25 @@ 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_( + # 净消耗 = (真实消费 charge + 预扣占用 hold) - (真实退款 refund + 预扣释放 hold_release) + # 注意: + # hold(预扣占用):type=consume,charge_action='hold',amount<0 → 加项 + # hold_release(预扣释放):type=refund,charge_action='hold_release',amount>0 → 减项(type=refund 天然包含) + # charge(真实扣费):type=consume,charge/NULL → 加项 + # refund(真实退款):type=refund,refund/NULL → 减项 + _charge_hold_filter = and_( CreditRecord.type == "consume", - real_credit_charge_filter, + credit_charge_action_filter, # charge / hold / NULL(历史 charge) ) - _consume_expr = case((_consume_filter, func.abs(CreditRecord.amount)), else_=0) - _refund_expr = case((CreditRecord.type == "refund", func.abs(CreditRecord.amount)), else_=0) + _charge_hold_expr = case((_charge_hold_filter, func.abs(CreditRecord.amount)), else_=0) + # type=refund = 真实退款 + 预扣释放退回(账本强制 hold_release.type=refund) + _refund_release_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(_consume_expr), 0).label("total_consume"), - func.coalesce(func.sum(_refund_expr), 0).label("total_refund"), + func.coalesce(func.sum(_charge_hold_expr), 0).label("total_charge_hold"), + func.coalesce(func.sum(_refund_release_expr), 0).label("total_refund_release"), ) .where( CreditRecord.type.in_(["consume", "refund"]), @@ -1986,14 +2019,14 @@ 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(_consume_expr), 0) - func.coalesce(func.sum(_refund_expr), 0)).desc()) + # 按"净消耗 = 真实+预扣 - 退款+释放"倒序排序(排行榜) + .order_by((func.coalesce(func.sum(_charge_hold_expr), 0) - func.coalesce(func.sum(_refund_release_expr), 0)).desc()) )).all() credits_by_team = [ TeamCreditOut( team_name=row.team_name, team_id=row.team_id, - credits=round(float(row.total_consume or 0) - float(row.total_refund or 0), 2), + credits=round(max(float(row.total_charge_hold or 0) - float(row.total_refund_release or 0), 0.0), 2), ) for row in team_credit_rows ] diff --git a/video-gen-api/app/schemas/admin.py b/video-gen-api/app/schemas/admin.py index cdfcd3df..8c145a7b 100644 --- a/video-gen-api/app/schemas/admin.py +++ b/video-gen-api/app/schemas/admin.py @@ -162,8 +162,17 @@ class AdminCreditRecordSummaryOut(BaseModel): total_recharge: float = 0.0 total_consume: float = 0.0 total_refund: float = 0.0 - # 净消耗 = 真实消费(预扣释放不算)- 退款,这才是真正的"消耗了多少积分"口径 - # 前端展示"总消耗积分"时应使用此字段,而不是 total_consume(含未退回的) + # 消费类分解(仅 type=consume,不含 team_internal 团队内部转账;真实扣费 + 预扣占用 = total_consume) + # - total_charge : 真实扣费 charge(含历史 NULL),对应"筛选明细类型=消费 且 action=charge/NULL"求和 + # - total_hold : 预扣占用 hold + total_charge: float = 0.0 + total_hold: float = 0.0 + # 回退类分解(仅 type=refund;真实退款 + 预扣释放 = total_refund) + # - total_refund_real : 真实退款 refund(含历史 NULL) + # - total_hold_release: 预扣释放 hold_release + total_refund_real: float = 0.0 + total_hold_release: float = 0.0 + # 净消耗 = max(total_consume - total_refund, 0) = 实际"用掉了"的积分 net_consume: float = 0.0 transaction_count: int = 0 generation_count: int = 0 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 ee5f400e..5fde90e6 100644 --- a/video-gen-api/app/services/admin_credit_record_service.py +++ b/video-gen-api/app/services/admin_credit_record_service.py @@ -296,26 +296,80 @@ async def list_admin_credit_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 + # - consume 类型:amount 是负数(扣减积分),统计用 abs() 保证为正值 + # team_internal(团队内部积分流转/管理员分配)不参与消费/扣费统计——它不是真实消费 + # - refund 类型:amount 是正数(退回积分),为兼容旧数据/边缘场景也用 abs() 保证统计值恒正 + # 子分类:真实退款 refund(action='refund'/NULL) + 预扣释放 hold_release(action='hold_release') + # - recharge 类型:amount 是正数(充值增加),金额直接求和,不需要 abs + # + # 口径更新(Bug 修复 · 第二次修正): + # 1. 消费类统计仅看 type=consume(排除 team_internal 团队内部转账) + # 2. 真实扣费 / 预扣占用 / 真实退款 / 预扣释放 全部改为"独立统计列",不再用差值推导 + # (避免任何一类范围不同导致推导失真) + # + # 消费类(type=consume): + # - total_charge :真实扣费 charge_action in (NULL, 'charge') abs 求和 + # - total_hold :预扣占用 charge_action = 'hold' abs 求和 + # - total_consume :total_charge + total_hold = charge_action in (NULL, charge, hold) abs 求和 + # 回退类(type=refund): + # - total_refund_real :真实退款 charge_action in (NULL, 'refund') abs 求和 + # - total_hold_release :预扣释放 charge_action = 'hold_release' abs 求和 + # - total_refund :total_refund_real + total_hold_release = type=refund 全部 abs 求和 + # 净消耗 net_consume = max(total_consume - total_refund, 0) + # + # 按积分 subject 分类的子项(图片/视频/提词/分析)仍保持「仅真实扣费 charge」口径不变: + # 预扣是按任务预估的冻结,不是按图/视频实际产出,会让子分类统计失真。 + _real_charge_action = or_( + CreditRecord.charge_action.is_(None), + CreditRecord.charge_action == "charge", + ) + _charge_or_hold_action = or_( + CreditRecord.charge_action.is_(None), + CreditRecord.charge_action == "charge", + CreditRecord.charge_action == "hold", + ) + _real_refund_action = or_( + CreditRecord.charge_action.is_(None), + CreditRecord.charge_action == "refund", + ) + # 仅统计 type=consume 的消费类(排除 team_internal 团队内部转账) + _consume_type = CreditRecord.type == "consume" + # 预扣释放 / 真实退款 filter(都是 type=refund,账本 L256 强校验 hold_release.type=refund) + _hold_release_filter = and_( + CreditRecord.type == "refund", + CreditRecord.charge_action == "hold_release", + ) + _refund_type = CreditRecord.type == "refund" summary_query = select( + # 0: 充值 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", func.abs(CreditRecord.amount)), else_=0)), 0), + # 1: 总消费 = total_charge + total_hold(真实扣费 + 预扣占用) + func.coalesce(func.sum(case((and_(_consume_type, _charge_or_hold_action), func.abs(CreditRecord.amount)), else_=0)), 0), + # 2: 总回退 = 真实退款 + 预扣释放(type=refund 全部流水) + func.coalesce(func.sum(case((_refund_type, func.abs(CreditRecord.amount)), else_=0)), 0), + # 3: 交易笔数 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)), - func.count(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "image", 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(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "video", CreditRecord.type == "consume", (or_(CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge"))), func.concat(CreditRecord.owner_type, ":", CreditRecord.owner_id)), else_=None))), - func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "image", CreditRecord.type == "consume", (or_(CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge"))), func.abs(CreditRecord.amount)), else_=0)), 0), - func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "video", CreditRecord.type == "consume", (or_(CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge"))), func.abs(CreditRecord.amount)), else_=0)), 0), - func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.TEXT.value, CreditRecord.type == "consume", (or_(CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge"))), func.abs(CreditRecord.amount)), else_=0)), 0), - func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.ANALYSIS.value, CreditRecord.type == "consume", (or_(CreditRecord.charge_action.is_(None), CreditRecord.charge_action == "charge"))), func.abs(CreditRecord.amount)), else_=0)), 0), + # 4-11: 生成条数 / 尝试次数 / 图片视频条数 / 图片视频提词分析消费(仍按 charge 口径) + func.count(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, _consume_type, _real_charge_action), func.concat(CreditRecord.owner_type, ":", CreditRecord.owner_id)), else_=None))), + func.count(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, _consume_type, _real_charge_action), 1), else_=None)), + func.count(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "image", _consume_type, _real_charge_action), func.concat(CreditRecord.owner_type, ":", CreditRecord.owner_id)), else_=None))), + func.count(distinct(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "video", _consume_type, _real_charge_action), func.concat(CreditRecord.owner_type, ":", CreditRecord.owner_id)), else_=None))), + func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "image", _consume_type, _real_charge_action), func.abs(CreditRecord.amount)), else_=0)), 0), + func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.MEDIA.value, CreditRecord.media_type == "video", _consume_type, _real_charge_action), func.abs(CreditRecord.amount)), else_=0)), 0), + func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.TEXT.value, _consume_type, _real_charge_action), func.abs(CreditRecord.amount)), else_=0)), 0), + func.coalesce(func.sum(case((and_(CreditRecord.credit_subject == CreditRecordSubject.ANALYSIS.value, _consume_type, _real_charge_action), func.abs(CreditRecord.amount)), else_=0)), 0), + # 12-14: Token func.coalesce(func.sum(CreditRecord.total_tokens), 0), func.coalesce(func.sum(CreditRecord.input_tokens), 0), func.coalesce(func.sum(CreditRecord.output_tokens), 0), + # 15: 真实扣费 total_charge(独立列:type=consume AND charge_action in (NULL, 'charge')) + func.coalesce(func.sum(case((and_(_consume_type, _real_charge_action), func.abs(CreditRecord.amount)), else_=0)), 0), + # 16: 预扣占用 total_hold(独立列:type=consume AND charge_action='hold') + func.coalesce(func.sum(case((and_(_consume_type, CreditRecord.charge_action == "hold"), func.abs(CreditRecord.amount)), else_=0)), 0), + # 17: 真实退款 total_refund_real(独立列:type=refund AND charge_action in (NULL, 'refund')) + func.coalesce(func.sum(case((and_(_refund_type, _real_refund_action), func.abs(CreditRecord.amount)), else_=0)), 0), + # 18: 预扣释放 total_hold_release(独立列:type=refund AND charge_action='hold_release') + func.coalesce(func.sum(case((_hold_release_filter, func.abs(CreditRecord.amount)), else_=0)), 0), ).select_from(CreditRecord).join(User, CreditRecord.user_id == User.id, isouter=True) if where_clause is not None: summary_query = summary_query.where(where_clause) @@ -323,12 +377,20 @@ async def list_admin_credit_records( _total_recharge = _round2(s[0]) _total_consume = _round2(s[1]) _total_refund = _round2(s[2]) - # 净消耗 = 真实消费 - 退款;如果退款超过消费(连续退了跨周期的),下限为 0 避免负数 + _total_charge = _round2(s[15]) + _total_hold = _round2(s[16]) + _total_refund_real = _round2(s[17]) + _total_hold_release = _round2(s[18]) + # 净消耗 = 总消费 − 总回退;若回退跨周期导致负数,按 0 兜底 _net_consume = _round2(max(_total_consume - _total_refund, 0.0)) summary = { "total_recharge": _total_recharge, "total_consume": _total_consume, "total_refund": _total_refund, + "total_charge": _total_charge, + "total_hold": _total_hold, + "total_refund_real": _total_refund_real, + "total_hold_release": _total_hold_release, "net_consume": _net_consume, "transaction_count": int(s[3] or 0), "generation_count": int(s[4] or 0),