feat: remove legacy signature confirmation
This commit is contained in:
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user