feat: complete cmpp gateway delivery recovery workflows

This commit is contained in:
hectorzhao
2026-07-08 16:30:06 +08:00
parent cc628d0214
commit 8144f08652
60 changed files with 8901 additions and 94 deletions
+105 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { Search, Smartphone } from 'lucide-react';
import { adminApi, type SmsMessageRecord, type SmsUplinkMessage } from '@/api/adminApi';
import { adminApi, type SmsMessageRecord, type SmsUplinkMatchCandidate, type SmsUplinkMessage } from '@/api/adminApi';
import {
Breadcrumb,
Button,
@@ -20,19 +20,44 @@ function getTime(value?: string | null) {
return value ? `${value.slice(0, 10)} ${value.slice(11, 19)}` : '-';
}
function matchStatusText(status?: string | null) {
const map: Record<string, string> = {
matched: '已匹配',
ambiguous: '待认领',
unmatched: '未匹配',
};
return status ? (map[status] ?? status) : '-';
}
function candidateStatusText(status?: string | null) {
const map: Record<string, string> = {
pending: '待认领',
claimed: '已认领',
rejected: '已排除',
};
return status ? (map[status] ?? status) : '-';
}
function UplinkDetailModal({
claimError,
claimingId,
detailError,
matchedRecords,
matching,
message,
onClaim,
onClose,
}: {
claimError: string;
claimingId: string;
detailError: string;
matchedRecords: SmsMessageRecord[];
matching: boolean;
message: SmsUplinkMessage;
onClaim: (candidate: SmsUplinkMatchCandidate) => void;
onClose: () => void;
}) {
const candidates = message.matchCandidates ?? [];
return (
<Modal
footer={<Button onClick={onClose} variant="ghost"></Button>}
@@ -69,6 +94,10 @@ function UplinkDetailModal({
<span>ID</span>
<strong>{message.messageId || '-'}</strong>
</div>
<div>
<span></span>
<strong>{matchStatusText(message.matchStatus)}</strong>
</div>
<div className="admin-uplink-info-grid__full">
<span></span>
<strong>{message.content || '-'}</strong>
@@ -76,6 +105,58 @@ function UplinkDetailModal({
</div>
</section>
<section className="admin-uplink-match-section">
<h3></h3>
{claimError ? <p className="form-error">{claimError}</p> : null}
{candidates.length === 0 ? <div className="admin-uplink-empty-match"></div> : null}
{candidates.map((candidate) => (
<article className="admin-uplink-match-card" key={candidate.id}>
<div className="admin-uplink-match-grid">
<div>
<span></span>
<strong>{candidate.tenant?.name ?? candidate.tenantId}</strong>
</div>
<div>
<span></span>
<strong>{candidate.application?.name ?? candidate.applicationId}</strong>
</div>
<div>
<span></span>
<strong>{candidate.matchSource === 'access_number' ? '接入号' : '手机号时间窗'}</strong>
</div>
<div>
<span></span>
<strong>{candidate.confidence}</strong>
</div>
<div>
<span></span>
<strong>{candidateStatusText(candidate.status)}</strong>
</div>
<div>
<span></span>
<strong>{candidate.messageRecord?.messageId ?? '-'}</strong>
</div>
</div>
{candidate.messageRecord ? (
<div className="admin-uplink-match-content">
<span></span>
<p>{candidate.messageRecord.content}</p>
</div>
) : null}
<div className="admin-uplink-candidate-footer">
<span>{candidate.reason ?? '-'}</span>
<Button
disabled={claimingId === candidate.id || candidate.status === 'claimed' || candidate.status === 'rejected'}
onClick={() => onClaim(candidate)}
size="sm"
>
{candidate.status === 'claimed' ? '已认领' : claimingId === candidate.id ? '认领中...' : '认领并推送'}
</Button>
</div>
</article>
))}
</section>
<section className="admin-uplink-match-section">
<h3></h3>
{matching ? <p>...</p> : null}
@@ -125,8 +206,10 @@ export function AdminSmsUplinkRecordsPage() {
const [selectedMessage, setSelectedMessage] = useState<SmsUplinkMessage | null>(null);
const [loading, setLoading] = useState(true);
const [matching, setMatching] = useState(false);
const [claimingId, setClaimingId] = useState('');
const [error, setError] = useState('');
const [detailError, setDetailError] = useState('');
const [claimError, setClaimError] = useState('');
function loadData() {
setLoading(true);
@@ -143,6 +226,7 @@ export function AdminSmsUplinkRecordsPage() {
setSelectedMessage(message);
setMatchedRecords([]);
setDetailError('');
setClaimError('');
if (!message.messageId) {
return;
@@ -177,6 +261,22 @@ export function AdminSmsUplinkRecordsPage() {
setContentKeyword('');
}
function handleClaim(candidate: SmsUplinkMatchCandidate) {
if (!selectedMessage) {
return;
}
setClaimingId(candidate.id);
setClaimError('');
adminApi.claimUplinkMatchCandidate(selectedMessage.id, { candidateId: candidate.id })
.then((updated) => {
setMessages((items) => items.map((item) => (item.id === updated.id ? { ...item, ...updated } : item)));
setSelectedMessage((current) => (current && current.id === updated.id ? { ...current, ...updated } : current));
loadData();
})
.catch((reason: Error) => setClaimError(reason.message || '上行认领失败'))
.finally(() => setClaimingId(''));
}
const columns: Array<TableColumn<SmsUplinkMessage>> = [
{
key: 'select',
@@ -190,6 +290,7 @@ export function AdminSmsUplinkRecordsPage() {
{ key: 'content', title: '上行内容', render: (record) => <span className="uplink-content">{record.content}</span> },
{ key: 'channel', title: '上行通道', width: '260px', render: (record) => <strong>{record.channel?.name ?? record.channelId}</strong> },
{ key: 'accessNo', title: '上行接入号', width: '180px', render: (record) => <strong>{record.destId}</strong> },
{ key: 'matchStatus', title: '匹配状态', width: '140px', render: (record) => <strong>{matchStatusText(record.matchStatus)}</strong> },
{
key: 'actions',
title: '操作',
@@ -228,10 +329,13 @@ export function AdminSmsUplinkRecordsPage() {
{selectedMessage ? (
<UplinkDetailModal
claimError={claimError}
claimingId={claimingId}
detailError={detailError}
matchedRecords={matchedRecords}
matching={matching}
message={selectedMessage}
onClaim={handleClaim}
onClose={() => setSelectedMessage(null)}
/>
) : null}