积分消耗明细改版V1

This commit is contained in:
2026-08-12 11:49:53 +08:00
parent e3719eae09
commit eb7c60684e
39 changed files with 648 additions and 975 deletions
+1 -1
View File
@@ -1328,7 +1328,7 @@ export async function adminDeductCredits(userId: string, payload: Record<string,
return api.post(`/admin/credit-management/users/${userId}/deduct`, payload);
}
// ── LLM Fixed Pre-deduct ──────────────────────────────────
// ── LLM 场景积分配置 ───────────────────────────────────────
export async function getLlmBillingPolicies(): Promise<import('../types').LlmBillingPolicy[]> {
return api.get('/admin/llm-billing/policies');
}
@@ -47,10 +47,11 @@ const RECORD_TYPE_MAP: Record<string, { text: string; color: string; icon: React
const CHARGE_ACTION_MAP: Record<string, { text: string; color: string }> = {
charge: { text: '真实费', color: 'red' },
charge: { text: '真实费', color: 'red' },
refund: { text: '真实退款', color: 'blue' },
hold: { text: '预扣占用', color: 'gold' },
hold_release: { text: '预扣释放', color: 'green' },
pre_deduct: { text: '历史固定预扣', color: 'default' },
hold: { text: '历史预扣占用', color: 'gold' },
hold_release: { text: '历史预扣释放', color: 'green' },
};
const userScopeOptions = [
@@ -105,10 +106,11 @@ const chargeKindOptions = [
const chargeActionOptions = [
{ value: '', label: '全部交易动作' },
{ value: 'charge', label: '真实费' },
{ value: 'charge', label: '真实费' },
{ value: 'refund', label: '真实退款' },
{ value: 'hold', label: '预扣占用' },
{ value: 'hold_release', label: '预扣释放' },
{ value: 'pre_deduct', label: '历史固定预扣' },
{ value: 'hold', label: '历史预扣占用' },
{ value: 'hold_release', label: '历史预扣释放' },
];
const sourceModuleOptions = [
@@ -306,7 +308,7 @@ const AdminCreditRecords: React.FC = () => {
{ title: '说明', minWidth: 18, maxWidth: 42, render: (r) => r.description || '' },
{ title: '业务归属类型', maxWidth: 18, render: (r) => r.ownerType || '' },
{ title: '业务归属ID', maxWidth: 28, render: (r) => r.ownerId || '' },
{ title: '积分来源分摊', minWidth: 24, maxWidth: 48, render: (r) => (r.allocations || []).map(a => `${a.sourceType || '-'}:${a.sourceId || '-'}=${a.amount}`).join('') },
{ title: '积分来源分摊', minWidth: 24, maxWidth: 48, render: (r) => (r.allocations || []).map(a => `${a.sourceTypeLabel || a.sourceType || '-'}:${a.sourceId || '-'}=${a.amount}`).join('') },
{ title: 'BizKey', maxWidth: 36, render: (r) => r.bizKey || '' },
];
@@ -322,22 +324,18 @@ const AdminCreditRecords: React.FC = () => {
],
summaryRows: [
['总充值', exportSummary.totalRecharge],
['总消费(真实扣费 + 预扣占用)', 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.totalConsume],
['总真实退款', exportSummary.totalRefund],
['净消耗(总消费 总退款,≥ 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],
@@ -389,32 +387,18 @@ const AdminCreditRecords: React.FC = () => {
<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={{ color: '#94a3b8' }}></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 style={{ fontSize: 11, color: '#94a3b8', marginTop: 2 }}></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={{ color: '#94a3b8' }}>退</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 style={{ fontSize: 11, color: '#94a3b8', marginTop: 2 }}>退</div>
</div>
</Space>
</Card>
@@ -423,7 +407,7 @@ const AdminCreditRecords: React.FC = () => {
<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 style={{ fontSize: 11, color: '#818cf8', marginTop: 2 }}> 退 0</div>
</div>
</Space>
</Card>
@@ -498,8 +482,8 @@ const AdminCreditRecords: React.FC = () => {
rowKey="id"
dataSource={record.allocations || []}
columns={[
{ title: '动作', dataIndex: 'allocationAction', width: 150 },
{ title: '积分来源', dataIndex: 'sourceType', width: 160, render: (v: string) => v || '-' },
{ title: '动作', dataIndex: 'allocationActionLabel', width: 150, render: (v: string, r: any) => v || r.allocationAction || '-' },
{ title: '积分来源', dataIndex: 'sourceTypeLabel', width: 160, render: (v: string, r: any) => v || r.sourceType || '-' },
{ title: '来源ID', dataIndex: 'sourceId', width: 260, render: (v: string) => v || '-' },
{ title: '积分数量', dataIndex: 'amount', width: 120, render: (v: number) => n(v) },
{ title: '生效时间', dataIndex: 'validFrom', width: 190, render: (v: string) => v ? formatDate(v) : '-' },
@@ -6,7 +6,7 @@ import type { LlmBillingExecution } from '../types';
import { formatDate, formatDatePrecise } from '../utils/formatDate';
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
pre_deducted: { label: '已预扣', color: 'blue' },
charged: { label: '已消费', color: 'blue' },
processing: { label: '处理中', color: 'processing' },
succeeded: { label: '业务成功', color: 'success' },
final_failed: { label: '最终失败', color: 'error' },
@@ -41,7 +41,7 @@ const AdminLlmBillingExecutions: React.FC = () => {
}))} />
<Descriptions bordered size="small" column={4} items={[
{ key: 'scene', label: '功能', children: record.sceneNameSnapshot },
{ key: 'credit', label: '固定预扣', children: record.preDeductCredits },
{ key: 'credit', label: '消费积分', children: record.chargeCredits },
{ key: 'calls', label: '调用', children: `${record.totalCallCount}(成功 ${record.successfulCallCount} / 失败 ${record.failedCallCount}` },
{ key: 'tokens', label: '累计 Token', children: record.totalTokens },
{ key: 'refundA', label: '有效积分退回', children: record.refundAvailableCredits },
@@ -53,7 +53,7 @@ const AdminLlmBillingExecutions: React.FC = () => {
{ title: '用户', dataIndex: 'userId' },
{ title: '业务对象', render: (_: unknown, r: LlmBillingExecution) => `${r.ownerType}:${r.ownerId}` },
{ title: '模型', dataIndex: 'modelNameSnapshot' },
{ title: '预扣', dataIndex: 'preDeductCredits' },
{ title: '消费积分', dataIndex: 'chargeCredits' },
{ title: '调用/Token', render: (_: unknown, r: LlmBillingExecution) => `${r.totalCallCount} / ${r.totalTokens}` },
{ title: '状态', dataIndex: 'status', render: (v: string) => { const config = STATUS_LABELS[v] || { label: v, color: 'default' }; return <Tag color={config.color}>{config.label}</Tag>; } },
{ title: '请求时间', dataIndex: 'requestTime', render: (v: string) => formatDate(v) },
@@ -20,32 +20,64 @@ const AdminLlmBillingPolicies: React.FC = () => {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const load = async () => { setLoading(true); try { setItems(await getLlmBillingPolicies()); } finally { setLoading(false); } };
const load = async () => {
setLoading(true);
try {
setItems(await getLlmBillingPolicies());
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, []);
const edit = (item?: LlmBillingPolicy) => { setEditing(item || null); setOpen(true); form.resetFields(); form.setFieldsValue(item ? { scene_code: item.sceneCode, pre_deduct_credits: item.preDeductCredits, is_active: item.isActive } : { is_active: true, pre_deduct_credits: 5 }); };
const edit = (item?: LlmBillingPolicy) => {
setEditing(item || null);
setOpen(true);
form.resetFields();
form.setFieldsValue(item
? { scene_code: item.sceneCode, charge_credits: item.chargeCredits, is_active: item.isActive }
: { is_active: true, charge_credits: 5 });
};
const save = async () => {
try {
const values = await form.validateFields();
if (editing) await updateLlmBillingPolicy(editing.id, values); else await createLlmBillingPolicy(values);
message.success('已保存'); setOpen(false); load();
} catch (error: any) { if (!error?.errorFields) message.error(error?.message || '保存失败'); }
if (editing) await updateLlmBillingPolicy(editing.id, values);
else await createLlmBillingPolicy(values);
message.success('保存');
setOpen(false);
load();
} catch (error: any) {
if (!error?.errorFields) message.error(error?.message || '保存失败');
}
};
return <Card title={<Space><RobotOutlined />LLM固定预扣配置</Space>} extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => edit()}></Button>}>
return <Card
title={<Space><RobotOutlined />LLM积分配置</Space>}
extra={<Button type="primary" icon={<PlusOutlined />} onClick={() => edit()}></Button>}
>
<Table rowKey="id" loading={loading} dataSource={items} columns={[
{ title: '功能', dataIndex: 'sceneName' },
{ title: '场景编码', dataIndex: 'sceneCode' },
{ title: '固定预扣积分', dataIndex: 'preDeductCredits' },
{ title: '消费积分', dataIndex: 'chargeCredits' },
{ title: '版本', dataIndex: 'version' },
{ title: '状态', dataIndex: 'isActive', render: (v: boolean) => <Tag color={v ? 'green' : 'default'}>{v ? '启用' : '停用'}</Tag> },
{ title: '操作', render: (_: unknown, r: LlmBillingPolicy) => <Button type="link" icon={<EditOutlined />} onClick={() => edit(r)}></Button> },
]} />
<Modal open={open} title={editing ? '编辑LLM场景预扣' : '新增LLM场景预扣'} onOk={save} onCancel={() => setOpen(false)}>
<Modal open={open} title={editing ? '编辑LLM积分场景' : '新增LLM积分场景'} onOk={save} onCancel={() => setOpen(false)}>
<Form form={form} layout="vertical">
<Form.Item name="scene_code" label="业务功能" rules={[{ required: true }]}><Select disabled={!!editing} options={SCENES} /></Form.Item>
<Form.Item name="pre_deduct_credits" label="固定预扣积分" rules={[{ required: true }]}><InputNumber min={0.01} precision={2} style={{ width: '100%' }} /></Form.Item>
<Form.Item name="scene_code" label="业务功能" rules={[{ required: true }]}>
<Select disabled={!!editing} options={SCENES} />
</Form.Item>
<Form.Item name="charge_credits" label="消费积分" rules={[{ required: true }]}>
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="is_active" label="启用" valuePropName="checked"><Switch /></Form.Item>
</Form>
</Modal>
</Card>;
};
export default AdminLlmBillingPolicies;
@@ -63,31 +63,6 @@ const AdminSettings: React.FC = () => {
try {
const values = await form.validateFields();
// 仅当 llm_billing_enabled 字段在当前标签页渲染时,才校验预扣积分
const llmBillingEnabled = values.llm_billing_enabled !== undefined && values.llm_billing_enabled !== null
? !['0', 'false', 'no', 'off', 'disabled'].includes(
String(values.llm_billing_enabled).trim().toLowerCase(),
)
: false;
if (llmBillingEnabled) {
const holdKeys = [
'optimize_hold_credits',
'llm_hold_credits_default',
'llm_hold_credits_generation_record_prompt',
'llm_hold_credits_module_image_prompt',
'llm_hold_credits_module_video_prompt',
'llm_hold_credits_shot_video_analysis',
];
const invalidKey = holdKeys.find((key) => {
const numericValue = Number(values[key]);
return !Number.isFinite(numericValue) || numericValue <= 0;
});
if (invalidKey) {
message.error('启用 LLM 统一计费时,所有预扣积分必须大于 0');
return;
}
}
setSaving(true);
for (const config of configs) {
const newVal = values[config.key];
+3 -3
View File
@@ -816,9 +816,9 @@ const AdminUsers: React.FC = () => {
pagination={false}
dataSource={creditBalances}
columns={[
{ title: '来源', dataIndex: 'sourceType', width: 150, render: (v: string) => v || '-' },
{ title: '来源', dataIndex: 'sourceTypeLabel', width: 150, render: (v: string, r: any) => v || r.sourceType || '-' },
{ title: '来源ID', dataIndex: 'sourceId', width: 220, ellipsis: true, render: (v: string) => v || '-' },
{ title: '积分等级', dataIndex: 'creditLevel', width: 100, render: (v: string) => v === 'promotional' ? '活动积分' : '普通积分' },
{ title: '积分等级', dataIndex: 'creditLevelLabel', width: 100, render: (v: string, r: any) => v || r.creditLevel || '-' },
{ title: '发放', dataIndex: 'grantAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
{ title: '剩余', dataIndex: 'unspentAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
{ title: '已消费', dataIndex: 'consumedAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
@@ -826,7 +826,7 @@ const AdminUsers: React.FC = () => {
{ title: '已撤销', dataIndex: 'revokedAmount', width: 100, render: (v: number) => Number(v || 0).toLocaleString() },
{ title: '生效时间', dataIndex: 'validFrom', width: 170, render: (v: string) => formatDate(v) },
{ title: '最后可用时间', dataIndex: 'lastUsableAt', width: 180, render: (v: string) => formatDate(v) },
{ title: '状态', dataIndex: 'status', width: 90, render: (v: string) => <Tag color={v === 'active' ? 'green' : v === 'expired' ? 'orange' : v === 'revoked' ? 'red' : 'default'}>{v}</Tag> },
{ title: '状态', dataIndex: 'status', width: 90, render: (v: string, r: any) => <Tag color={v === 'active' ? 'green' : v === 'expired' ? 'orange' : v === 'revoked' ? 'red' : 'default'}>{r.statusLabel || v || '-'}</Tag> },
]}
scroll={{ x: 1410, y: 460 }}
/>
+15 -12
View File
@@ -868,17 +868,17 @@ export interface VideoPromptSchemaPreviewOut {
export interface AdminCreditRecordSummary {
totalRecharge: number;
/** 总消费(仅 type=consume,不含团队内部转账)= 真实扣费 + 预扣占用 */
/** 总消费:仅真实消费,不含历史 hold/团队内部转账;兼容历史 LLM pre_deduct 真实消费 */
totalConsume: number;
/** 总回退(仅 type=refund)= 真实退款 + 预扣释放 */
/** 总退款:仅真实退款,不含历史 hold_release */
totalRefund: number;
/** 独立统计列:真实扣费 charge(含历史 NULL),对应"筛选类型=消费 & action=charge/NULL"求和 */
/** 兼容字段:与 totalConsume 同真实消费口径 */
totalCharge: number;
/** 独立统计列:预扣占用 hold */
/** 历史兼容字段:hold 占用,不计入 totalConsume */
totalHold: number;
/** 独立统计列:真实退款 refund(含历史 NULL) */
/** 兼容字段:与 totalRefund 同真实退款口径 */
totalRefundReal: number;
/** 独立统计列:预扣释放 hold_releasetype=refund, action=hold_release */
/** 历史兼容字段:hold_release,不计入 totalRefund */
totalHoldRelease: number;
/** 净消耗 = max(totalConsume - totalRefund, 0),即真正"用掉了"的积分 */
netConsume: number;
@@ -887,13 +887,13 @@ export interface AdminCreditRecordSummary {
generationAttemptCount: number;
imageGenerationCount: number;
videoGenerationCount: number;
/** 子分类消费(图片)仅真实费 charge 口径 */
/** 子分类消费(图片)仅真实费 charge 口径 */
imageConsume: number;
/** 子分类消费(视频)仅真实费 charge 口径 */
/** 子分类消费(视频)仅真实费 charge 口径 */
videoConsume: number;
/** 子分类消费(提词)仅真实费 charge 口径 */
/** 子分类消费(提词)仅真实费 charge 口径 */
textConsume: number;
/** 子分类消费(分析)仅真实费 charge 口径 */
/** 子分类消费(分析)仅真实费 charge 口径 */
analysisConsume: number;
totalTokens: number;
inputTokens: number;
@@ -905,9 +905,12 @@ export interface AdminCreditRecordAllocation {
creditBalanceId: string;
sourceAllocationId?: string | null;
allocationAction: string;
allocationActionLabel?: string;
amount: number;
creditLevel?: string;
creditLevelLabel?: string;
sourceType?: string;
sourceTypeLabel?: string;
sourceId?: string;
validFrom?: string;
expiresAt?: string;
@@ -1503,7 +1506,7 @@ export interface LlmBillingPolicy {
id: string;
sceneCode: string;
sceneName: string;
preDeductCredits: number;
chargeCredits: number;
isActive: boolean;
version: number;
createdBy?: string | null;
@@ -1545,7 +1548,7 @@ export interface LlmBillingExecution {
modelNameSnapshot?: string | null;
providerSnapshot?: string | null;
requestTime: string;
preDeductCredits: number;
chargeCredits: number;
status: string;
totalCallCount: number;
successfulCallCount: number;