feat: add report material workflows and gateway safeguards
This commit is contained in:
+139
-1
@@ -103,6 +103,33 @@ async function requestBlob(path: string, options: RequestOptions = {}): Promise<
|
||||
return response.blob();
|
||||
}
|
||||
|
||||
async function requestForm<T>(path: string, form: FormData, reauthenticationAttempted = false): Promise<T> {
|
||||
const headers = new Headers();
|
||||
const session = readSession();
|
||||
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
|
||||
const response = await fetch(`/api${path}`, { method: 'POST', headers, body: form, credentials: 'same-origin' });
|
||||
if (response.status === 401 && session) {
|
||||
const body = await readErrorBody(response.clone());
|
||||
if (body.code === 'SESSION_LOCKED') {
|
||||
dispatchSessionEvent('locked', { message: body.message });
|
||||
throw new Error(typeof body.message === 'string' ? body.message : '由于长时间未操作,会话已安全锁定');
|
||||
}
|
||||
clearSession();
|
||||
dispatchSessionEvent('logout', { code: body.code, message: body.message });
|
||||
window.location.assign(session.portal === 'admin' ? '/admin/login' : '/client/login');
|
||||
throw new Error('登录会话已失效,请重新登录');
|
||||
}
|
||||
if (response.status === 403 && session && !reauthenticationAttempted) {
|
||||
const body = await readErrorBody(response.clone());
|
||||
if (body.code === 'RECENT_AUTHENTICATION_REQUIRED') {
|
||||
await requestReauthentication();
|
||||
return requestForm<T>(path, form, true);
|
||||
}
|
||||
}
|
||||
if (!response.ok) throw new Error(await readErrorMessage(response));
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export type AdminChannel = {
|
||||
id: string;
|
||||
code: string;
|
||||
@@ -556,7 +583,6 @@ export type ChannelGroupItem = DictionaryItem & {
|
||||
priority: number;
|
||||
weight?: number;
|
||||
isBackup?: boolean;
|
||||
rateLimitPerSecond?: number | null;
|
||||
channel?: AdminChannel;
|
||||
};
|
||||
|
||||
@@ -570,9 +596,53 @@ export type ChannelReportField = DictionaryItem & {
|
||||
required: boolean;
|
||||
description?: string | null;
|
||||
sortOrder?: number;
|
||||
exportName?: string | null;
|
||||
columnWidth?: number;
|
||||
imageWidth?: number;
|
||||
imageHeight?: number;
|
||||
defaultValue?: string | null;
|
||||
transform?: string | null;
|
||||
drainageField?: DictionaryItem | null;
|
||||
};
|
||||
|
||||
export type ReportMaterialPendingItem = {
|
||||
id: string;
|
||||
reportType: 'signature' | 'drainage';
|
||||
signatureId: string;
|
||||
drainageItemId?: string | null;
|
||||
materialVersion: number;
|
||||
changedAt: string;
|
||||
name: string;
|
||||
detail?: string | null;
|
||||
signatureName?: string;
|
||||
tenant?: TenantOption;
|
||||
application?: ClientSmsApplication | null;
|
||||
};
|
||||
|
||||
export type ReportImportMapping = {
|
||||
sourceHeader: string;
|
||||
sourceHeaderPath?: string;
|
||||
sourceColumnIndex: number;
|
||||
targetFieldCode: string;
|
||||
targetKind: 'signatureName' | 'purpose' | 'siteName' | 'url' | 'remark' | 'dynamic';
|
||||
fieldType: 'string' | 'image' | 'file';
|
||||
required?: boolean;
|
||||
transform?: string;
|
||||
sortOrder?: number;
|
||||
};
|
||||
|
||||
export type ReportImportProfile = {
|
||||
id: string;
|
||||
name: string;
|
||||
reportType: 'signature' | 'drainage';
|
||||
tenantId?: string | null;
|
||||
applicationId?: string | null;
|
||||
sheetName?: string | null;
|
||||
headerRowCount: number;
|
||||
dataStartRow: number;
|
||||
columns: ReportImportMapping[];
|
||||
};
|
||||
|
||||
export type ApplicationReportField = {
|
||||
id: string;
|
||||
code: string;
|
||||
@@ -962,6 +1032,51 @@ export type GatewayDownstreamRecoveryStatus = {
|
||||
application?: EnterpriseApplication | null;
|
||||
};
|
||||
|
||||
export type GatewaySubmitException = {
|
||||
id: string;
|
||||
streamMessageId: string;
|
||||
tenantId?: string | null;
|
||||
applicationId?: string | null;
|
||||
channelId?: string | null;
|
||||
traceId?: string | null;
|
||||
messageId?: string | null;
|
||||
submitId?: string | null;
|
||||
status: 'pending' | 'requeueing' | 'requeued' | 'resolved' | string;
|
||||
failureCode: string;
|
||||
failureMessage: string;
|
||||
attempts: number;
|
||||
maxAttempts: number;
|
||||
commandPayload?: Record<string, unknown> | null;
|
||||
rawPayloadAvailable?: boolean;
|
||||
messageState?: {
|
||||
status: string;
|
||||
submitStatus?: string | null;
|
||||
receiptStatus?: string | null;
|
||||
phoneNumber: string;
|
||||
content: string;
|
||||
} | null;
|
||||
manualRetryCount: number;
|
||||
lastRetryStreamId?: string | null;
|
||||
lastRetriedAt?: string | null;
|
||||
resolvedAt?: string | null;
|
||||
resolvedStatus?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tenant?: Pick<TenantOption, 'id' | 'name' | 'code' | 'status'> | null;
|
||||
application?: Pick<EnterpriseApplication, 'id' | 'tenantId' | 'name' | 'status'> | null;
|
||||
channel?: Pick<AdminChannel, 'id' | 'code' | 'name' | 'status' | 'carrier' | 'sendRegion' | 'rateLimitPerSecond'> | null;
|
||||
};
|
||||
|
||||
export type GatewaySubmitExceptionResponse = PagedResponse<GatewaySubmitException> & {
|
||||
summary: {
|
||||
pending: number;
|
||||
requeueing: number;
|
||||
requeued: number;
|
||||
resolved: number;
|
||||
oldestPendingAt?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
export type DownstreamRecoveryStatusResponse = PagedResponse<GatewayDownstreamRecoveryStatus> & {
|
||||
summary: {
|
||||
total: number;
|
||||
@@ -1163,6 +1278,25 @@ export const adminApi = {
|
||||
listChannelReportFields: (channelId?: string) => request<ChannelReportField[]>(withQuery('/admin/channel-report-fields', { channelId })),
|
||||
createChannelReportField: (body: Record<string, unknown>) =>
|
||||
request<ChannelReportField>('/admin/channel-report-fields', { method: 'POST', body: JSON.stringify(body) }),
|
||||
replaceChannelReportFields: (channelId: string, reportType: 'signature' | 'drainage', fields: Array<Record<string, unknown>>) =>
|
||||
request<ChannelReportField[]>(`/admin/channels/${channelId}/report-fields/${reportType}`, { method: 'PUT', body: JSON.stringify({ fields }) }),
|
||||
listPendingReportMaterials: (query: { reportType?: 'signature' | 'drainage'; tenantId?: string; applicationId?: string } = {}) =>
|
||||
request<ReportMaterialPendingItem[]>(withQuery('/admin/report-materials/pending', query)),
|
||||
listReportImportProfiles: (reportType?: 'signature' | 'drainage') =>
|
||||
request<ReportImportProfile[]>(withQuery('/admin/report-materials/import-profiles', { reportType })),
|
||||
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 }) => {
|
||||
const form = new FormData();
|
||||
form.set('file', file);
|
||||
Object.entries(body).forEach(([key, value]) => { if (value !== undefined) form.set(key, String(value)); });
|
||||
return requestForm<Record<string, unknown> & { id: string; columns: Array<{ sourceColumnIndex: number; columnLetter: string; sourceHeader: string; sourceHeaderPath: string; imageCount: number }>; rows: Array<Record<string, unknown>>; suggestedMappings: ReportImportMapping[] }>('/admin/report-materials/imports/analyze', form);
|
||||
},
|
||||
commitReportMaterialImport: (id: string, body: { mappings: ReportImportMapping[]; profile?: Omit<ReportImportProfile, 'id'> & { id?: string } }) =>
|
||||
request<Record<string, unknown>>(`/admin/report-materials/imports/${id}/commit`, { method: 'PUT', body: JSON.stringify(body) }),
|
||||
listReportMaterialBatches: () => request<Array<Record<string, unknown>>>('/admin/report-materials/batches'),
|
||||
createReportMaterialBatch: (body: { createdById?: string; items: Array<{ reportType: 'signature' | 'drainage'; signatureId: string; drainageItemId?: string }> }) =>
|
||||
request<Record<string, unknown>>('/admin/report-materials/batches', { method: 'POST', body: JSON.stringify(body) }),
|
||||
listReportTasks: (query: { tenantId?: string; status?: string; channelId?: string; reportType?: 'signature' | 'drainage' } = {}) => request<ReportTask[]>(withQuery('/admin/report-tasks', query)),
|
||||
createReportTask: (body: { tenantId: string; signatureId: string; channelId: string; reportType?: 'signature' | 'drainage'; drainageItemId?: string; createdById?: string }) =>
|
||||
request<ReportTask>('/admin/report-tasks/generate', { method: 'POST', body: JSON.stringify(body) }),
|
||||
@@ -1188,6 +1322,10 @@ export const adminApi = {
|
||||
claimUplinkMatchCandidate: (uplinkMessageId: string, body: { candidateId: string; operatorId?: string }) =>
|
||||
request<SmsUplinkMessage>(`/admin/operations/uplink-messages/${uplinkMessageId}/claim`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
listMonitor: (query: { tenantId?: string; channelId?: string } = {}) => request<Record<string, unknown>>(withQuery('/admin/operations/monitor', query)),
|
||||
listGatewaySubmitExceptions: (query: { tenantId?: string; applicationId?: string; channelId?: string; status?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
|
||||
request<GatewaySubmitExceptionResponse>(withQuery('/admin/operations/gateway-submit-dead-letters', query)),
|
||||
requeueGatewaySubmitException: (id: string, body: { confirmedNotSubmitted: boolean; reason: string }) =>
|
||||
request<GatewaySubmitException>(`/admin/operations/gateway-submit-dead-letters/${id}/requeue`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
listStatistics: (query: { tenantId?: string; groupBy?: string } = {}) => request<Array<Record<string, unknown>>>(withQuery('/admin/operations/statistics', query)),
|
||||
getDownstreamDeliveryDashboard: (query: { tenantId?: string; applicationId?: string; deliveryType?: string; createdAtFrom?: string; createdAtTo?: string } = {}) =>
|
||||
request<DownstreamDeliveryDashboard>(withQuery('/admin/operations/downstream-deliveries/dashboard', query)),
|
||||
|
||||
Reference in New Issue
Block a user