fix: close receipt delivery workflows

This commit is contained in:
hectorzhao
2026-07-24 14:40:57 +08:00
parent 2ee39056eb
commit 91f04f5288
22 changed files with 829 additions and 48 deletions
+26 -5
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { Plus, Search } from 'lucide-react';
import { Breadcrumb, Button, DateRangeInput, Input, ManualRechargeDialog, Pagination, Tag, type DateRangeValue } from '@/components/ui';
import { Plus, ReceiptText, Search } from 'lucide-react';
import { Breadcrumb, Button, DateRangeInput, Input, ManualRechargeDialog, Pagination, RechargeReceiptDialog, Tag, type DateRangeValue } from '@/components/ui';
import { adminApi, type RechargeOrder, type TenantAccount, type TenantOption } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { formatCents } from '@/utils/currency';
@@ -24,6 +24,7 @@ export function AdminRechargeRecordsPage() {
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
const [dateRange, setDateRange] = useState<DateRangeValue>({});
const [manualOpen, setManualOpen] = useState(false);
const [receiptRecord, setReceiptRecord] = useState<RechargeOrder | null>(null);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
@@ -67,6 +68,9 @@ export function AdminRechargeRecordsPage() {
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleRows = filteredRows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
const receiptTenant = receiptRecord
? receiptRecord.tenant ?? tenants.find((tenant) => tenant.id === receiptRecord.tenantId)
: undefined;
useEffect(() => {
setPage(1);
@@ -107,15 +111,16 @@ export function AdminRechargeRecordsPage() {
<th style={{ width: '140px' }}></th>
<th style={{ width: '120px' }}></th>
<th style={{ width: '300px' }}></th>
<th style={{ width: '130px' }}></th>
</tr>
</thead>
<tbody>
{error ? (
<tr><td className="ui-table__empty" colSpan={6}>{error}</td></tr>
<tr><td className="ui-table__empty" colSpan={7}>{error}</td></tr>
) : loading ? (
<tr><td className="ui-table__empty" colSpan={6}>...</td></tr>
<tr><td className="ui-table__empty" colSpan={7}>...</td></tr>
) : filteredRows.length === 0 ? (
<tr><td className="ui-table__empty" colSpan={6}></td></tr>
<tr><td className="ui-table__empty" colSpan={7}></td></tr>
) : visibleRows.map((record) => {
const tenantName = record.tenant?.name ?? tenants.find((tenant) => tenant.id === record.tenantId)?.name ?? record.tenantId;
return (
@@ -126,6 +131,16 @@ export function AdminRechargeRecordsPage() {
<td>{record.balanceAfterCents === null || record.balanceAfterCents === undefined ? '-' : `¥${formatCents(record.balanceAfterCents)}`}</td>
<td><Tag tone="warning"></Tag></td>
<td><RemarkCell value={record.remark ?? undefined} /></td>
<td>
<Button
icon={<ReceiptText size={15} />}
onClick={() => setReceiptRecord(record)}
size="sm"
variant="ghost"
>
</Button>
</td>
</tr>
);
})}
@@ -157,6 +172,12 @@ export function AdminRechargeRecordsPage() {
balanceCents: accounts.find((account) => account.tenantId === tenant.id)?.balanceCents ?? 0,
}))}
/>
<RechargeReceiptDialog
onClose={() => setReceiptRecord(null)}
open={Boolean(receiptRecord)}
record={receiptRecord}
tenant={receiptTenant}
/>
</section>
);
}
+32 -11
View File
@@ -15,13 +15,6 @@ const carrierMeta: Record<Carrier, { label: string; description: string }> = {
telecom: { label: '电信', description: '电信号码只会进入电信通道组' },
};
const deliveryModeOptions = [
{ label: '仅 CMPP', value: 'cmpp' },
{ label: '仅 HTTP', value: 'http' },
{ label: 'CMPP + HTTP 双投', value: 'both' },
{ label: '不投递', value: 'none' },
];
const httpCapabilityOptions: Array<{ key: keyof HttpApiConfig; label: string }> = [
{ key: 'sendEnabled', label: '单条发送' },
{ key: 'messageQueryEnabled', label: '状态查询' },
@@ -62,6 +55,8 @@ export function AdminSmsApplicationFormPage() {
allowClientManualRetry: true, allowClientTest: true,
});
const [httpIpAddress, setHttpIpAddress] = useState('');
const [receiptWebhookUrl, setReceiptWebhookUrl] = useState('');
const [uplinkWebhookUrl, setUplinkWebhookUrl] = useState('');
const [groups, setGroups] = useState<ChannelGroup[]>([]);
const [mobileGroupId, setMobileGroupId] = useState('');
const [unicomGroupId, setUnicomGroupId] = useState('');
@@ -128,10 +123,15 @@ export function AdminSmsApplicationFormPage() {
useEffect(() => {
if (!appId) return;
let cancelled = false;
adminApi.getApplicationHttpApiConfig(appId).then((result) => {
Promise.all([
adminApi.getApplicationHttpApiConfig(appId),
adminApi.listApplicationHttpWebhooks(appId),
]).then(([result, webhooks]) => {
if (cancelled) return;
if (result.config) setHttpConfig(result.config);
setHttpIpAddress(result.ipAllowlist.join('\n'));
setReceiptWebhookUrl(webhooks.find((item) => item.eventType === 'receipt')?.url ?? '');
setUplinkWebhookUrl(webhooks.find((item) => item.eventType === 'uplink')?.url ?? '');
}).catch((failure: Error) => {
if (!cancelled) setError(failure.message || 'HTTP接口配置加载失败');
});
@@ -245,6 +245,10 @@ export function AdminSmsApplicationFormPage() {
})),
});
await adminApi.updateApplicationHttpApiConfig(application.id, { ...httpConfig, ipAllowlist: parseIpAllowlist(httpIpAddress) });
await Promise.all([
adminApi.saveApplicationHttpWebhook(application.id, 'receipt', { url: receiptWebhookUrl.trim() }),
adminApi.saveApplicationHttpWebhook(application.id, 'uplink', { url: uplinkWebhookUrl.trim() }),
]);
goBack();
} catch (failure) {
setError(failure instanceof Error ? failure.message : '短信应用保存失败');
@@ -403,8 +407,6 @@ export function AdminSmsApplicationFormPage() {
<Input label="HTTP QPS" onChange={(event) => setHttpConfig((current) => ({ ...current, qpsLimit: Number(event.target.value) || 1 }))} value={String(httpConfig.qpsLimit)} />
<Input label="签名时间容差(秒)" onChange={(event) => setHttpConfig((current) => ({ ...current, timestampToleranceSeconds: Number(event.target.value) || 300 }))} value={String(httpConfig.timestampToleranceSeconds)} />
<Input label="最多有效凭据数" onChange={(event) => setHttpConfig((current) => ({ ...current, maxCredentialCount: Number(event.target.value) || 2 }))} value={String(httpConfig.maxCredentialCount)} />
<Select label="回执投递方式" onChange={(event) => setHttpConfig((current) => ({ ...current, receiptDeliveryMode: event.target.value as HttpApiConfig['receiptDeliveryMode'] }))} options={deliveryModeOptions} value={httpConfig.receiptDeliveryMode} />
<Select label="上行投递方式" onChange={(event) => setHttpConfig((current) => ({ ...current, uplinkDeliveryMode: event.target.value as HttpApiConfig['uplinkDeliveryMode'] }))} options={deliveryModeOptions} value={httpConfig.uplinkDeliveryMode} />
<Input label="Webhook 超时(秒)" onChange={(event) => setHttpConfig((current) => ({ ...current, webhookTimeoutSeconds: Number(event.target.value) || 10 }))} value={String(httpConfig.webhookTimeoutSeconds)} />
<Input label="Webhook 最大尝试次数" onChange={(event) => setHttpConfig((current) => ({ ...current, webhookMaxAttempts: Number(event.target.value) || 7 }))} value={String(httpConfig.webhookMaxAttempts)} />
<div className="admin-app-form-row admin-app-form-row--wide"><span>HTTP </span><div className="radio-row">
@@ -413,7 +415,26 @@ export function AdminSmsApplicationFormPage() {
<label><input checked={httpConfig.allowClientManualRetry} onChange={() => setHttpConfig((current) => ({ ...current, allowClientManualRetry: !current.allowClientManualRetry }))} type="checkbox" /></label>
</div></div>
</div>
) : <div className="admin-app-protocol-empty">HTTP Webhook </div>}
) : <div className="admin-app-protocol-empty">HTTP </div>}
<div className="admin-app-form-grid admin-app-protocol-body">
<Input
hint="留空不推送;HTTP接口开通后按该地址推送状态回执。"
label="HTTP 回执地址"
onChange={(event) => setReceiptWebhookUrl(event.target.value)}
placeholder="https://example.com/webhooks/sms/receipt"
value={receiptWebhookUrl}
/>
<Input
hint="留空不推送;HTTP接口开通后按该地址推送上行短信。"
label="HTTP 上行地址"
onChange={(event) => setUplinkWebhookUrl(event.target.value)}
placeholder="https://example.com/webhooks/sms/uplink"
value={uplinkWebhookUrl}
/>
<div className="admin-app-form-row admin-app-form-row--wide">
<div className="admin-app-form-tip"><Info size={17} /><span>CMPP开通则走CMPPHTTP开通且地址非空则走HTTP</span></div>
</div>
</div>
</section>
<section className="ui-detail-section">