feat: harden CMPP delivery and platform workflows

This commit is contained in:
hectorzhao
2026-07-20 18:07:29 +08:00
parent 80fb5a8f53
commit f02c33cbb7
61 changed files with 1834 additions and 281 deletions
+21 -3
View File
@@ -1,4 +1,5 @@
import { clearSession, dispatchSessionEvent, getSessionTenantId, hasRecentUserActivity, readSession, requestReauthentication, type LoginSession } from './session';
import { assertUploadFileSize } from '@/utils/fileUpload';
type RequestOptions = RequestInit & {
tenantId?: string;
@@ -346,8 +347,10 @@ export type ClientSmsSignature = {
};
export type ClientSmsSignatureView = Pick<ClientSmsSignature,
'id' | 'tenantId' | 'applicationId' | 'name' | 'purpose' | 'auditStatus' | 'rejectReason' | 'createdAt' | 'updatedAt' | 'materials'
'id' | 'tenantId' | 'applicationId' | 'name' | 'purpose' | 'auditStatus' | 'reportStatus' | 'rejectReason' | 'createdAt' | 'updatedAt' | 'materials'
> & {
pendingReport?: boolean;
reportChangedAt?: string;
application?: Pick<ClientSmsApplication, 'id' | 'name' | 'status'> | null;
submittedMaterialCount: number;
reportValues: Record<string, unknown>;
@@ -474,6 +477,8 @@ export type SmsMessageRecord = {
carrier?: string | null;
province?: string | null;
content: string;
clientSrcId?: string | null;
applicationExtension?: string | null;
billingUnits: number;
amountCents: number;
status: string;
@@ -794,6 +799,7 @@ export type RiskReviewTask = {
rejectReason?: string | null;
createdAt: string;
reviewedAt?: string | null;
reviewedBy?: { id: string; username: string; displayName: string } | null;
riskHits?: Array<{ id: string; ruleName: string; reason: string }>;
_count?: { messageRecords: number };
};
@@ -845,6 +851,7 @@ export type DailyReconciliationReport = {
applicationName: string;
sentUnits: number;
successUnits: number;
failedUnits: number;
generatedAt: string;
updatedAt: string;
};
@@ -861,7 +868,9 @@ export type DailyProfitReport = {
channelId?: string | null;
sentUnits: number;
successUnits: number;
failedUnits: number;
revenueCents: number;
refundCents: number;
costCents: number;
profitCents: number;
profitRateBps: number;
@@ -883,6 +892,7 @@ export type DailyQualityReport = {
drainageInfoId?: string | null;
sentUnits: number;
successUnits: number;
failedUnits: number;
successRateBps: number;
avgArrivalMs?: number | null;
generatedAt: string;
@@ -1351,6 +1361,7 @@ export const adminApi = {
saveReportImportProfile: (body: Omit<ReportImportProfile, 'id'> & { id?: string }) =>
request<ReportImportProfile>('/admin/report-materials/import-profiles', { method: 'POST', body: JSON.stringify(body) }),
analyzeReportMaterialImport: (file: File, body: { tenantId: string; applicationId?: string; reportType: 'signature' | 'drainage'; sheetName?: string; headerRowCount?: number; dataStartRow?: number; profileId?: string }) => {
assertUploadFileSize(file);
const form = new FormData();
form.set('file', file);
Object.entries(body).forEach(([key, value]) => { if (value !== undefined) form.set(key, String(value)); });
@@ -1433,6 +1444,7 @@ export const adminApi = {
request<{ items: DictionaryItem[]; total: number; page: number; pageSize: number }>(withQuery('/admin/dictionaries/phone-carrier-rules', query)),
createPhoneCarrierRule: (body: { carrier: string; pattern: string; priority?: number; status?: string; remark?: string }) =>
request<DictionaryItem>('/admin/dictionaries/phone-carrier-rules', { method: 'POST', body: JSON.stringify(body) }),
deletePhoneCarrierRule: (id: string) => request<DictionaryItem>(`/admin/dictionaries/phone-carrier-rules/${id}`, { method: 'DELETE' }),
listDrainageFields: () => request<DictionaryItem[]>('/admin/dictionaries/drainage-fields'),
createDrainageField: (body: { code: string; name: string; fieldType: 'string' | 'image' | 'file'; required?: boolean; status?: string; description?: string }) =>
request<DictionaryItem>('/admin/dictionaries/drainage-fields', { method: 'POST', body: JSON.stringify(body) }),
@@ -1442,6 +1454,7 @@ export const adminApi = {
request<CommonReportField>('/admin/dictionaries/common-report-fields', { method: 'POST', body: JSON.stringify(body) }),
deleteCommonReportField: (id: string) => request<CommonReportField>(`/admin/dictionaries/common-report-fields/${id}`, { method: 'DELETE' }),
uploadFileObject: async (file: File, body: { purpose: string; prefix?: string }, tenantId?: string) => {
assertUploadFileSize(file);
const form = new FormData();
form.set('file', file);
form.set('purpose', body.purpose);
@@ -1539,8 +1552,12 @@ export const clientApi = {
request<SmsDrainageInfo>(`/client/drainage-infos/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
changeDrainageInfoStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<SmsDrainageInfo>(`/client/drainage-infos/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
listTemplates: (query: { status?: string; keyword?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsTemplate[]>(withQuery('/client/templates', query), { tenantId }),
listTemplates: (query: { status?: string; keyword?: string; includeHistory?: boolean } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsTemplate[]>(withQuery('/client/templates', {
status: query.status,
keyword: query.keyword,
includeHistory: query.includeHistory === undefined ? undefined : String(query.includeHistory),
}), { tenantId }),
createTemplate: (body: { tenantId?: string; applicationId: string; signatureId?: string; name: string; content: string; category?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsTemplate>('/client/templates', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
updateTemplate: (id: string, body: { applicationId?: string; signatureId?: string | null; name?: string; content?: string; category?: string; auditStatus?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
@@ -1568,6 +1585,7 @@ export const clientApi = {
createFileObject: (body: { bucket?: string; objectKey: string; fileName: string; contentType: string; sizeBytes: number; purpose: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<FileObject>('/admin/files', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId, bucket: body.bucket ?? 'cmpp-platform' }) }),
uploadFileObject: async (file: File, body: { purpose: string; prefix?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => {
assertUploadFileSize(file);
const form = new FormData();
form.set('file', file);
form.set('purpose', body.purpose);