feat: add platform dunning notifications

This commit is contained in:
Codex
2026-06-30 07:28:32 +08:00
parent e66623158d
commit 51f8cbdec8
26 changed files with 1782 additions and 24 deletions

View File

@@ -7,6 +7,8 @@ import {
loadPlatformAuditLogs,
loadPlatformAuditNotificationChannels,
loadPlatformAuditNotificationEvents,
loadPlatformDunningNotificationChannels,
loadPlatformDunningNotificationEvents,
loadPlatformInvoices,
loadPlatformOverview,
loadPlatformQuestionBankGrants,
@@ -17,6 +19,8 @@ import {
type PlatformAuditLogItem,
type PlatformAuditNotificationChannelItem,
type PlatformAuditNotificationEventItem,
type PlatformDunningNotificationChannelItem,
type PlatformDunningNotificationEventItem,
type PlatformInvoiceItem,
type PlatformOverview,
type PlatformQuestionBankGrant,
@@ -48,6 +52,8 @@ export default function PlatformWorkbenchPage() {
const [auditAlerts, setAuditAlerts] = useState<PlatformAuditAlertItem[]>([]);
const [auditNotificationChannels, setAuditNotificationChannels] = useState<PlatformAuditNotificationChannelItem[]>([]);
const [auditNotificationEvents, setAuditNotificationEvents] = useState<PlatformAuditNotificationEventItem[]>([]);
const [dunningNotificationChannels, setDunningNotificationChannels] = useState<PlatformDunningNotificationChannelItem[]>([]);
const [dunningNotificationEvents, setDunningNotificationEvents] = useState<PlatformDunningNotificationEventItem[]>([]);
const [banks, setBanks] = useState<PlatformQuestionBankItem[]>([]);
const [grants, setGrants] = useState<PlatformQuestionBankGrant[]>([]);
const [error, setError] = useState('');
@@ -64,7 +70,9 @@ export default function PlatformWorkbenchPage() {
loadPlatformAuditAlerts({ status: 'open', limit: 6 }).catch(() => ({ items: [] })),
loadPlatformAuditNotificationChannels({ enabled: true, limit: 6 }).catch(() => ({ items: [] })),
loadPlatformAuditNotificationEvents({ limit: 6 }).catch(() => ({ items: [] })),
]).then(([overviewPayload, tenantPayload, invoicePayload, bankPayload, grantPayload, auditPayload, alertPayload, channelPayload, eventPayload]) => {
loadPlatformDunningNotificationChannels({ enabled: true, limit: 6 }).catch(() => ({ items: [] })),
loadPlatformDunningNotificationEvents({ limit: 6 }).catch(() => ({ items: [] })),
]).then(([overviewPayload, tenantPayload, invoicePayload, bankPayload, grantPayload, auditPayload, alertPayload, channelPayload, eventPayload, dunningChannelPayload, dunningEventPayload]) => {
setOverview(overviewPayload.item || null);
setTenants(tenantPayload.items || []);
setInvoices(invoicePayload.items || []);
@@ -74,6 +82,8 @@ export default function PlatformWorkbenchPage() {
setAuditAlerts(alertPayload.items || []);
setAuditNotificationChannels(channelPayload.items || []);
setAuditNotificationEvents(eventPayload.items || []);
setDunningNotificationChannels(dunningChannelPayload.items || []);
setDunningNotificationEvents(dunningEventPayload.items || []);
}).catch(nextError => setError(nextError instanceof Error ? nextError.message : '平台后台加载失败'));
}, []);
@@ -230,6 +240,30 @@ export default function PlatformWorkbenchPage() {
</View>
{!auditNotificationChannels.length && !auditNotificationEvents.length ? <View className='platform-empty'></View> : null}
</View>
<View className='platform-section'>
<Text className='platform-section-title'></Text>
<View className='platform-grid'>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(dunningNotificationChannels.length)}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(dunningNotificationEvents.length)}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(dunningNotificationEvents.filter(item => item.status === 'failed').length)}</Text></View>
<View className='platform-metric'><Text className='platform-metric-label'></Text><Text className='platform-metric-value'>{String(dunningNotificationEvents.filter(item => item.status === 'retrying').length)}</Text></View>
</View>
<View className='platform-list'>
{dunningNotificationChannels.map(item => (
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.name || item.channelCode || '-'}</Text>
<Text className='platform-row-meta'>{item.provider || '-'} · level {String(item.minReminderLevel || '-')} · {item.webhook?.host || '-'} · {item.reminderTypes?.join('/') || '-'}</Text>
</View>
))}
{dunningNotificationEvents.map(item => (
<View className='platform-row' key={item.id}>
<Text className='platform-row-main'>{item.invoiceNo || item.invoiceId || '-'}</Text>
<Text className='platform-row-meta'>{item.channelName || item.channelCode || '-'} · {item.status || '-'} · HTTP {String(item.lastHttpCode || '-')} · {item.tenantName || item.tenantSlug || '-'} · {money(item.invoiceBalanceCents)}</Text>
</View>
))}
</View>
{!dunningNotificationChannels.length && !dunningNotificationEvents.length ? <View className='platform-empty'></View> : null}
</View>
{error ? <Text className='platform-error'>{error}</Text> : null}
</View>
</View>

View File

@@ -328,6 +328,55 @@ export interface PlatformAuditNotificationEventItem {
updatedAt?: string | null;
}
export interface PlatformDunningNotificationChannelItem {
id: string;
channelCode?: string | null;
name?: string | null;
description?: string | null;
enabled?: boolean | null;
provider?: string | null;
secretRef?: string | null;
reminderTypes?: string[] | null;
reminderChannels?: string[] | null;
minReminderLevel?: number | string | null;
tenantIds?: string[] | null;
timeoutSec?: number | string | null;
webhook?: {
protocol?: string | null;
host?: string | null;
pathname?: string | null;
} | null;
metadata?: Record<string, unknown> | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface PlatformDunningNotificationEventItem {
id: string;
channelId?: string | null;
channelCode?: string | null;
channelName?: string | null;
reminderId?: string | null;
invoiceId?: string | null;
invoiceNo?: string | null;
tenantId?: string | null;
tenantName?: string | null;
tenantSlug?: string | null;
provider?: string | null;
status?: string | null;
attempts?: number | string | null;
lastHttpCode?: number | string | null;
lastError?: string | null;
lastResponseSummary?: string | null;
reminderType?: string | null;
reminderChannel?: string | null;
reminderLevel?: number | string | null;
invoiceBalanceCents?: number | string | null;
sentAt?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface CreatePlatformTenantInput {
slug: string;
name: string;
@@ -521,6 +570,32 @@ export async function loadPlatformAuditNotificationEvents(query: {
});
}
export async function loadPlatformDunningNotificationChannels(query: {
enabled?: boolean;
provider?: string;
limit?: number;
} = {}) {
return apiRequest<{ items?: PlatformDunningNotificationChannelItem[] }>('/api/platform-admin/dunning-notification-channels', {
query: { ...query, limit: query.limit || 50 },
tenantId: null,
});
}
export async function loadPlatformDunningNotificationEvents(query: {
channelId?: string;
reminderId?: string;
invoiceId?: string;
tenantId?: string;
status?: string;
provider?: string;
limit?: number;
} = {}) {
return apiRequest<{ items?: PlatformDunningNotificationEventItem[] }>('/api/platform-admin/dunning-notification-events', {
query: { ...query, limit: query.limit || 50 },
tenantId: null,
});
}
export async function loadPlatformInvoices(query: { tenantId?: string; status?: string; limit?: number } = {}) {
return apiRequest<{ items?: PlatformInvoiceItem[] }>('/api/platform-admin/invoices', {
query: { ...query, limit: query.limit || 80 },