feat: remove legacy signature confirmation

This commit is contained in:
hectorzhao
2026-08-10 22:40:13 +08:00
parent 827d8921a8
commit 2ecb24cf8d
15 changed files with 237 additions and 162 deletions
@@ -1,10 +1,9 @@
import { useCallback, useEffect, useState } from 'react';
import { Check, Plus, RefreshCw, Search, Settings2, ShieldOff, Trash2 } from 'lucide-react';
import { Plus, RefreshCw, Search, Settings2, ShieldOff, Trash2 } from 'lucide-react';
import {
adminApi,
type AdminChannel,
type EnterpriseApplication,
type LegacySignatureReportTask,
type SignatureRetirementMessage,
type SignatureRetirementRule,
type SignatureRetirementRuleType,
@@ -19,11 +18,6 @@ const carrierLabels = { mobile: '移动', unicom: '联通', telecom: '电信' };
const ruleTypeLabels: Record<SignatureRetirementRuleType, string> = {
enterprise_global: '企业全局规则', enterprise_application: '企业应用特殊规则', channel_global: '通道全局规则', channel: '通道特殊规则',
};
const statusOptions = [
{ value: 'pending', label: '待处理' }, { value: 'waiting_material', label: '待材料' }, { value: 'reporting', label: '报备中' },
{ value: 'approved', label: '已通过' }, { value: 'failed', label: '失败' }, { value: 'rejected', label: '驳回' }, { value: 'abandoned', label: '已放弃' },
];
type RuleDraft = {
ruleType: SignatureRetirementRuleType; targetId: string; enabled: boolean;
mobileWindowDays: string; mobileThreshold: string; unicomWindowDays: string; unicomThreshold: string;
@@ -61,7 +55,6 @@ export function AdminSignatureRetirementPage() {
const [webhooks, setWebhooks] = useState<SignatureRetirementWebhook[]>([]);
const [messages, setMessages] = useState<SignatureRetirementMessage[]>([]);
const [suppressions, setSuppressions] = useState<SignatureRetirementSuppression[]>([]);
const [legacyTasks, setLegacyTasks] = useState<LegacySignatureReportTask[]>([]);
const [applications, setApplications] = useState<EnterpriseApplication[]>([]);
const [channels, setChannels] = useState<AdminChannel[]>([]);
const [tenants, setTenants] = useState<TenantOption[]>([]);
@@ -75,23 +68,21 @@ export function AdminSignatureRetirementPage() {
const [ruleDraft, setRuleDraft] = useState<RuleDraft | null>(null);
const [webhookOpen, setWebhookOpen] = useState(false);
const [webhookDraft, setWebhookDraft] = useState({ name: '', platform: 'wecom' as 'wecom' | 'feishu', url: '' });
const [legacyDraft, setLegacyDraft] = useState<LegacySignatureReportTask | null>(null);
const [legacyResults, setLegacyResults] = useState<Record<string, { status: string; approvedAt: string }>>({});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const loadData = useCallback(async (targetPage = 1, filters = defaultMessageFilters()) => {
setLoading(true);
try {
const [configuration, messageResult, activeSuppressions, history, applicationRows, channelRows, tenantRows] = await Promise.all([
const [configuration, messageResult, activeSuppressions, applicationRows, channelRows, tenantRows] = await Promise.all([
adminApi.getSignatureRetirementConfiguration(),
adminApi.listSignatureRetirementMessages(messageQuery(targetPage, filters)),
adminApi.listSignatureRetirementSuppressions(), adminApi.listLegacySignatureReportTasks(),
adminApi.listSignatureRetirementSuppressions(),
adminApi.listEnterpriseApplications(), adminApi.listChannels(), adminApi.listTenants(),
]);
setRules(configuration.rules); setWebhooks(configuration.webhooks.filter((item) => item.status === 'active'));
setMessages(messageResult.items); setMessageTotal(messageResult.total); setMessagePage(messageResult.page);
setSuppressions(activeSuppressions); setLegacyTasks(history);
setSuppressions(activeSuppressions);
setApplications(applicationRows); setChannels(channelRows); setTenants(tenantRows); setError('');
} catch (failure) {
setError(failure instanceof Error ? failure.message : '签名清退预警数据加载失败');
@@ -133,13 +124,6 @@ export function AdminSignatureRetirementPage() {
{ key: 'reason', title: '原因', render: (item) => item.reason || '-' },
{ key: 'actions', title: '操作', align: 'right', render: (item) => <Button icon={<ShieldOff size={15} />} onClick={() => { setActionError(''); setCancelSuppressionDraft({ id: item.id, reason: '' }); }} size="sm" variant="ghost"></Button> },
];
const legacyColumns: Array<TableColumn<LegacySignatureReportTask>> = [
{ key: 'signature', title: '签名', render: (item) => <><strong>{item.signature?.name}</strong><br /><small>{item.signature?.tenant?.name}</small></> },
{ key: 'channel', title: '历史通道', render: (item) => <>{item.channel?.name}<br /><small>{supportedCarriers(item).map((carrier) => carrierLabels[carrier]).join('、')}</small></> },
{ key: 'status', title: '原状态', render: (item) => <Tag tone={item.status === 'approved' ? 'success' : 'neutral'}>{statusLabel(item.status)}</Tag> },
{ key: 'actions', title: '操作', align: 'right', render: (item) => <Button icon={<Check size={15} />} onClick={() => openLegacy(item)} size="sm"></Button> },
];
async function saveRule() {
if (!ruleDraft) return;
setLoading(true);
@@ -188,20 +172,6 @@ export function AdminSignatureRetirementPage() {
}
async function deleteWebhook(id: string) { if (!window.confirm('确认停用该Webhook')) return; await adminApi.deleteSignatureRetirementWebhook(id); await loadData(messagePage, appliedMessageFilters); }
async function readAll() { await adminApi.readAllSignatureRetirementMessagesToday(); await loadMessages(messagePage, appliedMessageFilters); window.dispatchEvent(new Event('cmpp-retirement-count-refresh')); }
function openLegacy(task: LegacySignatureReportTask) {
setLegacyDraft(task);
// 历史通道级结果不能推导任何运营商状态;留空迫使操作人逐项依据供应商结果确认。
setLegacyResults(Object.fromEntries(supportedCarriers(task).map((carrier) => [carrier, { status: '', approvedAt: '' }])));
}
async function confirmLegacy() {
if (!legacyDraft) return;
await adminApi.confirmLegacySignatureReportTask(legacyDraft.id, {
results: supportedCarriers(legacyDraft).map((carrier) => ({ carrier, status: legacyResults[carrier]?.status ?? 'pending', approvedAt: legacyResults[carrier]?.status === 'approved' && legacyResults[carrier]?.approvedAt ? new Date(legacyResults[carrier].approvedAt).toISOString() : undefined })),
reason: '历史通道级报备按运营商人工确认',
});
setLegacyDraft(null); await loadData(messagePage, appliedMessageFilters);
}
const tenantOptions = tenants.map((item) => ({ value: item.id, label: item.name }));
const applicationOptions = applications
.filter((item) => !messageFilters.tenantId || item.tenantId === messageFilters.tenantId)
@@ -220,7 +190,6 @@ export function AdminSignatureRetirementPage() {
{ value: 'rules', label: '检测规则', content: <div className="surface"><div className="section-heading"><div><h2></h2><p className="muted"></p></div><Button icon={<Plus size={16} />} onClick={() => setRuleDraft({ ...emptyRule })}></Button></div><Table columns={ruleColumns} data={rules} emptyText="暂无规则,未配置规则的维度不会进入检测" pagination={false} rowKey="id" /></div> },
{ value: 'webhooks', label: 'Webhook', content: <div className="surface"><div className="section-heading"><div><h2> / </h2><p className="muted">退</p></div><Button icon={<Plus size={16} />} onClick={() => setWebhookOpen(true)}>Webhook</Button></div><div className="settings-list">{webhooks.map((item) => <div className="settings-list__item" key={item.id}><div><strong>{item.name}</strong><p>{item.platform === 'wecom' ? '企业微信' : '飞书'} · {item.urlMasked}</p></div><Button icon={<Trash2 size={15} />} onClick={() => void deleteWebhook(item.id)} variant="ghost"></Button></div>)}{!webhooks.length ? <p className="empty-state">Webhook</p> : null}</div></div> },
{ value: 'suppressions', label: `抑制管理(${suppressions.length}`, content: <div className="surface"><Table columns={suppressionColumns} data={suppressions} emptyText="暂无有效抑制" pagination={false} rowKey="id" /></div> },
{ value: 'legacy', label: `历史待确认(${legacyTasks.length}`, content: <div className="surface"><div className="section-heading"><div><h2></h2><p className="muted"></p></div></div><Table columns={legacyColumns} data={legacyTasks} emptyText="历史通道级报备均已确认" pagination={false} rowKey="id" /></div> },
];
return <section className="page-stack">
@@ -234,7 +203,6 @@ export function AdminSignatureRetirementPage() {
<Modal dirty={Boolean(cancelSuppressionDraft?.reason.trim())} open={Boolean(cancelSuppressionDraft)} title="取消抑制" onClose={() => { setCancelSuppressionDraft(null); setActionError(''); }} footer={<><Button onClick={() => { setCancelSuppressionDraft(null); setActionError(''); }} variant="ghost"></Button><Button disabled={loading || !cancelSuppressionDraft?.reason.trim()} onClick={() => void confirmCancelSuppression()}></Button></>}>
{cancelSuppressionDraft ? <div className="page-stack"><p className="muted"></p><Textarea label="取消原因" maxLength={500} onChange={(event) => setCancelSuppressionDraft((value) => value ? { ...value, reason: event.target.value } : value)} placeholder="请填写取消抑制原因" rows={4} value={cancelSuppressionDraft.reason} />{actionError ? <p className="form-error">{actionError}</p> : null}</div> : null}
</Modal>
<Modal open={Boolean(legacyDraft)} title="按运营商确认历史报备结果" onClose={() => setLegacyDraft(null)} footer={<><Button onClick={() => setLegacyDraft(null)} variant="ghost"></Button><Button disabled={!legacyConfirmationReady(legacyDraft, legacyResults)} onClick={() => void confirmLegacy()}></Button></>}><div className="page-stack"><p className="muted"></p>{legacyDraft ? supportedCarriers(legacyDraft).map((carrier) => <div className="surface" key={carrier}><strong>{carrierLabels[carrier]}</strong><div className="form-grid"><Select label="报备状态" onChange={(event) => setLegacyResults((value) => ({ ...value, [carrier]: { ...value[carrier], status: event.target.value, approvedAt: event.target.value === 'approved' ? value[carrier]?.approvedAt ?? '' : '' } }))} options={[{ value: '', label: '请选择' }, ...statusOptions]} value={legacyResults[carrier]?.status ?? ''} /><Input disabled={legacyResults[carrier]?.status !== 'approved'} label="通过时间" onChange={(event) => setLegacyResults((value) => ({ ...value, [carrier]: { ...value[carrier], approvedAt: event.target.value } }))} type="datetime-local" value={legacyResults[carrier]?.approvedAt ?? ''} /></div></div>) : null}</div></Modal>
</section>;
}
@@ -247,15 +215,6 @@ function RuleModal({ applications, channels, draft, loading, onChange, onClose,
function ruleToDraft(rule: SignatureRetirementRule): RuleDraft { return { ruleType: rule.ruleType, targetId: rule.targetId ?? '', enabled: rule.enabled, mobileWindowDays: String(rule.mobileWindowDays), mobileThreshold: String(rule.mobileThreshold), unicomWindowDays: String(rule.unicomWindowDays), unicomThreshold: String(rule.unicomThreshold), telecomWindowDays: String(rule.telecomWindowDays), telecomThreshold: String(rule.telecomThreshold), messageTemplate: rule.messageTemplate ?? '' }; }
function targetName(rule: SignatureRetirementRule, applications: EnterpriseApplication[], channels: AdminChannel[]) { if (!rule.targetId) return '全局默认'; return rule.ruleType === 'channel' ? channels.find((item) => item.id === rule.targetId)?.name ?? rule.targetId : applications.find((item) => item.id === rule.targetId)?.name ?? rule.targetId; }
function supportedCarriers(task: LegacySignatureReportTask) { const values = task.channel?.carriers?.length ? task.channel.carriers : task.channel?.carrier === 'all' ? [...carriers] : [task.channel?.carrier]; return values.filter((item): item is typeof carriers[number] => carriers.includes(item as typeof carriers[number])); }
function statusLabel(status: string) { return statusOptions.find((item) => item.value === status)?.label ?? status; }
function legacyConfirmationReady(task: LegacySignatureReportTask | null, results: Record<string, { status: string; approvedAt: string }>) {
return Boolean(task && supportedCarriers(task).every((carrier) => {
const result = results[carrier];
return statusOptions.some((option) => option.value === result?.status) && (result.status !== 'approved' || Boolean(result.approvedAt));
}));
}
function localDateTime(value?: string | null) { if (!value) return ''; const date = new Date(value); const offset = date.getTimezoneOffset() * 60_000; return new Date(date.getTime() - offset).toISOString().slice(0, 16); }
function formatDate(value?: string | null) { return value ? new Date(value).toLocaleDateString('zh-CN') : '-'; }
function formatDateTime(value?: string | null) { return value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-'; }
function errorMessage(error: unknown, fallback: string) { return error instanceof Error ? error.message : fallback; }
@@ -27,7 +27,7 @@ export function SignatureReportModal({ item, onClose }: { item: ClientSmsSignatu
<button className="admin-report-carrier--unicom active" type="button"><strong></strong><span><CarrierReportTag summary={item.carrierReportSummary?.unicom} /></span></button>
<button className="admin-report-carrier--telecom active" type="button"><strong></strong><span><CarrierReportTag summary={item.carrierReportSummary?.telecom} /></span></button>
</div>
<div className="page-stack">{(item.reportTargets ?? []).map((target) => <div className="surface" key={`${target.channelId}:${target.carrier}`} style={{ display: 'flex', justifyContent: 'space-between', padding: 12 }}><span>{target.channel.name}{carrierLabel(target.carrier)}{target.approvalScope === 'legacy_channel' ? ' · 历史通道级结果(待确认)' : ''}</span><CarrierReportTag summary={{ status: target.status, approved: target.status === 'approved' ? 1 : 0, total: 1 }} /></div>)}</div>
<div className="page-stack">{(item.reportTargets ?? []).map((target) => <div className="surface" key={`${target.channelId}:${target.carrier}`} style={{ display: 'flex', justifyContent: 'space-between', padding: 12 }}><span>{target.channel.name}{carrierLabel(target.carrier)}</span><CarrierReportTag summary={{ status: target.status, approved: target.status === 'approved' ? 1 : 0, total: 1 }} /></div>)}</div>
</div>
</Modal>
);
@@ -49,7 +49,7 @@ export function ChannelReportStatusModal({ item, onClose, onSaved }: { item: Cli
return <Modal footer={<><Button onClick={onClose} variant="ghost"></Button><Button disabled={!targets.length || saving} onClick={() => void save()}>{saving ? '保存中...' : '保存状态'}</Button></>} onClose={onClose} open size="xl" title="按通道修改签名报备状态">
<div className="page-stack"><div className="signature-alert"><Info size={18} /><span></span></div>
{error ? <p className="form-error">{error}</p> : null}
{targets.length ? targets.map((target) => { const key = `${target.channelId}:${target.carrier}`; return <div className="surface admin-report-target-row" key={key}><div><strong>{target.channel.name}</strong><div className="muted">{carrierLabel(target.carrier)}{target.approvalScope === 'legacy_channel' ? ' · 历史通道级结果,保存后转为运营商级确认' : ''}</div></div><Select onChange={(event) => setStatuses((current) => ({ ...current, [key]: event.target.value }))} options={reportStatusOptions} value={statuses[key] ?? target.status} /></div>; }) : <div className="empty-state"></div>}
{targets.length ? targets.map((target) => { const key = `${target.channelId}:${target.carrier}`; return <div className="surface admin-report-target-row" key={key}><div><strong>{target.channel.name}</strong><div className="muted">{carrierLabel(target.carrier)}</div></div><Select onChange={(event) => setStatuses((current) => ({ ...current, [key]: event.target.value }))} options={reportStatusOptions} value={statuses[key] ?? target.status} /></div>; }) : <div className="empty-state"></div>}
<Textarea label="修改原因" onChange={(event) => setReason(event.target.value)} placeholder="请输入运营商工单、确认依据或人工处理说明" rows={3} value={reason} />
</div>
</Modal>;