import { useEffect, useState } from 'react'; import { Search, Smartphone, UserX } from 'lucide-react'; import { adminApi, type SmsMessageRecord, type SmsUplinkMatchCandidate, type SmsUplinkMessage } from '@/api/adminApi'; import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Table, type DateRangeValue, type TableColumn, } from '@/components/ui'; function getDate(value?: string | null) { return value ? value.slice(0, 10) : ''; } function getTime(value?: string | null) { return value ? `${value.slice(0, 10)} ${value.slice(11, 19)}` : '-'; } function matchStatusText(status?: string | null) { const map: Record = { matched: '已匹配', ambiguous: '待认领', unmatched: '未匹配', }; return status ? (map[status] ?? status) : '-'; } function candidateStatusText(status?: string | null) { const map: Record = { pending: '待认领', claimed: '已认领', rejected: '已排除', }; return status ? (map[status] ?? status) : '-'; } function UplinkDetailModal({ blacklistFeedback, blacklisting, claimError, claimingId, detailError, matchedRecords, matching, message, onAddBlacklist, onClaim, onClose, }: { blacklistFeedback: string; blacklisting: boolean; claimError: string; claimingId: string; detailError: string; matchedRecords: SmsMessageRecord[]; matching: boolean; message: SmsUplinkMessage; onAddBlacklist: () => void; onClaim: (candidate: SmsUplinkMatchCandidate) => void; onClose: () => void; }) { const candidates = message.matchCandidates ?? []; return ( {message.tenantId && message.applicationId ? ( ) : null} } onClose={onClose} open size="xl" title="上行短信详情" >

上行信息

手机号码 {message.phoneNumber || '-'}
上行时间 {getTime(message.receivedAt)}
上行企业 {message.tenant?.name ?? message.tenantId ?? '-'}
上行通道 {message.channel?.name ?? message.channelId ?? '-'}
上行接入号 {message.destId || '-'}
上行网关消息ID {message.gatewayMessageId || '-'}
关联平台消息ID {message.messageRecord?.messageId ?? message.messageId ?? '-'}
匹配状态 {matchStatusText(message.matchStatus)}
匹配说明 {message.matchReason || '-'}
上行内容 {message.content || '-'}

候选认领

{claimError ?

{claimError}

: null} {candidates.length === 0 ?
暂无人工认领候选
: null} {candidates.map((candidate) => (
候选企业 {candidate.tenant?.name ?? candidate.tenantId}
候选应用 {candidate.application?.name ?? candidate.applicationId}
候选来源 {candidate.matchSource === 'access_number' ? '接入号' : '手机号时间窗'}
置信度 {candidate.confidence}
状态 {candidateStatusText(candidate.status)}
下发短信 {candidate.messageRecord?.messageId ?? '-'}
{candidate.messageRecord ? (
下发内容

{candidate.messageRecord.content}

) : null}
{candidate.reason ?? '-'}
))}

匹配发送记录

{blacklistFeedback ?

{blacklistFeedback}

: null} {matching ?

正在查询真实下发记录...

: null} {detailError ?

{detailError}

: null} {!matching && matchedRecords.length === 0 && !detailError ? (
{message.matchStatus === 'ambiguous' ? '存在多个候选,请先认领正确应用' : '暂无匹配发送记录'}
) : null} {matchedRecords.map((record) => (
发送时间 {getTime(record.queuedAt)}
发送企业 {record.tenant?.name ?? record.tenantId ?? '-'}
发送应用 {record.application?.name ?? record.applicationId ?? '-'}
客户提交接入号 {record.channel?.srcId ?? '-'}
下发内容

{record.content}

))}
); } export function AdminSmsUplinkRecordsPage() { const [messages, setMessages] = useState([]); const [matchedRecords, setMatchedRecords] = useState([]); const [dateRange, setDateRange] = useState({}); const [phoneKeyword, setPhoneKeyword] = useState(''); const [contentKeyword, setContentKeyword] = useState(''); const [selectedMessage, setSelectedMessage] = useState(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(''); const [blacklisting, setBlacklisting] = useState(false); const [blacklistFeedback, setBlacklistFeedback] = useState(''); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const pageSize = 10; function loadData(targetPage = page, filters = { phoneKeyword, contentKeyword, dateRange }) { setLoading(true); adminApi.listAdminUplinkMessagesPage({ phoneNumber: filters.phoneKeyword.trim() || undefined, keyword: filters.contentKeyword.trim() || undefined, startTime: filters.dateRange.start ? `${filters.dateRange.start}T00:00:00+08:00` : undefined, endTime: filters.dateRange.end ? `${filters.dateRange.end}T23:59:59.999+08:00` : undefined, page: targetPage, pageSize, }) .then((result) => { setMessages(result.items); setTotal(result.total); setError(''); }) .catch((reason: Error) => setError(reason.message || '短信上行记录加载失败')) .finally(() => setLoading(false)); } function openDetail(message: SmsUplinkMessage) { setSelectedMessage(message); setMatchedRecords(message.messageRecord ? [message.messageRecord] : []); setDetailError(''); setClaimError(''); setBlacklistFeedback(''); if (message.messageRecord || !message.messageId) { return; } setMatching(true); adminApi.listOperationMessages({ tenantId: message.tenantId ?? undefined, messageId: message.messageId, page: 1, pageSize: 10 }) .then((result) => setMatchedRecords(result.items)) .catch((reason: Error) => setDetailError(reason.message || '匹配发送记录加载失败')) .finally(() => setMatching(false)); } function handleAddBlacklist() { if (!selectedMessage?.tenantId || !selectedMessage.applicationId) { setBlacklistFeedback('请先匹配或认领企业应用'); return; } if (!window.confirm(`确认将 ${selectedMessage.phoneNumber} 加入当前应用黑名单?`)) { return; } setBlacklisting(true); setBlacklistFeedback(''); adminApi.createEnterpriseBlacklist({ tenantId: selectedMessage.tenantId, applicationId: selectedMessage.applicationId, phoneNumber: selectedMessage.phoneNumber, reason: '上行短信人工加入', status: 'active', }) .then(() => setBlacklistFeedback('已加入当前应用黑名单')) .catch((reason: Error) => setBlacklistFeedback(reason.message || '加入应用黑名单失败')) .finally(() => setBlacklisting(false)); } useEffect(() => { loadData(page); }, [page]); function resetFilters() { setDateRange({}); setPhoneKeyword(''); setContentKeyword(''); setPage(1); loadData(1, { phoneKeyword: '', contentKeyword: '', dateRange: {} }); } 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)); setMatchedRecords(updated.messageRecord ? [updated.messageRecord] : []); loadData(page); }) .catch((reason: Error) => setClaimError(reason.message || '上行认领失败')) .finally(() => setClaimingId('')); } const columns: Array> = [ { key: 'select', title: '', width: '48px', align: 'center', render: () => , }, { key: 'phoneNumber', title: '手机号码', width: '120px', render: (record) => {record.phoneNumber} }, { key: 'receivedAt', title: '上行时间', width: '156px', render: (record) => {getTime(record.receivedAt)} }, { key: 'content', title: '上行内容', width: '260px', render: (record) => {record.content} }, { key: 'channel', title: '上行通道', width: '160px', render: (record) => {record.channel?.name ?? record.channelId} }, { key: 'accessNo', title: '上行接入号', width: '120px', render: (record) => {record.destId} }, { key: 'matchStatus', title: '匹配状态', width: '100px', render: (record) => {matchStatusText(record.matchStatus)} }, { key: 'actions', title: '操作', width: '88px', align: 'center', render: (record) => ( ), }, ]; return (

短信上行记录

setPhoneKeyword(event.target.value)} prefix={} value={phoneKeyword} /> setContentKeyword(event.target.value)} value={contentKeyword} />
{error ?

{error}

: null}
setPage((value) => Math.max(1, value - 1))} onNext={() => setPage((value) => Math.min(Math.max(1, Math.ceil(total / pageSize)), value + 1))} previousDisabled={page <= 1} nextDisabled={page >= Math.max(1, Math.ceil(total / pageSize))} /> {selectedMessage ? ( setSelectedMessage(null)} /> ) : null} ); }