1、修复后台首页数据概览的团队积分消耗排行情况
2、修复后台交易流水数据
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
|||||||
Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography,
|
Button, Card, DatePicker, Input, message, Select, Space, Table, Tag, Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
ArrowDownOutlined, ArrowUpOutlined, DownloadOutlined, ReloadOutlined, RollbackOutlined, WalletOutlined,
|
ArrowDownOutlined, ArrowUpOutlined, DollarOutlined, DownloadOutlined, ReloadOutlined, RollbackOutlined, WalletOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { exportStyledExcel, type StyledExcelColumn } from '../utils/excelExport';
|
import { exportStyledExcel, type StyledExcelColumn } from '../utils/excelExport';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -17,6 +17,11 @@ const DEFAULT_SUMMARY: AdminCreditRecordSummary = {
|
|||||||
totalRecharge: 0,
|
totalRecharge: 0,
|
||||||
totalConsume: 0,
|
totalConsume: 0,
|
||||||
totalRefund: 0,
|
totalRefund: 0,
|
||||||
|
totalCharge: 0,
|
||||||
|
totalHold: 0,
|
||||||
|
totalRefundReal: 0,
|
||||||
|
totalHoldRelease: 0,
|
||||||
|
netConsume: 0,
|
||||||
transactionCount: 0,
|
transactionCount: 0,
|
||||||
generationCount: 0,
|
generationCount: 0,
|
||||||
generationAttemptCount: 0,
|
generationAttemptCount: 0,
|
||||||
@@ -310,17 +315,22 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
],
|
],
|
||||||
summaryRows: [
|
summaryRows: [
|
||||||
['总充值', exportSummary.totalRecharge],
|
['总充值', exportSummary.totalRecharge],
|
||||||
['总消费', exportSummary.totalConsume],
|
['总消费(真实扣费 + 预扣占用)', exportSummary.totalConsume],
|
||||||
['总回退', exportSummary.totalRefund],
|
[' · 真实扣费(独立统计: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.transactionCount],
|
||||||
['生成条数', exportSummary.generationCount],
|
['生成条数', exportSummary.generationCount],
|
||||||
['生成尝试次数', exportSummary.generationAttemptCount],
|
['生成尝试次数', exportSummary.generationAttemptCount],
|
||||||
['图片生成条数', exportSummary.imageGenerationCount],
|
['图片生成条数', exportSummary.imageGenerationCount],
|
||||||
['视频生成条数', exportSummary.videoGenerationCount],
|
['视频生成条数', exportSummary.videoGenerationCount],
|
||||||
['图片消费积分', exportSummary.imageConsume],
|
['图片消费积分(仅真实扣费)', exportSummary.imageConsume],
|
||||||
['视频消费积分', exportSummary.videoConsume],
|
['视频消费积分(仅真实扣费)', exportSummary.videoConsume],
|
||||||
['提词消费积分', exportSummary.textConsume],
|
['提词消费积分(仅真实扣费)', exportSummary.textConsume],
|
||||||
['视频分析积分', exportSummary.analysisConsume],
|
['视频分析积分(仅真实扣费)', exportSummary.analysisConsume],
|
||||||
['总 Token', exportSummary.totalTokens],
|
['总 Token', exportSummary.totalTokens],
|
||||||
['输入 Token', exportSummary.inputTokens],
|
['输入 Token', exportSummary.inputTokens],
|
||||||
['输出 Token', exportSummary.outputTokens],
|
['输出 Token', exportSummary.outputTokens],
|
||||||
@@ -358,11 +368,64 @@ const AdminCreditRecords: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
|
||||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}><Space><ArrowUpOutlined style={{ color: '#10b981', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总充值</div><div style={{ fontSize: 22, fontWeight: 800, color: '#10b981' }}>+{n(summary.totalRecharge)}</div></div></Space></Card>
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}><Space><ArrowDownOutlined style={{ color: '#ef4444', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总消费</div><div style={{ fontSize: 22, fontWeight: 800, color: '#ef4444' }}>-{n(summary.totalConsume)}</div></div></Space></Card>
|
<Space><ArrowUpOutlined style={{ color: '#10b981', fontSize: 22 }} />
|
||||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}><Space><RollbackOutlined style={{ color: '#3b82f6', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>总回退</div><div style={{ fontSize: 22, fontWeight: 800, color: '#3b82f6' }}>+{n(summary.totalRefund)}</div></div></Space></Card>
|
<div>
|
||||||
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}><Space><WalletOutlined style={{ color: '#6366f1', fontSize: 22 }} /><div><div style={{ color: '#94a3b8' }}>交易 / 生成</div><div style={{ fontSize: 22, fontWeight: 800 }}>{n(summary.transactionCount)} / {n(summary.generationCount)}</div></div></Space></Card>
|
<div style={{ color: '#94a3b8' }}>总充值</div>
|
||||||
|
<div style={{ fontSize: 22, fontWeight: 800, color: '#10b981' }}>+{n(summary.totalRecharge)}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
|
<Space><ArrowDownOutlined style={{ color: '#ef4444', fontSize: 22 }} />
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div style={{ color: '#94a3b8' }}>
|
||||||
|
总消费
|
||||||
|
<span style={{ marginLeft: 6, fontSize: 10, color: '#94a3b8' }}>(真实扣费 + 预扣占用)</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 22, fontWeight: 800, color: '#ef4444' }}>-{n(summary.totalConsume)}</div>
|
||||||
|
<div style={{ fontSize: 11, color: '#94a3b8', marginTop: 2 }}>
|
||||||
|
真实 <span style={{ color: '#b91c1c', fontWeight: 600 }}>{n(summary.totalCharge)}</span>
|
||||||
|
<span style={{ margin: '0 4px', color: '#cbd5e1' }}>|</span>
|
||||||
|
预扣 <span style={{ color: '#d97706', fontWeight: 600 }}>{n(summary.totalHold)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
|
<Space><RollbackOutlined style={{ color: '#3b82f6', fontSize: 22 }} />
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div style={{ color: '#94a3b8' }}>
|
||||||
|
总回退
|
||||||
|
<span style={{ marginLeft: 6, fontSize: 10, color: '#94a3b8' }}>(真实退款 + 预扣释放)</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: 22, fontWeight: 800, color: '#3b82f6' }}>+{n(summary.totalRefund)}</div>
|
||||||
|
<div style={{ fontSize: 11, color: '#94a3b8', marginTop: 2 }}>
|
||||||
|
退款 <span style={{ color: '#1d4ed8', fontWeight: 600 }}>{n(summary.totalRefundReal)}</span>
|
||||||
|
<span style={{ margin: '0 4px', color: '#cbd5e1' }}>|</span>
|
||||||
|
释放 <span style={{ color: '#047857', fontWeight: 600 }}>{n(summary.totalHoldRelease)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5', background: 'linear-gradient(135deg, #faf5ff 0%, #eef2ff 100%)' }}>
|
||||||
|
<Space><DollarOutlined style={{ color: '#6366f1', fontSize: 22 }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#6366f1' }}>净消耗(实际用掉)</div>
|
||||||
|
<div style={{ fontSize: 22, fontWeight: 800, color: '#4338ca' }}>{n(summary.netConsume)}</div>
|
||||||
|
<div style={{ fontSize: 11, color: '#818cf8', marginTop: 2 }}>总消费 − 总回退(≥ 0)</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
<Card bordered={false} style={{ borderRadius: 12, border: '1px solid #f0f0f5' }}>
|
||||||
|
<Space><WalletOutlined style={{ color: '#6366f1', fontSize: 22 }} />
|
||||||
|
<div>
|
||||||
|
<div style={{ color: '#94a3b8' }}>交易 / 生成</div>
|
||||||
|
<div style={{ fontSize: 22, fontWeight: 800 }}>{n(summary.transactionCount)} / {n(summary.generationCount)}</div>
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, minmax(0, 1fr))', gap: 16, marginBottom: 16 }}>
|
||||||
|
|||||||
@@ -868,16 +868,32 @@ export interface VideoPromptSchemaPreviewOut {
|
|||||||
|
|
||||||
export interface AdminCreditRecordSummary {
|
export interface AdminCreditRecordSummary {
|
||||||
totalRecharge: number;
|
totalRecharge: number;
|
||||||
|
/** 总消费(仅 type=consume,不含团队内部转账)= 真实扣费 + 预扣占用 */
|
||||||
totalConsume: number;
|
totalConsume: number;
|
||||||
|
/** 总回退(仅 type=refund)= 真实退款 + 预扣释放 */
|
||||||
totalRefund: number;
|
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;
|
transactionCount: number;
|
||||||
generationCount: number;
|
generationCount: number;
|
||||||
generationAttemptCount: number;
|
generationAttemptCount: number;
|
||||||
imageGenerationCount: number;
|
imageGenerationCount: number;
|
||||||
videoGenerationCount: number;
|
videoGenerationCount: number;
|
||||||
|
/** 子分类消费(图片)仅真实扣费 charge 口径 */
|
||||||
imageConsume: number;
|
imageConsume: number;
|
||||||
|
/** 子分类消费(视频)仅真实扣费 charge 口径 */
|
||||||
videoConsume: number;
|
videoConsume: number;
|
||||||
|
/** 子分类消费(提词)仅真实扣费 charge 口径 */
|
||||||
textConsume: number;
|
textConsume: number;
|
||||||
|
/** 子分类消费(分析)仅真实扣费 charge 口径 */
|
||||||
analysisConsume: number;
|
analysisConsume: number;
|
||||||
totalTokens: number;
|
totalTokens: number;
|
||||||
inputTokens: number;
|
inputTokens: number;
|
||||||
|
|||||||
@@ -1811,20 +1811,45 @@ async def get_stats(
|
|||||||
)
|
)
|
||||||
)).scalar() or 0
|
)).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_(
|
real_credit_charge_filter = or_(
|
||||||
CreditRecord.charge_action.is_(None),
|
CreditRecord.charge_action.is_(None),
|
||||||
CreditRecord.charge_action == "charge",
|
CreditRecord.charge_action == "charge",
|
||||||
)
|
)
|
||||||
|
|
||||||
credits_consumed = (await db.execute(
|
# 核心数据「消耗积分」= 净消耗 = 真实消费 + 预扣占用 - 真实退款 - 预扣释放。
|
||||||
select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
|
# 说明:
|
||||||
CreditRecord.type == "consume",
|
# hold(预扣占用):type=consume,charge_action='hold',amount<0
|
||||||
real_credit_charge_filter,
|
# 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_start,
|
||||||
CreditRecord.created_at <= date_end,
|
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(
|
alipay_revenue = (await db.execute(
|
||||||
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
|
select(func.coalesce(func.sum(PaymentOrder.amount), 0)).where(
|
||||||
@@ -1888,14 +1913,19 @@ async def get_stats(
|
|||||||
)
|
)
|
||||||
)).scalar() or 0
|
)).scalar() or 0
|
||||||
|
|
||||||
last_period_credits_consumed = (await db.execute(
|
last_period_net_row = (await db.execute(
|
||||||
select(func.coalesce(func.sum(func.abs(CreditRecord.amount)), 0)).where(
|
select(
|
||||||
CreditRecord.type == "consume",
|
func.coalesce(func.sum(_stats_real_and_hold), 0),
|
||||||
real_credit_charge_filter,
|
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_start,
|
||||||
CreditRecord.created_at <= last_period_end,
|
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天,便于图表展示)
|
# ── 每日各模块积分消耗(始终返回选中日期往前7天,便于图表展示)
|
||||||
# 把 timestamptz 按东八区(业务时区)偏移后再转 DATE,
|
# 把 timestamptz 按东八区(业务时区)偏移后再转 DATE,
|
||||||
@@ -1963,22 +1993,25 @@ async def get_stats(
|
|||||||
]
|
]
|
||||||
|
|
||||||
# ── 各团队积分消耗(有团队 vs 无团队,使用流水中的团队快照)
|
# ── 各团队积分消耗(有团队 vs 无团队,使用流水中的团队快照)
|
||||||
# 净消耗 = 实际消费(charge_action IS NULL 或 charge) - 退款(charge_action=refund 不限制)
|
# 净消耗 = (真实消费 charge + 预扣占用 hold) - (真实退款 refund + 预扣释放 hold_release)
|
||||||
# 注意:real_credit_charge_filter 仅应限制 consume 类型(预扣 hold 释放不算真实消费);
|
# 注意:
|
||||||
# refund 类型的 charge_action = 'refund',不能套用同一个 filter,
|
# hold(预扣占用):type=consume,charge_action='hold',amount<0 → 加项
|
||||||
# 所以把该 filter 下移到 CASE WHEN 内而不是写在 WHERE 中,避免退款被误过滤。
|
# hold_release(预扣释放):type=refund,charge_action='hold_release',amount>0 → 减项(type=refund 天然包含)
|
||||||
_consume_filter = and_(
|
# charge(真实扣费):type=consume,charge/NULL → 加项
|
||||||
|
# refund(真实退款):type=refund,refund/NULL → 减项
|
||||||
|
_charge_hold_filter = and_(
|
||||||
CreditRecord.type == "consume",
|
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)
|
_charge_hold_expr = case((_charge_hold_filter, func.abs(CreditRecord.amount)), else_=0)
|
||||||
_refund_expr = case((CreditRecord.type == "refund", 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(
|
team_credit_rows = (await db.execute(
|
||||||
select(
|
select(
|
||||||
func.coalesce(CreditRecord.team_name_snapshot, '未分配团队').label('team_name'),
|
func.coalesce(CreditRecord.team_name_snapshot, '未分配团队').label('team_name'),
|
||||||
CreditRecord.team_id_snapshot.label('team_id'),
|
CreditRecord.team_id_snapshot.label('team_id'),
|
||||||
func.coalesce(func.sum(_consume_expr), 0).label("total_consume"),
|
func.coalesce(func.sum(_charge_hold_expr), 0).label("total_charge_hold"),
|
||||||
func.coalesce(func.sum(_refund_expr), 0).label("total_refund"),
|
func.coalesce(func.sum(_refund_release_expr), 0).label("total_refund_release"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
CreditRecord.type.in_(["consume", "refund"]),
|
CreditRecord.type.in_(["consume", "refund"]),
|
||||||
@@ -1986,14 +2019,14 @@ 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(_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()
|
)).all()
|
||||||
credits_by_team = [
|
credits_by_team = [
|
||||||
TeamCreditOut(
|
TeamCreditOut(
|
||||||
team_name=row.team_name,
|
team_name=row.team_name,
|
||||||
team_id=row.team_id,
|
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
|
for row in team_credit_rows
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -162,8 +162,17 @@ class AdminCreditRecordSummaryOut(BaseModel):
|
|||||||
total_recharge: float = 0.0
|
total_recharge: float = 0.0
|
||||||
total_consume: float = 0.0
|
total_consume: float = 0.0
|
||||||
total_refund: float = 0.0
|
total_refund: float = 0.0
|
||||||
# 净消耗 = 真实消费(预扣释放不算)- 退款,这才是真正的"消耗了多少积分"口径
|
# 消费类分解(仅 type=consume,不含 team_internal 团队内部转账;真实扣费 + 预扣占用 = total_consume)
|
||||||
# 前端展示"总消耗积分"时应使用此字段,而不是 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
|
net_consume: float = 0.0
|
||||||
transaction_count: int = 0
|
transaction_count: int = 0
|
||||||
generation_count: int = 0
|
generation_count: int = 0
|
||||||
|
|||||||
@@ -296,26 +296,80 @@ async def list_admin_credit_records(
|
|||||||
items = [_record_to_item(record, user, deleted_map) for record, user in rows]
|
items = [_record_to_item(record, user, deleted_map) for record, user in rows]
|
||||||
|
|
||||||
# 说明:
|
# 说明:
|
||||||
# - consume / team_internal:amount 是负数(扣减积分),统计用 abs() 保证为正值
|
# - consume 类型:amount 是负数(扣减积分),统计用 abs() 保证为正值
|
||||||
# - refund:amount 是正数(退回积分),但为兼容旧数据/边缘场景也用 abs() 保证统计值恒正
|
# team_internal(团队内部积分流转/管理员分配)不参与消费/扣费统计——它不是真实消费
|
||||||
# 避免前台筛选"退款"类型时总退款显示为 0/负数导致用户误以为没有统计
|
# - refund 类型:amount 是正数(退回积分),为兼容旧数据/边缘场景也用 abs() 保证统计值恒正
|
||||||
# - recharge: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(
|
summary_query = select(
|
||||||
|
# 0: 充值
|
||||||
func.coalesce(func.sum(case((CreditRecord.type == "recharge", CreditRecord.amount), else_=0)), 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),
|
# 1: 总消费 = total_charge + total_hold(真实扣费 + 预扣占用)
|
||||||
func.coalesce(func.sum(case((CreditRecord.type == "refund", func.abs(CreditRecord.amount)), else_=0)), 0),
|
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(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))),
|
# 4-11: 生成条数 / 尝试次数 / 图片视频条数 / 图片视频提词分析消费(仍按 charge 口径)
|
||||||
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, _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 == "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(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 == "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.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.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.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 == "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.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.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.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.ANALYSIS.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.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.total_tokens), 0),
|
||||||
func.coalesce(func.sum(CreditRecord.input_tokens), 0),
|
func.coalesce(func.sum(CreditRecord.input_tokens), 0),
|
||||||
func.coalesce(func.sum(CreditRecord.output_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)
|
).select_from(CreditRecord).join(User, CreditRecord.user_id == User.id, isouter=True)
|
||||||
if where_clause is not None:
|
if where_clause is not None:
|
||||||
summary_query = summary_query.where(where_clause)
|
summary_query = summary_query.where(where_clause)
|
||||||
@@ -323,12 +377,20 @@ async def list_admin_credit_records(
|
|||||||
_total_recharge = _round2(s[0])
|
_total_recharge = _round2(s[0])
|
||||||
_total_consume = _round2(s[1])
|
_total_consume = _round2(s[1])
|
||||||
_total_refund = _round2(s[2])
|
_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))
|
_net_consume = _round2(max(_total_consume - _total_refund, 0.0))
|
||||||
summary = {
|
summary = {
|
||||||
"total_recharge": _total_recharge,
|
"total_recharge": _total_recharge,
|
||||||
"total_consume": _total_consume,
|
"total_consume": _total_consume,
|
||||||
"total_refund": _total_refund,
|
"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,
|
"net_consume": _net_consume,
|
||||||
"transaction_count": int(s[3] or 0),
|
"transaction_count": int(s[3] or 0),
|
||||||
"generation_count": int(s[4] or 0),
|
"generation_count": int(s[4] or 0),
|
||||||
|
|||||||
Reference in New Issue
Block a user