积分冻结释放
This commit is contained in:
@@ -326,6 +326,7 @@ export async function getCreditRecords(filters?: AdminCreditRecordQueryParams):
|
||||
setMaybe(params, 'credit_subject', filters?.creditSubject);
|
||||
setMaybe(params, 'media_type', filters?.mediaType);
|
||||
setMaybe(params, 'charge_kind', filters?.chargeKind);
|
||||
setMaybe(params, 'charge_action', filters?.chargeAction);
|
||||
setMaybe(params, 'source_module', filters?.sourceModule);
|
||||
setMaybe(params, 'source_step_code', filters?.sourceStepCode);
|
||||
setMaybe(params, 'billing_scene', filters?.billingScene);
|
||||
|
||||
@@ -38,6 +38,14 @@ const RECORD_TYPE_MAP: Record<string, { text: string; color: string; icon: React
|
||||
team_internal: { text: '团队内部', color: 'cyan', icon: <WalletOutlined /> },
|
||||
};
|
||||
|
||||
|
||||
const CHARGE_ACTION_MAP: Record<string, { text: string; color: string }> = {
|
||||
charge: { text: '真实扣费', color: 'red' },
|
||||
refund: { text: '真实退款', color: 'blue' },
|
||||
hold: { text: '预扣占用', color: 'gold' },
|
||||
hold_release: { text: '预扣释放', color: 'green' },
|
||||
};
|
||||
|
||||
const userScopeOptions = [
|
||||
{ value: '', label: '全部用户' },
|
||||
{ value: 'admin', label: '后台用户' },
|
||||
@@ -85,6 +93,15 @@ const chargeKindOptions = [
|
||||
{ value: 'team_internal', label: '团队内部转移' },
|
||||
];
|
||||
|
||||
|
||||
const chargeActionOptions = [
|
||||
{ value: '', label: '全部交易动作' },
|
||||
{ value: 'charge', label: '真实扣费' },
|
||||
{ value: 'refund', label: '真实退款' },
|
||||
{ value: 'hold', label: '预扣占用' },
|
||||
{ value: 'hold_release', label: '预扣释放' },
|
||||
];
|
||||
|
||||
const sourceModuleOptions = [
|
||||
{ value: '', label: '全部模块' },
|
||||
{ value: 'ai_creation', label: 'AI创作' },
|
||||
@@ -171,6 +188,7 @@ const AdminCreditRecords: React.FC = () => {
|
||||
const [creditSubject, setCreditSubject] = useState('');
|
||||
const [mediaType, setMediaType] = useState('');
|
||||
const [chargeKind, setChargeKind] = useState('');
|
||||
const [chargeAction, setChargeAction] = useState('');
|
||||
const [sourceModule, setSourceModule] = useState('');
|
||||
const [sourceStepCode, setSourceStepCode] = useState('');
|
||||
const [billingScene, setBillingScene] = useState('');
|
||||
@@ -186,13 +204,14 @@ const AdminCreditRecords: React.FC = () => {
|
||||
creditSubject: creditSubject || undefined,
|
||||
mediaType: mediaType || undefined,
|
||||
chargeKind: chargeKind || undefined,
|
||||
chargeAction: chargeAction || undefined,
|
||||
sourceModule: sourceModule || undefined,
|
||||
sourceStepCode: sourceStepCode || undefined,
|
||||
billingScene: billingScene || undefined,
|
||||
startDate: dateRange[0]?.format('YYYY-MM-DD'),
|
||||
endDate: dateRange[1]?.format('YYYY-MM-DD'),
|
||||
...buildScope(userScope),
|
||||
}), [page, pageSize, userNameFilter, teamFilter, recordType, creditSubject, mediaType, chargeKind, sourceModule, sourceStepCode, billingScene, dateRange, userScope]);
|
||||
}), [page, pageSize, userNameFilter, teamFilter, recordType, creditSubject, mediaType, chargeKind, chargeAction, sourceModule, sourceStepCode, billingScene, dateRange, userScope]);
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
@@ -221,6 +240,7 @@ const AdminCreditRecords: React.FC = () => {
|
||||
setCreditSubject('');
|
||||
setMediaType('');
|
||||
setChargeKind('');
|
||||
setChargeAction('');
|
||||
setSourceModule('');
|
||||
setSourceStepCode('');
|
||||
setBillingScene('');
|
||||
@@ -255,6 +275,7 @@ const AdminCreditRecords: React.FC = () => {
|
||||
{ title: '前台归类', maxWidth: 18, render: (r) => r.frontendUserKindLabel || '-' },
|
||||
{ title: '归属团队', maxWidth: 20, render: (r) => r.teamNameSnapshot || '未分配团队' },
|
||||
{ title: '流水类型', maxWidth: 14, align: 'center', render: (r) => r.recordTypeLabel || r.type || '-' },
|
||||
{ title: '交易动作', maxWidth: 16, align: 'center', render: (r) => r.chargeActionLabel || (r.chargeAction ? (CHARGE_ACTION_MAP[r.chargeAction]?.text || r.chargeAction) : '-') },
|
||||
{ title: '积分类型', maxWidth: 20, render: (r) => r.creditSubjectLabel || '-' },
|
||||
{ title: '扣费子类', maxWidth: 22, render: (r) => r.chargeKindLabel || '-' },
|
||||
{ title: '模块', maxWidth: 20, render: (r) => r.sourceModuleLabel || '-' },
|
||||
@@ -321,6 +342,7 @@ const AdminCreditRecords: React.FC = () => {
|
||||
{ title: '用户类型', dataIndex: 'userTypeLabel', width: 120, render: (_: string, r: AdminCreditRecord) => <Tag color={r.userType === 'admin' ? 'orange' : 'blue'}>{r.userTypeLabel || '-'}</Tag> },
|
||||
{ title: '归属团队', dataIndex: 'teamNameSnapshot', width: 130, render: (v: string) => v ? <Tag color="blue">{v}</Tag> : <Typography.Text type="secondary">未分配</Typography.Text> },
|
||||
{ title: '流水类型', dataIndex: 'recordType', width: 100, render: (v: string, r: AdminCreditRecord) => { const cfg = RECORD_TYPE_MAP[v] || { text: r.recordTypeLabel || v || '-', color: 'default', icon: null }; return <Tag color={cfg.color} icon={cfg.icon}>{cfg.text}</Tag>; } },
|
||||
{ title: '交易动作', dataIndex: 'chargeAction', width: 110, render: (v: string, r: AdminCreditRecord) => { const cfg = CHARGE_ACTION_MAP[v] || { text: r.chargeActionLabel || v || '-', color: 'default' }; return v ? <Tag color={cfg.color}>{r.chargeActionLabel || cfg.text}</Tag> : <Typography.Text type="secondary">历史</Typography.Text>; } },
|
||||
{ title: '积分类型', dataIndex: 'creditSubjectLabel', width: 150, render: (v: string) => <Tag>{v || '-'}</Tag> },
|
||||
{ title: '模块', dataIndex: 'sourceModuleLabel', width: 130, render: (v: string) => v || '-' },
|
||||
{ title: '步骤/场景', key: 'scene', width: 210, render: (_: any, r: AdminCreditRecord) => <div><div>{r.billingSceneLabel || '-'}</div><div style={{ fontSize: 12, color: '#94a3b8' }}>{r.sourceStepCodeLabel || '-'}</div></div> },
|
||||
@@ -368,6 +390,7 @@ const AdminCreditRecords: React.FC = () => {
|
||||
<Select value={creditSubject} onChange={(v) => { setPage(1); setCreditSubject(v); }} style={{ width: 180 }} options={creditSubjectOptions} />
|
||||
<Select value={mediaType} onChange={(v) => { setPage(1); setMediaType(v); }} style={{ width: 110 }} options={mediaTypeOptions} />
|
||||
<Select value={chargeKind} onChange={(v) => { setPage(1); setChargeKind(v); }} style={{ width: 150 }} options={chargeKindOptions} />
|
||||
<Select value={chargeAction} onChange={(v) => { setPage(1); setChargeAction(v); }} style={{ width: 140 }} options={chargeActionOptions} />
|
||||
<Select value={sourceModule} onChange={(v) => { setPage(1); setSourceModule(v); }} style={{ width: 150 }} options={sourceModuleOptions} />
|
||||
<Select value={sourceStepCode} onChange={(v) => { setPage(1); setSourceStepCode(v); }} style={{ width: 150 }} options={sourceStepOptions} />
|
||||
<Select value={billingScene} onChange={(v) => { setPage(1); setBillingScene(v); }} style={{ width: 220 }} options={billingSceneOptions} />
|
||||
@@ -394,7 +417,7 @@ const AdminCreditRecords: React.FC = () => {
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条记录`,
|
||||
}}
|
||||
scroll={{ x: 2050 }}
|
||||
scroll={{ x: 2160 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -48,8 +48,14 @@ const AdminSettings: React.FC = () => {
|
||||
setConfigs(data);
|
||||
const formValues: Record<string, any> = {};
|
||||
data.forEach(c => { formValues[c.key] = c.value; });
|
||||
// 预扣积分默认值
|
||||
// LLM 预扣积分默认值
|
||||
if (!formValues.optimize_hold_credits) formValues.optimize_hold_credits = '5';
|
||||
if (!formValues.llm_billing_enabled) formValues.llm_billing_enabled = 'true';
|
||||
if (!formValues.llm_hold_credits_default) formValues.llm_hold_credits_default = '5';
|
||||
if (!formValues.llm_hold_credits_generation_record_prompt) formValues.llm_hold_credits_generation_record_prompt = '5';
|
||||
if (!formValues.llm_hold_credits_module_image_prompt) formValues.llm_hold_credits_module_image_prompt = '5';
|
||||
if (!formValues.llm_hold_credits_module_video_prompt) formValues.llm_hold_credits_module_video_prompt = '10';
|
||||
if (!formValues.llm_hold_credits_shot_video_analysis) formValues.llm_hold_credits_shot_video_analysis = '10';
|
||||
formValues.resource_capacity_enabled = capacity.enabled;
|
||||
formValues.resource_capacity_limit_value = capacity.limitValue || '1.000';
|
||||
formValues.resource_capacity_limit_unit = capacity.limitUnit || 'GB';
|
||||
@@ -64,25 +70,80 @@ const AdminSettings: React.FC = () => {
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const llmBillingEnabled = !['0', 'false', 'no', 'off', 'disabled'].includes(
|
||||
String(values.llm_billing_enabled ?? 'true').trim().toLowerCase(),
|
||||
);
|
||||
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);
|
||||
const llmManagedKeys = new Set([
|
||||
'optimize_hold_credits',
|
||||
'llm_billing_enabled',
|
||||
'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',
|
||||
]);
|
||||
for (const config of configs) {
|
||||
if (llmManagedKeys.has(config.key)) continue;
|
||||
const newVal = values[config.key];
|
||||
if (newVal !== undefined && String(newVal) !== config.value) {
|
||||
await updateSystemConfig(config.id, String(newVal ?? ''));
|
||||
}
|
||||
}
|
||||
// AI创作预扣积分 - 不存在则创建
|
||||
const holdVal = values.optimize_hold_credits;
|
||||
if (holdVal !== undefined && holdVal !== null && holdVal !== '') {
|
||||
const existing = configs.find(c => c.key === 'optimize_hold_credits');
|
||||
const saveManagedConfig = async (key: string, value: unknown, description: string) => {
|
||||
if (value === undefined || value === null || value === '') return;
|
||||
const normalizedValue = String(value);
|
||||
const existing = configs.find(c => c.key === key);
|
||||
if (existing) {
|
||||
if (String(holdVal) !== existing.value) {
|
||||
await updateSystemConfig(existing.id, String(holdVal));
|
||||
}
|
||||
if (normalizedValue !== existing.value) await updateSystemConfig(existing.id, normalizedValue);
|
||||
} else {
|
||||
await createSystemConfig('optimize_hold_credits', String(holdVal), '提示词理解预扣积分数量(防止并发超卖)');
|
||||
await createSystemConfig(key, normalizedValue, description);
|
||||
}
|
||||
};
|
||||
|
||||
const enabledConfig = [
|
||||
'llm_billing_enabled',
|
||||
values.llm_billing_enabled,
|
||||
'是否启用 LLM 统一预扣与真实扣费结算',
|
||||
] as const;
|
||||
const llmHoldConfigs = [
|
||||
['optimize_hold_credits', values.optimize_hold_credits, '提示词理解预扣积分数量(防止并发超卖)'],
|
||||
['llm_hold_credits_default', values.llm_hold_credits_default, 'LLM 默认预扣积分数量'],
|
||||
['llm_hold_credits_generation_record_prompt', values.llm_hold_credits_generation_record_prompt, 'AI创作提示词优化预扣积分数量'],
|
||||
['llm_hold_credits_module_image_prompt', values.llm_hold_credits_module_image_prompt, '模块图片 AI 提词优化预扣积分数量'],
|
||||
['llm_hold_credits_module_video_prompt', values.llm_hold_credits_module_video_prompt, '模块视频 AI 提词优化预扣积分数量'],
|
||||
['llm_hold_credits_shot_video_analysis', values.llm_hold_credits_shot_video_analysis, '拆镜视频分析预扣积分数量'],
|
||||
] as const;
|
||||
|
||||
// 关闭时先关开关,随后允许保存 0;启用时先保存正数预扣,最后再打开开关。
|
||||
if (!llmBillingEnabled) {
|
||||
await saveManagedConfig(...enabledConfig);
|
||||
}
|
||||
for (const config of llmHoldConfigs) {
|
||||
await saveManagedConfig(...config);
|
||||
}
|
||||
if (llmBillingEnabled) {
|
||||
await saveManagedConfig(...enabledConfig);
|
||||
}
|
||||
|
||||
await saveGlobalResourceCapacity({
|
||||
enabled: !!values.resource_capacity_enabled,
|
||||
limitValue: String(values.resource_capacity_limit_value ?? '1.000'),
|
||||
@@ -193,7 +254,7 @@ const AdminSettings: React.FC = () => {
|
||||
'SEO 设置': configs.filter(c => c.key.startsWith('seo_')),
|
||||
'用户积分配置': configs.filter(c => c.key.startsWith('user_') && c.key.includes('credits')),
|
||||
'其他配置': configs.filter(c => c.key === 'operation_manual'),
|
||||
'AI创作配置': configs.filter(c => c.key === 'optimize_hold_credits'),
|
||||
'AI创作配置': configs.filter(c => c.key === 'optimize_hold_credits' || c.key.startsWith('llm_')),
|
||||
};
|
||||
|
||||
const getFieldDescription = (config: SystemConfig): string => {
|
||||
@@ -209,7 +270,13 @@ const AdminSettings: React.FC = () => {
|
||||
user_login_credits: '用户每日登录赠送的积分数量',
|
||||
user_login_credits_enabled: '是否启用每日登录赠送积分功能',
|
||||
operation_manual: '操作手册链接,前台用户菜单将展示该入口,点击跳转此链接',
|
||||
optimize_hold_credits: 'AI创作时预扣积分数量,用于防止并发超卖。预扣后按实际消耗多退少补',
|
||||
optimize_hold_credits: '兼容旧配置。新 LLM 配置为空时回退使用该值',
|
||||
llm_billing_enabled: '是否启用 LLM 统一预扣、释放预扣和真实扣费结算',
|
||||
llm_hold_credits_default: 'LLM 场景默认预扣积分,场景配置为空时使用',
|
||||
llm_hold_credits_generation_record_prompt: 'AI创作提示词优化发起前预扣积分',
|
||||
llm_hold_credits_module_image_prompt: '爆款开头/拆镜复刻图片 AI 提词优化发起前预扣积分',
|
||||
llm_hold_credits_module_video_prompt: '爆款开头/拆镜复刻视频 AI 提词优化发起前预扣积分',
|
||||
llm_hold_credits_shot_video_analysis: '拆镜原视频/片段视频分析发起前预扣积分',
|
||||
};
|
||||
return descMap[config.key] || config.description || '';
|
||||
};
|
||||
@@ -339,7 +406,7 @@ const AdminSettings: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (config.key === 'user_register_credits' || config.key === 'user_login_credits' || config.key === 'optimize_hold_credits') {
|
||||
if (config.key === 'user_register_credits' || config.key === 'user_login_credits' || config.key === 'optimize_hold_credits' || config.key.startsWith('llm_hold_credits')) {
|
||||
return <Input type="number" min={1} placeholder={config.description} size="large" />;
|
||||
}
|
||||
return <Input placeholder={config.description} size="large" />;
|
||||
@@ -403,11 +470,23 @@ const AdminSettings: React.FC = () => {
|
||||
{/* AI创作预扣积分 - 固定显示 */}
|
||||
<Form.Item
|
||||
name="optimize_hold_credits"
|
||||
label={<span style={{ fontWeight: 500 }}>提示词理解预扣积分数量</span>}
|
||||
extra="AI创作时预扣积分数量,用于防止并发超卖。预扣后按实际消耗多退少补"
|
||||
label={<span style={{ fontWeight: 500 }}>兼容旧预扣积分数量</span>}
|
||||
extra="兼容旧配置。新 LLM 场景配置为空时回退使用该值"
|
||||
>
|
||||
<Input type="number" min={1} placeholder="默认5" size="large" />
|
||||
<Input type="number" min={0} placeholder="默认5" size="large" />
|
||||
</Form.Item>
|
||||
{[
|
||||
['llm_billing_enabled', '启用 LLM 统一计费', 'true 表示启用,false 表示关闭'],
|
||||
['llm_hold_credits_default', 'LLM 默认预扣积分', '默认5'],
|
||||
['llm_hold_credits_generation_record_prompt', 'AI创作提词预扣积分', '默认5'],
|
||||
['llm_hold_credits_module_image_prompt', '模块图片提词预扣积分', '默认5'],
|
||||
['llm_hold_credits_module_video_prompt', '模块视频提词预扣积分', '默认10'],
|
||||
['llm_hold_credits_shot_video_analysis', '拆镜视频分析预扣积分', '默认10'],
|
||||
].map(([name, label, extra]) => (
|
||||
<Form.Item key={name} name={name} label={<span style={{ fontWeight: 500 }}>{label}</span>} extra={extra}>
|
||||
{name === 'llm_billing_enabled' ? <Input placeholder="true / false" size="large" /> : <Input type="number" min={1} placeholder={extra} size="large" />}
|
||||
</Form.Item>
|
||||
))}
|
||||
</div>
|
||||
</Form>
|
||||
),
|
||||
|
||||
@@ -913,6 +913,7 @@ export interface AdminCreditRecord {
|
||||
chargeKind?: string;
|
||||
chargeKindLabel?: string;
|
||||
chargeAction?: string;
|
||||
chargeActionLabel?: string;
|
||||
creditSubject?: string;
|
||||
creditSubjectLabel?: string;
|
||||
mediaType?: string;
|
||||
@@ -956,6 +957,7 @@ export interface AdminCreditRecordQueryParams {
|
||||
creditSubject?: string;
|
||||
mediaType?: string;
|
||||
chargeKind?: string;
|
||||
chargeAction?: string;
|
||||
sourceModule?: string;
|
||||
sourceStepCode?: string;
|
||||
billingScene?: string;
|
||||
|
||||
Reference in New Issue
Block a user