fix: align daily operations statistics

This commit is contained in:
hectorzhao
2026-07-24 22:08:06 +08:00
parent c78ed1cc32
commit 53736c96e8
9 changed files with 433 additions and 77 deletions
+57 -10
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react';
import { AlertTriangle, Download, MessageSquare, Search, Smartphone } from 'lucide-react';
import { AlertTriangle, Download, Info, MessageSquare, Search, Smartphone } from 'lucide-react';
import { adminApi, type SmsMessageRecord, type SmsMessageSegmentAudit, type SmsReceiptRecord, type SmsSubmitRecord } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Input, Modal, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
import { formatCents } from '@/utils/currency';
@@ -8,8 +8,9 @@ const statusLabelMap: Record<string, string> = {
delivered: '发送成功',
queued: '排队中',
submitted: '已提交',
submit_failed: '提交失败',
unknown: '未知',
failed: '失败',
failed: '送达失败',
rejected: '已拒绝',
};
@@ -17,6 +18,7 @@ const statusToneMap: Record<string, 'success' | 'neutral' | 'danger' | 'info'> =
delivered: 'success',
queued: 'info',
submitted: 'info',
submit_failed: 'danger',
unknown: 'neutral',
failed: 'danger',
rejected: 'danger',
@@ -26,6 +28,7 @@ const statusDotClassMap: Record<string, string> = {
delivered: 'is-success',
queued: 'is-unknown',
submitted: 'is-unknown',
submit_failed: 'is-failed',
unknown: 'is-unknown',
failed: 'is-failed',
rejected: 'is-failed',
@@ -85,6 +88,36 @@ function getStatusLabel(status?: string | null) {
return status ? (statusLabelMap[status] ?? status) : '-';
}
function isSubmitFailure(record: SmsMessageRecord) {
return record.status === 'submit_failed' || ['rejected', 'timeout'].includes(record.submitStatus ?? '');
}
function getRecordStatus(record: SmsMessageRecord) {
return isSubmitFailure(record) ? 'submit_failed' : record.status;
}
function getRecordStatusLabel(record: SmsMessageRecord) {
return getStatusLabel(getRecordStatus(record));
}
function getReceiptNotice(record: SmsMessageRecord) {
const hasPlatformFailureReceipt = (record.receiptRecords ?? []).some((receipt) =>
receipt.gatewayMessageId.startsWith('PLATFORM:') && receipt.rawStatus === 'REJECTD',
);
if (hasPlatformFailureReceipt) {
const deliveries = (record.downstreamDeliveries ?? []).filter((item) => item.deliveryType === 'receipt');
if (deliveries.some((item) => item.status === 'delivered')) {
return '平台已生成失败回执并通知企业';
}
const deliveryStatuses = Array.from(new Set(deliveries.map((item) => item.status)));
return `平台已生成失败回执,企业通知状态:${deliveryStatuses.join('、') || '待投递'}`;
}
if (!record.tenantId && !record.applicationId && record.messageId.startsWith('MSG-TEST-')) {
return '运营端通道测试,无需生成客户回执';
}
return null;
}
function getCarrierLabel(carrier?: string | null) {
return carrier ? (carrierLabelMap[carrier] ?? carrier) : '-';
}
@@ -121,7 +154,8 @@ function buildRouteRows(record: SmsMessageRecord): RouteRow[] {
}];
}
function StatusLine({ status }: { status: string }) {
function StatusLine({ record }: { record: SmsMessageRecord }) {
const status = getRecordStatus(record);
return (
<span className="admin-sms-record-status">
<i className={statusDotClassMap[status] ?? 'is-unknown'} />
@@ -149,7 +183,7 @@ function downloadCsv(records: SmsMessageRecord[]) {
record.billingUnits,
formatCents(record.amountCents),
record.channel?.name ?? record.channelId ?? '',
getStatusLabel(record.status),
getRecordStatusLabel(record),
getTime(record.deliveredAt),
record.content,
]),
@@ -176,6 +210,8 @@ function SendDetailModal({
}) {
const routeRows = buildRouteRows(record);
const sentAccessNumber = `${record.channel?.srcId ?? ''}${record.applicationExtension ?? ''}`;
const displayStatus = getRecordStatus(record);
const receiptNotice = getReceiptNotice(record);
return (
<Modal
footer={<Button onClick={onClose} variant="ghost"></Button>}
@@ -188,7 +224,7 @@ function SendDetailModal({
<div className="admin-sms-detail-overview">
<div>
<span></span>
<Tag tone={record.status === 'delivered' ? 'success' : ['failed', 'rejected'].includes(record.status) ? 'danger' : 'info'}>{getStatusLabel(record.status)}</Tag>
<Tag tone={statusToneMap[displayStatus] ?? 'info'}>{getRecordStatusLabel(record)}</Tag>
</div>
<div>
<span></span>
@@ -219,6 +255,12 @@ function SendDetailModal({
<strong>{sentAccessNumber || '-'}</strong>
</div>
</div>
{receiptNotice ? (
<div className="admin-sms-detail-notice" role="status">
<Info size={20} />
<strong>{receiptNotice}</strong>
</div>
) : null}
<section>
<h3><MessageSquare size={18} /> </h3>
<p className="admin-sms-detail-content">{record.content}</p>
@@ -260,7 +302,7 @@ function SendDetailModal({
<h3></h3>
<div className="admin-sms-detail-status-grid">
<div><span></span><strong>{record.messageId}</strong></div>
<div><span></span><strong>{getStatusLabel(record.status)}</strong></div>
<div><span></span><strong>{getRecordStatusLabel(record)}</strong></div>
<div><span></span><strong>{record.submitStatus ?? '-'}</strong></div>
<div><span></span><strong>{record.receiptStatus ?? '-'}</strong></div>
</div>
@@ -377,7 +419,11 @@ export function AdminSmsRecordsPage() {
const enterpriseOptions = useMemo(() => {
const tenants = new Map<string, string>();
records.forEach((record) => tenants.set(record.tenantId, record.tenant?.name ?? record.tenantId));
records.forEach((record) => {
if (record.tenantId) {
tenants.set(record.tenantId, record.tenant?.name ?? record.tenantId);
}
});
return [{ label: '全部企业', value: 'all' }, ...Array.from(tenants, ([value, label]) => ({ label, value }))];
}, [records]);
@@ -441,7 +487,8 @@ export function AdminSmsRecordsPage() {
{ label: '全部', value: 'all' },
{ label: '发送成功', value: 'delivered' },
{ label: '未知', value: 'unknown' },
{ label: '失败', value: 'failed' },
{ label: '提交失败', value: 'submit_failed' },
{ label: '送达失败', value: 'failed' },
]}
value={status}
/>
@@ -460,10 +507,10 @@ export function AdminSmsRecordsPage() {
<article className="admin-sms-record-card" key={record.id}>
<header>
<div className="admin-sms-record-sender">
<strong>{record.tenant?.name ?? record.tenantId}</strong>
<strong>{record.tenant?.name ?? record.tenantId ?? '运营端通道测试'}</strong>
<span>{record.application?.name ?? record.applicationId ?? '-'}</span>
</div>
<StatusLine status={record.status} />
<StatusLine record={record} />
<time>{getDate(record.queuedAt)} {getClock(record.queuedAt)}</time>
</header>
<p className="admin-sms-record-content">{record.content}</p>