import { useCallback, useEffect, useMemo, useState } from 'react'; import { AlertTriangle, CheckCircle2, Clock3, Eye, RefreshCw, Search } from 'lucide-react'; import { adminApi, type AdminChannel, type EnterpriseApplication, type ReceiptAnomaly } from '@/api/adminApi'; import { Button, Input, Modal, Pagination, Select, Table, Tag, type TableColumn } from '@/components/ui'; const anomalyTypeLabel: Record = { aggregate_success_then_failure: '整条成功后又收到失败', }; const statusLabel: Record = { pending: '待处理', resolved: '已处理', ignored: '已忽略', }; const statusTone: Record = { pending: 'danger', resolved: 'success', ignored: 'neutral', }; function formatTime(value?: string | null) { if (!value) return '-'; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN', { hour12: false }); } function maskPhone(value?: string | null) { if (!value) return '-'; return value.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2'); } function ReceiptAnomalyDetailModal({ record, onClose }: { record: ReceiptAnomaly; onClose: () => void }) { return ( 关闭} onClose={onClose} open size="xl" title={

回执异常详情

{record.messageRecord?.messageId ?? record.anomalyKey}

} >
异常类型{anomalyTypeLabel[record.anomalyType] ?? record.anomalyType}
处理状态{statusLabel[record.status] ?? record.status}
企业{record.tenant?.name ?? '-'}
应用{record.application?.name ?? '-'}
通道{record.channel?.name ?? '-'}
手机号{maskPhone(record.messageRecord?.phoneNumber)}
MessageId{record.messageRecord?.messageId ?? '-'}
SubmitId{record.submitRecord?.submitId ?? '-'}
原平台终态{record.previousStatus ?? '-'}
新回执状态{record.incomingStatus ?? '-'}
供应商原始状态{record.rawStatus ?? '-'}
错误码{record.errorCode ?? '-'}
首次发生{formatTime(record.firstOccurredAt)}
最近发生{formatTime(record.lastOccurredAt)}
重复次数{record.occurrenceCount}
原始回执 Msg_Id{record.receiptRecord?.gatewayMessageId ?? '-'}
异常唯一键{record.anomalyKey}
判定依据平台已根据整条级成功回执形成终态,同一提交尝试后续又收到明确失败回执。平台保留成功终态,不自动改写已推送给客户的结果。

结构化判定信息

供应商原始报文请到“系统日志 → 通讯交互日志”按 MessageId 或 Msg_Id 追溯。

{JSON.stringify(record.detail ?? {}, null, 2)}
); } export function ReceiptAnomalyPanel() { const [items, setItems] = useState([]); const [applications, setApplications] = useState([]); const [channels, setChannels] = useState([]); const [summary, setSummary] = useState({ pending: 0, resolved: 0, ignored: 0, oldestPendingAt: null as string | null }); const [keyword, setKeyword] = useState(''); const [status, setStatus] = useState('all'); const [anomalyType, setAnomalyType] = useState('all'); const [applicationId, setApplicationId] = useState('all'); const [channelId, setChannelId] = useState('all'); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [detail, setDetail] = useState(null); const pageSize = 10; useEffect(() => { Promise.all([adminApi.listEnterpriseApplications(), adminApi.listChannels()]) .then(([appItems, channelItems]) => { setApplications(appItems); setChannels(channelItems.filter((item) => item.status !== 'deleted')); }) .catch((failure: Error) => setError(failure.message || '筛选条件加载失败')); }, []); const loadData = useCallback(() => { setLoading(true); adminApi.listReceiptAnomalies({ keyword, status, anomalyType, applicationId, channelId, page, pageSize }) .then((response) => { setItems(response.items); setTotal(response.total); setSummary({ ...response.summary, oldestPendingAt: response.summary.oldestPendingAt ?? null }); setError(''); }) .catch((failure: Error) => { setItems([]); setTotal(0); setError(failure.message || '回执异常加载失败'); }) .finally(() => setLoading(false)); }, [anomalyType, applicationId, channelId, keyword, page, status]); useEffect(() => { loadData(); }, [loadData]); const columns = useMemo>>(() => [ { key: 'lastOccurredAt', title: '最近发生', width: '170px', render: (record) => formatTime(record.lastOccurredAt) }, { key: 'messageId', title: '消息编号', width: '180px', render: (record) => {record.messageRecord?.messageId ?? '-'} }, { key: 'tenant', title: '企业 / 应用', width: '190px', render: (record) =>
{record.tenant?.name ?? '-'}{record.application?.name ?? '-'}
}, { key: 'channel', title: '通道', width: '170px', render: (record) =>
{record.channel?.name ?? '-'}{record.channel?.code ?? '-'}
}, { key: 'type', title: '异常类型', render: (record) =>
{anomalyTypeLabel[record.anomalyType] ?? record.anomalyType}{record.previousStatus ?? '-'} → {record.incomingStatus ?? '-'}
}, { key: 'count', title: '次数', width: '70px', align: 'center', render: (record) => record.occurrenceCount }, { key: 'status', title: '状态', width: '100px', render: (record) => {statusLabel[record.status] ?? record.status} }, { key: 'actions', title: '操作', width: '90px', align: 'right', render: (record) => }, ], []); const totalPages = Math.max(1, Math.ceil(total / pageSize)); return (

回执异常

展示供应商回执与平台已确定终态互相矛盾的数据,例如整条级成功后又收到同一提交尝试的失败回执。此处保存异常判定和处理状态;真实回执在回执表,原始 CMPP 报文在通讯交互日志。

{error ?

{error}

: null}
待处理{summary.pending}需要核对供应商回执口径。
最早待处理{formatTime(summary.oldestPendingAt)}长时间未核对应优先处理。
已处理{summary.resolved}已完成供应商或业务核对。
异常总数{total}当前筛选条件下的记录。
{ setKeyword(event.target.value); setPage(1); }} placeholder="MessageId、SubmitId、原始状态" value={keyword} /> { setAnomalyType(event.target.value); setPage(1); }} /> ({ label: item.name, value: item.id }))]} value={channelId} onChange={(event) => { setChannelId(event.target.value); setPage(1); }} />

回执异常记录

详情仅展示结构化判定信息,不用它替代原始通讯报文。

{total} 条
= totalPages} onPrevious={() => setPage((current) => Math.max(1, current - 1))} onNext={() => setPage((current) => Math.min(totalPages, current + 1))} /> {detail ? setDetail(null)} /> : null} ); }