fix(billing): 封住发布前计费竞态
ci / verify (pull_request) Failing after 8s
ci / verify (pull_request) Failing after 8s
阻止上游提交状态不明的任务被租约接管后重复执行,并为人工复核保留可操作的结算记录。 将生产提交绑定到当前估价签名,统一复用预处理快照,并补强规则形状、定点溢出与历史规则兼容校验。 已通过 PostgreSQL 16 集成测试、Go 全量测试与静态检查、前端测试与构建、OpenAPI、依赖审计、镜像、迁移、流水线和 SemVer 门禁。
This commit is contained in:
@@ -2,13 +2,14 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import { AlertTriangle, ReceiptText, RefreshCw, RotateCcw } from 'lucide-react';
|
||||
import type { BillingSettlement } from '@easyai-ai-gateway/contracts';
|
||||
import { listBillingSettlements, retryBillingSettlement } from '../../api';
|
||||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Select, Table, TableCell, TableHead, TableRow } from '../../components/ui';
|
||||
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, Select, Table, TableCell, TableHead, TableRow } from '../../components/ui';
|
||||
|
||||
export function BillingSettlementsPanel(props: { token: string }) {
|
||||
const [items, setItems] = useState<BillingSettlement[]>([]);
|
||||
const [status, setStatus] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [retrying, setRetrying] = useState('');
|
||||
const [confirming, setConfirming] = useState<BillingSettlement | null>(null);
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
const load = useCallback(async (signal?: AbortSignal) => {
|
||||
@@ -95,8 +96,8 @@ export function BillingSettlementsPanel(props: { token: string }) {
|
||||
<TableCell>{item.lastErrorCode || item.manualReviewReason || '-'}</TableCell>
|
||||
<TableCell>
|
||||
{(item.status === 'retryable_failed' || item.status === 'manual_review') ? (
|
||||
<Button type="button" size="xs" variant="outline" disabled={retrying === item.id} onClick={() => void retry(item)}>
|
||||
<RotateCcw size={13} />{retrying === item.id ? '提交中' : '重试'}
|
||||
<Button type="button" size="xs" variant="outline" disabled={retrying === item.id} onClick={() => setConfirming(item)}>
|
||||
<RotateCcw size={13} />{retrying === item.id ? '提交中' : retryActionLabel(item)}
|
||||
</Button>
|
||||
) : '-'}
|
||||
</TableCell>
|
||||
@@ -106,6 +107,19 @@ export function BillingSettlementsPanel(props: { token: string }) {
|
||||
) : (
|
||||
<Card><CardContent className="emptyState"><AlertTriangle size={18} /><strong>{loading ? '正在加载结算队列' : '暂无结算记录'}</strong><span>可切换状态筛选,查看待处理与人工复核记录。</span></CardContent></Card>
|
||||
)}
|
||||
<ConfirmDialog
|
||||
open={Boolean(confirming)}
|
||||
loading={Boolean(confirming && retrying === confirming.id)}
|
||||
title={confirming ? `${retryActionLabel(confirming)}这条计费记录?` : '处理计费记录?'}
|
||||
description={confirming ? retryActionDescription(confirming) : ''}
|
||||
confirmLabel={confirming ? retryActionLabel(confirming) : '确认'}
|
||||
onCancel={() => setConfirming(null)}
|
||||
onConfirm={() => {
|
||||
if (!confirming) return;
|
||||
const item = confirming;
|
||||
void retry(item).finally(() => setConfirming(null));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -126,6 +140,23 @@ function formatAmount(value: number) {
|
||||
return new Intl.NumberFormat('zh-CN', { minimumFractionDigits: 0, maximumFractionDigits: 9 }).format(value);
|
||||
}
|
||||
|
||||
function retryActionLabel(item: BillingSettlement) {
|
||||
if (item.status !== 'manual_review') return '重试';
|
||||
if (item.manualReviewReason === 'upstream_submission_unknown' && item.action === 'release') return '确认释放';
|
||||
if (item.manualReviewReason === 'historical_success_without_charge' && item.action === 'settle') return '确认结算';
|
||||
return '重新入队';
|
||||
}
|
||||
|
||||
function retryActionDescription(item: BillingSettlement) {
|
||||
if (item.manualReviewReason === 'upstream_submission_unknown' && item.action === 'release') {
|
||||
return '上游提交结果不明。确认后只会释放当前冻结,不会再次调用上游;请先完成外部核查。';
|
||||
}
|
||||
if (item.manualReviewReason === 'historical_success_without_charge' && item.action === 'settle') {
|
||||
return `确认后将按历史快照结算 ${formatAmount(item.amount)} ${item.currency},不会重新调用上游。`;
|
||||
}
|
||||
return '确认后会将该记录重新放入结算队列,不会重新调用上游。';
|
||||
}
|
||||
|
||||
function shortId(value: string) {
|
||||
return value.length > 12 ? `${value.slice(0, 8)}…${value.slice(-4)}` : value;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user