feat: harden sessions and track downstream acknowledgements

This commit is contained in:
hectorzhao
2026-07-14 14:18:43 +08:00
parent 3d37adcc9f
commit 8c03663f24
43 changed files with 1733 additions and 150 deletions
@@ -5,8 +5,20 @@ import { Breadcrumb, Button, Input, Modal, Pagination, Select, Table, Tag, type
const statusTone: Record<string, 'neutral' | 'info' | 'success' | 'warning' | 'danger'> = {
pending: 'warning',
awaiting_ack: 'info',
delivered: 'success',
failed: 'danger',
unconfirmed: 'warning',
rejected: 'danger',
};
const statusLabel: Record<string, string> = {
pending: '待首次投递',
awaiting_ack: '等待客户端确认',
delivered: '客户端已确认',
failed: '投递失败',
unconfirmed: '客户端未确认',
rejected: '客户端拒绝',
};
const deliveryTypeLabel: Record<string, string> = {
@@ -30,11 +42,17 @@ function DeliveryDetailModal({ record, onClose }: { record: DownstreamDeliveryRe
<div><span></span><strong>{record.tenant?.name ?? record.tenantId}</strong></div>
<div><span></span><strong>{record.application?.name ?? record.applicationId}</strong></div>
<div><span></span><strong>{deliveryTypeLabel[record.deliveryType] ?? record.deliveryType}</strong></div>
<div><span></span><strong>{record.status}</strong></div>
<div><span></span><strong>{statusLabel[record.status] ?? record.status}</strong></div>
<div><span> ID</span><strong>{record.messageId ?? '-'}</strong></div>
<div><span></span><strong>{record.retryCount}</strong></div>
<div><span></span><strong>{record.nextRetryAt ?? '-'}</strong></div>
<div><span></span><strong>{record.deliveredAt ?? '-'}</strong></div>
<div><span></span><strong>{record.retryEnabled ? '开启' : '关闭'}</strong></div>
<div><span></span><strong>{record.sentAt ?? '-'}</strong></div>
<div><span></span><strong>{record.acknowledgedAt ?? '-'}</strong></div>
<div><span>ACK Result</span><strong>{record.ackResult ?? '-'}</strong></div>
<div><span>ACK Sequence_Id</span><strong>{record.ackSequenceId ?? '-'}</strong></div>
<div><span>ACK Msg_Id</span><strong>{record.ackMessageId ?? '-'}</strong></div>
<div><span> ID</span><strong>{record.connectionId ?? '-'}</strong></div>
<div className="detail-grid__wide"><span></span><strong>{record.lastError ?? '-'}</strong></div>
</div>
<section className="report-history">
@@ -96,7 +114,7 @@ export function AdminDownstreamDeliveriesPage() {
}, [loadData]);
const selectableIds = useMemo(
() => records.filter((item) => item.status !== 'delivered').map((item) => item.id),
() => records.filter((item) => ['pending', 'failed', 'unconfirmed', 'rejected'].includes(item.status)).map((item) => item.id),
[records],
);
const allSelected = selectableIds.length > 0 && selectableIds.every((id) => selectedIds.includes(id));
@@ -114,7 +132,7 @@ export function AdminDownstreamDeliveriesPage() {
render: (record) => (
<input
type="checkbox"
disabled={record.status === 'delivered'}
disabled={!['pending', 'failed', 'unconfirmed', 'rejected'].includes(record.status)}
checked={selectedIds.includes(record.id)}
onChange={(event) => {
setSelectedIds((current) =>
@@ -132,7 +150,7 @@ export function AdminDownstreamDeliveriesPage() {
{ key: 'application', title: '应用', width: '180px', render: (record) => record.application?.name ?? '-' },
{ key: 'type', title: '类型', width: '110px', render: (record) => deliveryTypeLabel[record.deliveryType] ?? record.deliveryType },
{ key: 'messageId', title: '消息 ID', width: '180px', render: (record) => <strong className="admin-task-id">{record.messageId ?? '-'}</strong> },
{ key: 'status', title: '状态', width: '110px', render: (record) => <Tag tone={statusTone[record.status] ?? 'info'}>{record.status}</Tag> },
{ key: 'status', title: '状态', width: '150px', render: (record) => <Tag tone={statusTone[record.status] ?? 'info'}>{statusLabel[record.status] ?? record.status}</Tag> },
{ key: 'retry', title: '重试', width: '90px', align: 'center', render: (record) => record.retryCount },
{ key: 'error', title: '最后错误', render: (record) => record.lastError ?? '-' },
{
@@ -144,8 +162,10 @@ export function AdminDownstreamDeliveriesPage() {
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<Button icon={<Eye size={14} />} onClick={() => setDetail(record)} size="sm" variant="ghost"></Button>
<Button
disabled={!['pending', 'failed', 'unconfirmed', 'rejected'].includes(record.status)}
icon={<RefreshCw size={14} />}
onClick={() => {
if (!window.confirm('重投可能导致下游业务重复处理,确认继续吗?')) return;
adminApi.requeueDownstreamDelivery(record.id)
.then(() => loadData())
.catch((failure: Error) => setError(failure.message || '人工重投失败'));
@@ -192,9 +212,9 @@ export function AdminDownstreamDeliveriesPage() {
<div className="surface mini-status-card">
<CheckCircle2 size={22} />
<div>
<span></span>
<span></span>
<strong>{summary?.delivered ?? 0}</strong>
<small></small>
<small> Result=0 CMPP_DELIVER_RESP</small>
</div>
</div>
<div className="surface mini-status-card">
@@ -214,7 +234,10 @@ export function AdminDownstreamDeliveriesPage() {
options={[
{ label: '全部状态', value: 'all' },
{ label: '待投递', value: 'pending' },
{ label: '已投递', value: 'delivered' },
{ label: '等待客户端确认', value: 'awaiting_ack' },
{ label: '客户端已确认', value: 'delivered' },
{ label: '客户端未确认', value: 'unconfirmed' },
{ label: '客户端拒绝', value: 'rejected' },
{ label: '最终失败', value: 'failed' },
]}
value={status}
@@ -271,19 +294,23 @@ export function AdminDownstreamDeliveriesPage() {
<h2></h2>
</div>
<div className="downstream-breakdown-table">
<div className="downstream-breakdown-table__head">
<div className="downstream-breakdown-table__head downstream-breakdown-table__head--ack">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span>/</span>
<span></span>
</div>
{typeBreakdown.map((item) => (
<div className="downstream-breakdown-table__row" key={item.deliveryType}>
<div className="downstream-breakdown-table__row downstream-breakdown-table__row--ack" key={item.deliveryType}>
<strong>{deliveryTypeLabel[item.deliveryType] ?? item.deliveryType}</strong>
<span>{item.total}</span>
<span>{item.pending}</span>
<span>{item.awaitingAck}</span>
<span>{item.delivered}</span>
<span>{item.unconfirmed + item.rejected}</span>
<span>{item.failed}</span>
</div>
))}
@@ -312,7 +339,7 @@ export function AdminDownstreamDeliveriesPage() {
<div className="downstream-breakdown-table__head downstream-breakdown-table__head--apps">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
@@ -320,7 +347,7 @@ export function AdminDownstreamDeliveriesPage() {
<div className="downstream-breakdown-table__row downstream-breakdown-table__row--apps" key={item.applicationId}>
<strong>{item.name}</strong>
<span>{item.pending}</span>
<span>{item.failed}</span>
<span>{item.failed + item.unconfirmed + item.rejected}</span>
<span>{item.delivered}</span>
<span>{item.alertCount}</span>
</div>
@@ -333,7 +360,7 @@ export function AdminDownstreamDeliveriesPage() {
<div className="surface admin-task-table-card report-task-table-card">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, marginBottom: 16 }}>
<p style={{ margin: 0, color: 'var(--text-secondary)' }}>
{selectedIds.length} `pending/failed`
{selectedIds.length}
</p>
<div style={{ display: 'flex', gap: 8 }}>
<Button
@@ -347,6 +374,7 @@ export function AdminDownstreamDeliveriesPage() {
icon={<RefreshCw size={14} />}
disabled={selectedIds.length === 0}
onClick={() => {
if (!window.confirm(`即将重投 ${selectedIds.length} 条记录,可能导致下游业务重复处理,确认继续吗?`)) return;
adminApi.batchRequeueDownstreamDeliveries(selectedIds)
.then((result) => {
setError(result.failedCount > 0 ? `批量重投完成,成功 ${result.successCount} 条,失败 ${result.failedCount}` : '');