feat: add phone frequency controls and modularize codebase

This commit is contained in:
hectorzhao
2026-07-31 22:25:23 +08:00
parent 0af671b4ed
commit ca4f591a13
216 changed files with 41579 additions and 23694 deletions
+93
View File
@@ -0,0 +1,93 @@
import { request, requestBlob, requestForm, withQuery } from '../core/httpClient';
import type { AdminChannel, ChannelConnectionLogResponse, ChannelGroup, ChannelReportField, ChannelTestResponse, CmppConnectionState, DeleteTargetRequest, DeletionPreflight, DeletionResult, DeletionTargetType, DictionaryItem, PagedResult, ReportImportMapping, ReportImportProfile, ReportImportReviewBatch, ReportMaterialBatch, ReportMaterialBatchPreflight, ReportMaterialBatchResult, ReportMaterialPendingItem, ReportRecord, ReportTask } from '../types';
import { assertUploadFileSize } from '@/utils/fileUpload';
// Report generation consumes channel report fields, so these endpoints keep one
// explicit integration boundary while the facade remains unchanged.
export const adminChannelsReportsApi = {
listChannels: () => request<AdminChannel[]>('/admin/channels'),
listChannelsPage: (query: { keyword?: string; carrier?: string; status?: string; page: number; pageSize: number }) =>
request<PagedResult<AdminChannel>>(withQuery('/admin/channels', query)),
createChannel: (body: Partial<AdminChannel> & { passwordCipher?: string; desiredConnections?: number; windowSize?: number; heartbeatIntervalSeconds?: number; heartbeatMissThreshold?: number }) =>
request<AdminChannel>('/admin/channels', { method: 'POST', body: JSON.stringify(body) }),
updateChannel: (id: string, body: Partial<AdminChannel> & { passwordCipher?: string; desiredConnections?: number; windowSize?: number; heartbeatIntervalSeconds?: number; heartbeatMissThreshold?: number }) =>
request<AdminChannel>(`/admin/channels/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
copyChannel: (id: string, body: { operatorId?: string } = {}) => request<AdminChannel>(`/admin/channels/${id}/copy`, {
method: 'POST',
body: JSON.stringify(body),
}),
testChannel: (id: string, body: { phoneNumber?: string; phones?: string[] | string; content: string; accessNo?: string }) =>
request<ChannelTestResponse>(`/admin/channels/${id}/test`, { method: 'POST', body: JSON.stringify(body) }),
changeChannelStatus: (id: string, status: string, reason?: string) => request<AdminChannel>(`/admin/channels/${id}/status`, {
method: 'POST',
body: JSON.stringify({ status, reason }),
}),
deleteChannel: (id: string, reason?: string) => request<AdminChannel>(`/admin/channels/${id}`, {
method: 'DELETE',
body: JSON.stringify({ reason }),
}),
getDeletionPreflight: (type: DeletionTargetType, id: string) =>
request<DeletionPreflight>(`/admin/deletions/${type}/${id}/preflight`),
deleteGovernedTarget: (type: DeletionTargetType, id: string, body: DeleteTargetRequest) =>
request<DeletionResult>(`/admin/deletions/${type}/${id}`, { method: 'POST', body: JSON.stringify(body) }),
listChannelConnectionLogs: (id: string) => request<ChannelConnectionLogResponse>(`/admin/channels/${id}/connection-logs`),
listChannelGroups: () => request<ChannelGroup[]>('/admin/channel-groups'),
createChannelGroup: (body: { code: string; name: string; carrier: 'mobile' | 'unicom' | 'telecom'; description?: string; status?: string; retryEnabled?: boolean; retryTimeLimitHours?: number; retryTimeLimitMinutes?: number }) =>
request<ChannelGroup>('/admin/channel-groups', { method: 'POST', body: JSON.stringify(body) }),
updateChannelGroup: (id: string, body: { code?: string; name?: string; carrier?: 'mobile' | 'unicom' | 'telecom'; description?: string; status?: string; retryEnabled?: boolean; retryTimeLimitHours?: number; retryTimeLimitMinutes?: number; items?: Array<Record<string, unknown>> }) =>
request<ChannelGroup>(`/admin/channel-groups/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
deleteChannelGroup: (id: string) =>
request<ChannelGroup>(`/admin/channel-groups/${id}`, { method: 'DELETE' }),
addChannelGroupItem: (body: Record<string, unknown>) =>
request<DictionaryItem>('/admin/channel-groups/items', { method: 'POST', body: JSON.stringify(body) }),
listChannelRouteRules: () => request<DictionaryItem[]>('/admin/channel-route-rules'),
createChannelRouteRule: (body: { tenantId?: string; applicationId: string; groupId: string; carrier: string; priority?: number; status?: string }) =>
request<DictionaryItem>('/admin/channel-route-rules', { method: 'POST', body: JSON.stringify(body) }),
listChannelConnections: (id: string) => request<CmppConnectionState[]>(`/admin/channels/${id}/connections`),
replaceApplicationRouteRules: (applicationId: string, body: { routes: Array<{ carrier: 'mobile' | 'unicom' | 'telecom'; groupId: string; priority?: number; status?: string }> }) =>
request<DictionaryItem[]>(`/admin/enterprise-applications/${applicationId}/route-rules`, { method: 'PUT', body: JSON.stringify(body) }),
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; keyword?: string; startAt?: string; endAt?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResult<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 }) => {
assertUploadFileSize(file);
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) }),
listReportImportReviewBatches: (query: { reportType?: 'signature' | 'drainage'; status?: string; keyword?: string; startAt?: string; endAt?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResult<ReportImportReviewBatch>>(withQuery('/admin/report-materials/imports/review-batches', query)),
reviewReportImportItems: (id: string, body: { decision: 'approve' | 'reject'; itemIds?: string[]; reason?: string }) =>
request<{ batchId: string; status: string; approvedCount: number; rejectedCount: number; failedCount: number }>(`/admin/report-materials/imports/${id}/review`, { method: 'POST', body: JSON.stringify(body) }),
listReportMaterialBatches: (query: { keyword?: string; startAt?: string; endAt?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResult<ReportMaterialBatch>>(withQuery('/admin/report-materials/batches', query)),
preflightReportMaterialBatch: (body: { items: Array<{ reportType: 'signature' | 'drainage'; signatureId: string; drainageItemId?: string; materialVersion?: number }> }) =>
request<ReportMaterialBatchPreflight>('/admin/report-materials/batches/preflight', { method: 'POST', body: JSON.stringify(body) }),
createReportMaterialBatch: (body: { idempotencyKey: string; items: Array<{ reportType: 'signature' | 'drainage'; signatureId: string; drainageItemId?: string; materialVersion: number }> }) =>
request<ReportMaterialBatchResult>('/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)),
listReportTasksPage: (query: { tenantId?: string; status?: string; channelId?: string; reportType?: 'signature' | 'drainage'; keyword?: string; createdAtFrom?: string; createdAtTo?: string; page: number; pageSize: number }) =>
request<PagedResult<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) }),
changeReportTaskStatuses: (body: { items: Array<{ signatureId: string; channelId: string; status: string; reportType?: 'signature' | 'drainage'; drainageItemId?: string }>; reason?: string; operatorId?: string; sourceEntry?: 'enterprise_signature' | 'report_task' | 'channel_report' }) =>
request<Array<{ signatureId: string; reportStatus: string; carrierReportSummary: Record<string, { status: string; approved: number; total: number }> }>>('/admin/report-tasks/status-change', { method: 'POST', body: JSON.stringify(body) }),
createReportExport: (id: string, body: { fileObjectId?: string; fileName: string; rowCount?: number }) =>
request<Record<string, unknown>>(`/admin/report-tasks/${id}/export`, { method: 'POST', body: JSON.stringify(body) }),
importReportReceipt: (id: string, body: { fileObjectId?: string; fileName: string; fileContent?: string; delimiter?: ',' | '\t'; rowCount?: number; successCount?: number; failedCount?: number; statusAfter?: string; reason?: string; result?: Record<string, unknown> }) =>
request<Record<string, unknown>>(`/admin/report-tasks/${id}/receipt-import`, { method: 'POST', body: JSON.stringify(body) }),
listReportRecords: (query: { taskId?: string; channelId?: string } = {}) => request<ReportRecord[]>(withQuery('/admin/report-records', query)),
listReportRecordsPage: (query: { taskId?: string; channelId?: string; keyword?: string; reportType?: 'signature' | 'drainage'; createdAtFrom?: string; createdAtTo?: string; page: number; pageSize: number }) =>
request<PagedResult<ReportRecord>>(withQuery('/admin/report-records', query)),
};
+38
View File
@@ -0,0 +1,38 @@
import { request, requestBlob, requestForm, withQuery } from '../core/httpClient';
import type { FileObject } from '../types';
import { assertUploadFileSize } from '@/utils/fileUpload';
import { clearSession, dispatchSessionEvent, hasRecentUserActivity, readSession, redirectToPortalLogin } from '../session';
import { readErrorBody } from '../core/httpClient';
export const adminFilesApi = {
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);
if (body.prefix) {
form.set('prefix', body.prefix);
}
const headers = new Headers();
const session = readSession('admin');
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
if (tenantId) {
headers.set('x-tenant-id', tenantId);
}
const response = await fetch('/api/admin/files/upload', { method: 'POST', headers, body: form, credentials: 'same-origin' });
if (response.status === 401 && session) {
const error = await readErrorBody(response.clone());
if (error.code === 'SESSION_LOCKED') {
dispatchSessionEvent('admin', 'locked', { message: error.message });
} else {
clearSession('admin');
dispatchSessionEvent('admin', 'logout', { code: error.code, message: error.message });
redirectToPortalLogin('admin');
}
}
if (!response.ok) {
throw new Error(await response.text());
}
return response.json() as Promise<FileObject>;
},
};
+194
View File
@@ -0,0 +1,194 @@
import { request, requestBlob, requestForm, withQuery } from '../core/httpClient';
import type { AuditRecord, ClientSmsSignature, ClientSmsTemplate, CommonReportField, DictionaryItem, EnterpriseCertification, ManualRechargePreflight, ManualRechargeResult, PagedResult, PhoneFrequencyHit, PhoneFrequencyWhitelistItem, RechargeOrder, ReviewDecisionResult, ReviewPreflight, RiskReviewTask, RiskRuleItem, RiskTaskMessagePage, SmsDrainageInfo, SmsTemplateAudit, TenantAccount } from '../types';
// Review, risk and billing mutations keep their original URLs, payloads and
// response types behind one governance boundary.
export const adminGovernanceApi = {
listAccounts: () => request<TenantAccount[]>('/admin/billing/accounts'),
updateCreditLimit: (tenantId: string, body: { creditCents: number; operatorId?: string; remark?: string }) =>
request<TenantAccount>(`/admin/billing/accounts/${tenantId}/credit-limit`, { method: 'POST', body: JSON.stringify(body) }),
listManualRecharges: (tenantId?: string) => request<RechargeOrder[]>(withQuery('/admin/billing/manual-recharges', { tenantId })),
listManualRechargesPage: (query: { enterpriseKeyword?: string; createdAtFrom?: string; createdAtTo?: string; page: number; pageSize: number }) =>
request<PagedResult<RechargeOrder>>(withQuery('/admin/billing/manual-recharges', query)),
preflightManualRecharge: (body: { tenantId: string; amountCents: number }) =>
request<ManualRechargePreflight>('/admin/billing/manual-recharges/preflight', { method: 'POST', body: JSON.stringify(body) }),
createManualRecharge: (body: { tenantId: string; amountCents: number; expectedAccountUpdatedAt: string; idempotencyKey: string; remark?: string }) =>
request<ManualRechargeResult>('/admin/billing/manual-recharges', { method: 'POST', body: JSON.stringify(body) }),
listTemplateAudits: (query: { keyword?: string; status?: string }) => {
const params = new URLSearchParams();
if (query.keyword) params.set('keyword', query.keyword);
if (query.status && query.status !== 'all') params.set('status', query.status);
const suffix = params.toString() ? `?${params}` : '';
return request<SmsTemplateAudit[]>(`/admin/enterprise-templates${suffix}`);
},
approveTemplate: (id: string) => request<SmsTemplateAudit>(`/admin/templates/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
rejectTemplate: (id: string, reason = '运营审核驳回') => request<SmsTemplateAudit>(`/admin/templates/${id}/reject`, {
method: 'POST',
body: JSON.stringify({ reason }),
}),
approveSignature: (id: string) => request<ClientSmsSignature>(`/admin/signatures/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
rejectSignature: (id: string, reason = '运营审核驳回') => request<ClientSmsSignature>(`/admin/signatures/${id}/reject`, {
method: 'POST',
body: JSON.stringify({ reason }),
}),
getReviewPreflight: (type: 'signature' | 'template', id: string) =>
request<ReviewPreflight>(`/admin/reviews/${type}/${id}/preflight`),
submitReviewDecision: (type: 'signature' | 'template', id: string, body: { decision: 'approve' | 'reject'; expectedUpdatedAt: string; idempotencyKey: string; reason?: string }) =>
request<ReviewDecisionResult>(`/admin/reviews/${type}/${id}/decision`, { method: 'POST', body: JSON.stringify(body) }),
listEnterpriseSignatures: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; signatureKeyword?: string; drainageKeyword?: string } = {}) =>
request<ClientSmsSignature[]>(withQuery('/admin/enterprise-signatures', query)),
listEnterpriseSignaturesPage: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; signatureKeyword?: string; drainageKeyword?: string; page: number; pageSize: number }) =>
request<PagedResult<ClientSmsSignature>>(withQuery('/admin/enterprise-signatures', query)),
listEnterpriseSignatureOptions: (query: { tenantId?: string } = {}) =>
request<ClientSmsSignature[]>(withQuery('/admin/enterprise-signature-options', query)),
createEnterpriseSignature: (body: { tenantId: string; applicationId?: string; name: string; purpose?: string; drainageInfo?: Record<string, unknown> }) =>
request<ClientSmsSignature>('/admin/enterprise-signatures', { method: 'POST', body: JSON.stringify(body) }),
updateEnterpriseSignature: (id: string, body: { applicationId?: string | null; name?: string; purpose?: string; auditStatus?: string; drainageInfo?: Record<string, unknown> }) =>
request<ClientSmsSignature>(`/admin/enterprise-signatures/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
changeEnterpriseSignatureStatus: (id: string, status: string, reason?: string) =>
request<ClientSmsSignature>(`/admin/enterprise-signatures/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
listDrainageInfos: (query: { tenantId?: string; signatureId?: string; keyword?: string; status?: string } = {}) =>
request<SmsDrainageInfo[]>(withQuery('/admin/drainage-infos', query)),
listAuditRecords: (query: { targetType?: string; targetId?: string } = {}) =>
request<AuditRecord[]>(withQuery('/admin/audit-records', query)),
createDrainageInfo: (signatureId: string, body: { siteName: string; url: string; remark?: string; reportValues?: Record<string, unknown> }) =>
request<SmsDrainageInfo>(`/admin/enterprise-signatures/${signatureId}/drainage-infos`, { method: 'POST', body: JSON.stringify(body) }),
updateDrainageInfo: (id: string, body: { siteName?: string; url?: string; remark?: string; reportValues?: Record<string, unknown> }) =>
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
approveDrainageInfo: (id: string) =>
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/approve`, { method: 'POST', body: JSON.stringify({}) }),
rejectDrainageInfo: (id: string, reason: string) =>
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason }) }),
changeDrainageInfoStatus: (id: string, status: string, reason?: string) =>
request<SmsDrainageInfo>(`/admin/drainage-infos/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
listEnterpriseTemplates: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; nameKeyword?: string; contentKeyword?: string } = {}) =>
request<ClientSmsTemplate[]>(withQuery('/admin/enterprise-templates', query)),
listEnterpriseTemplatesPage: (query: { tenantId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; nameKeyword?: string; contentKeyword?: string; page: number; pageSize: number }) =>
request<PagedResult<ClientSmsTemplate>>(withQuery('/admin/enterprise-templates', query)),
createEnterpriseTemplate: (body: { tenantId: string; applicationId: string; signatureId?: string; name: string; content: string; category?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }) =>
request<ClientSmsTemplate>('/admin/enterprise-templates', { method: 'POST', body: JSON.stringify(body) }),
updateEnterpriseTemplate: (id: string, body: { applicationId?: string; signatureId?: string | null; name?: string; content?: string; category?: string; auditStatus?: string; variables?: Array<{ name: string; example?: string; required?: boolean }> }) =>
request<ClientSmsTemplate>(`/admin/enterprise-templates/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
changeEnterpriseTemplateStatus: (id: string, status: string, reason?: string) =>
request<ClientSmsTemplate>(`/admin/enterprise-templates/${id}/status`, { method: 'POST', body: JSON.stringify({ status, reason }) }),
listEnterpriseCertifications: (query: { keyword?: string; status?: string }) => {
const params = new URLSearchParams();
if (query.keyword) params.set('keyword', query.keyword);
if (query.status && query.status !== 'all') params.set('status', query.status);
const suffix = params.toString() ? `?${params}` : '';
return request<EnterpriseCertification[]>(`/admin/enterprise-certifications${suffix}`);
},
getEnterpriseCertification: (id: string) => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}`),
approveEnterpriseCertification: (id: string) => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}/approve`, {
method: 'POST',
body: JSON.stringify({}),
}),
rejectEnterpriseCertification: (id: string, reason = '运营审核驳回') => request<EnterpriseCertification>(`/admin/enterprise-certifications/${id}/reject`, {
method: 'POST',
body: JSON.stringify({ reason }),
}),
listRiskReviewTasks: (query: { tenantId?: string; status?: string } = {}) => request<RiskReviewTask[]>(withQuery('/admin/risk-review/tasks', query)),
listRiskRules: (applicationId?: string) =>
request<RiskRuleItem[]>(withQuery('/admin/risk-review/rules', { applicationId })),
createRiskRule: (body: {
applicationId?: string;
code: RiskRuleItem['code'];
thresholdValue: number;
action: RiskRuleItem['action'];
status: RiskRuleItem['status'];
priority?: number;
config?: RiskRuleItem['config'];
}) => request<RiskRuleItem>('/admin/risk-review/rules', { method: 'POST', body: JSON.stringify(body) }),
updateRiskRule: (id: string, body: {
thresholdValue?: number;
action?: RiskRuleItem['action'];
status?: RiskRuleItem['status'];
priority?: number;
config?: RiskRuleItem['config'];
}) => request<RiskRuleItem>(`/admin/risk-review/rules/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
listPhoneFrequencyHits: (query: {
tenantId?: string;
applicationId?: string;
phoneNumber?: string;
status?: 'active' | 'expired' | 'released';
createdAtFrom?: string;
createdAtTo?: string;
page?: number;
pageSize?: number;
} = {}) => request<PagedResult<PhoneFrequencyHit>>(withQuery('/admin/risk-review/phone-frequency-hits', query)),
releasePhoneFrequencyHit: (id: string, reason: string) =>
request<PhoneFrequencyHit>(`/admin/risk-review/phone-frequency-hits/${id}/release`, {
method: 'POST',
body: JSON.stringify({ reason }),
}),
listPhoneFrequencyWhitelist: (query: {
phoneNumber?: string;
keyword?: string;
status?: 'active' | 'inactive' | 'deleted';
updatedAtFrom?: string;
updatedAtTo?: string;
page?: number;
pageSize?: number;
} = {}) => request<PagedResult<PhoneFrequencyWhitelistItem>>(withQuery('/admin/risk-review/phone-frequency-whitelist', query)),
createPhoneFrequencyWhitelist: (body: {
phoneNumber: string;
reason: string;
remark?: string;
status?: 'active' | 'inactive';
}) => request<PhoneFrequencyWhitelistItem>('/admin/risk-review/phone-frequency-whitelist', {
method: 'POST',
body: JSON.stringify(body),
}),
updatePhoneFrequencyWhitelist: (id: string, body: {
phoneNumber?: string;
reason?: string;
remark?: string;
status?: 'active' | 'inactive';
}) => request<PhoneFrequencyWhitelistItem>(`/admin/risk-review/phone-frequency-whitelist/${id}`, {
method: 'PUT',
body: JSON.stringify(body),
}),
deletePhoneFrequencyWhitelist: (id: string, reason: string) =>
request<PhoneFrequencyWhitelistItem>(`/admin/risk-review/phone-frequency-whitelist/${id}`, {
method: 'DELETE',
body: JSON.stringify({ reason }),
}),
listRiskReviewTaskMessages: (id: string, query: { phone?: string; page?: number; pageSize?: number } = {}) =>
request<RiskTaskMessagePage>(withQuery(`/admin/risk-review/tasks/${id}/messages`, query)),
approveRiskReviewTask: (id: string, reason?: string) =>
request<RiskReviewTask>(`/admin/risk-review/tasks/${id}/approve`, { method: 'POST', body: JSON.stringify({ reason }) }),
rejectRiskReviewTask: (id: string, reason?: string) =>
request<RiskReviewTask>(`/admin/risk-review/tasks/${id}/reject`, { method: 'POST', body: JSON.stringify({ reason }) }),
rejectRiskReviewTasks: (ids: string[], reason: string) =>
request<RiskReviewTask[]>('/admin/risk-review/tasks/batch/reject', { method: 'POST', body: JSON.stringify({ ids, reason }) }),
listSensitiveWords: (query: { keyword?: string; status?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/sensitive-words', query)),
createSensitiveWord: (body: { word: string; level?: string; status?: string }) =>
request<DictionaryItem>('/admin/dictionaries/sensitive-words', { method: 'POST', body: JSON.stringify(body) }),
deleteSensitiveWord: (id: string) => request<DictionaryItem>(`/admin/dictionaries/sensitive-words/${id}`, { method: 'DELETE' }),
listGlobalBlacklist: (query: { keyword?: string; status?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/blacklists/global', query)),
createGlobalBlacklist: (body: { phoneNumber: string; reason?: string; status?: string; operatorId?: string }) =>
request<DictionaryItem>('/admin/dictionaries/blacklists/global', { method: 'POST', body: JSON.stringify(body) }),
deleteGlobalBlacklist: (id: string) => request<DictionaryItem>(`/admin/dictionaries/blacklists/global/${id}`, { method: 'DELETE' }),
listEnterpriseBlacklist: (query: { tenantId?: string; applicationId?: string; keyword?: string; status?: string; enterpriseKeyword?: string; applicationKeyword?: string; phoneNumber?: string; reasonKeyword?: string } = {}) => request<DictionaryItem[]>(withQuery('/admin/dictionaries/blacklists/enterprise', query)),
createEnterpriseBlacklist: (body: { tenantId: string; applicationId: string; phoneNumber: string; reason?: string; status?: string; operatorId?: string }) =>
request<DictionaryItem>('/admin/dictionaries/blacklists/enterprise', { method: 'POST', body: JSON.stringify(body) }),
deleteEnterpriseBlacklist: (id: string) => request<DictionaryItem>(`/admin/dictionaries/blacklists/enterprise/${id}`, { method: 'DELETE' }),
listPhoneSegments: (query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
request<{ items: DictionaryItem[]; total: number; page: number; pageSize: number }>(withQuery('/admin/dictionaries/phone-segments', query)),
createPhoneSegment: (body: { prefix: string; carrier: string; province?: string; city?: string }) =>
request<DictionaryItem>('/admin/dictionaries/phone-segments', { method: 'POST', body: JSON.stringify(body) }),
deletePhoneSegment: (id: string) => request<DictionaryItem>(`/admin/dictionaries/phone-segments/${id}`, { method: 'DELETE' }),
listPhoneCarrierRules: (query: { keyword?: string; page?: number; pageSize?: number } = {}) =>
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) }),
deleteDrainageField: (id: string) => request<DictionaryItem>(`/admin/dictionaries/drainage-fields/${id}`, { method: 'DELETE' }),
listCommonReportFields: () => request<CommonReportField[]>('/admin/dictionaries/common-report-fields'),
createCommonReportField: (body: { drainageFieldId: string; reportType: 'signature' | 'drainage'; required: boolean; sortOrder?: number }) =>
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' }),
};
+76
View File
@@ -0,0 +1,76 @@
import { request, requestBlob, requestForm, withQuery } from '../core/httpClient';
import type { ApplicationCmppParams, ApplicationConnectionsResponse, ApplicationDeactivationPreview, ApplicationReportField, CaptchaResponse, EnterpriseApplication, HttpApiConfig, HttpApiConfigResponse, HttpWebhookEndpoint, ManagedUser, PagedResult, TenantManagementRow, TenantOption, UserPayload } from '../types';
import type { LoginSession } from '../session';
import { portalSessionApi } from './session.api';
// R1 domain fragment. src/api/adminApi.ts remains the public compatibility facade.
export const adminIdentityApi = {
getCaptcha: () => request<CaptchaResponse>('/admin/auth/captcha'),
login: (body: { login: string; password: string; captchaId: string; captchaText: string }) =>
request<LoginSession>('/admin/auth/login', { method: 'POST', body: JSON.stringify(body) }),
touchSession: () => portalSessionApi.touch('admin'),
lockSession: () => portalSessionApi.lock('admin'),
unlockSession: (password: string) => portalSessionApi.unlock('admin', password),
reauthenticate: (password: string) => portalSessionApi.reauthenticate('admin', password),
logout: () => portalSessionApi.logout('admin'),
changeOwnPassword: (body: { currentPassword: string; password: string }) =>
portalSessionApi.changeOwnPassword('admin', body),
listTenants: () => request<TenantOption[]>('/admin/tenants'),
listTenantManagementRows: () => request<TenantManagementRow[]>('/admin/tenants/management-list'),
getTenant: (id: string) => request<TenantOption>(`/admin/tenants/${id}`),
createTenant: (body: { name: string; code?: string; status?: string; creditCode?: string; province?: string; city?: string; address?: string; contactName?: string; contactIdCard?: string; contactPhone?: string; contactEmail?: string; photoFileObjectId?: string }) =>
request<TenantOption>('/admin/tenants', { method: 'POST', body: JSON.stringify(body) }),
updateTenant: (id: string, body: { name?: string; code?: string; status?: string; creditCode?: string; province?: string; city?: string; address?: string; contactName?: string; contactIdCard?: string; contactPhone?: string; contactEmail?: string; photoFileObjectId?: string }) =>
request<TenantOption>(`/admin/tenants/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
changeTenantStatus: (id: string, status: string) =>
request<TenantOption>(`/admin/tenants/${id}/status`, { method: 'POST', body: JSON.stringify({ status }) }),
deleteTenant: (id: string) => request<TenantOption>(`/admin/tenants/${id}`, { method: 'DELETE' }),
listUsers: (query: { tenantId?: string; roleCode?: string; displayName?: string; login?: string; status?: string } = {}) =>
request<ManagedUser[]>(withQuery('/admin/users', query)),
createUser: (body: UserPayload) => request<ManagedUser>('/admin/users', { method: 'POST', body: JSON.stringify(body) }),
updateUser: (id: string, body: Omit<UserPayload, 'password'>) => request<ManagedUser>(`/admin/users/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
changeUserStatus: (id: string, status: string, operatorId?: string) =>
request<ManagedUser>(`/admin/users/${id}/status`, { method: 'POST', body: JSON.stringify({ status, operatorId }) }),
deleteUser: (id: string, operatorId?: string) => request<ManagedUser>(`/admin/users/${id}`, { method: 'DELETE', body: JSON.stringify({ operatorId }) }),
changeUserPassword: (id: string, password: string, operatorId?: string) =>
request<ManagedUser>(`/admin/users/${id}/password`, { method: 'POST', body: JSON.stringify({ password, operatorId }) }),
listEnterpriseApplications: (query: { tenantId?: string; keyword?: string; enterpriseKeyword?: string; applicationKeyword?: string; status?: string } = {}) =>
request<EnterpriseApplication[]>(withQuery('/admin/enterprise-applications', query)),
listEnterpriseApplicationsPage: (query: { tenantId?: string; keyword?: string; enterpriseKeyword?: string; applicationKeyword?: string; status?: string; page: number; pageSize: number }) =>
request<PagedResult<EnterpriseApplication>>(withQuery('/admin/enterprise-applications', query)),
listEnterpriseApplicationOptions: (query: { tenantId?: string } = {}) =>
request<EnterpriseApplication[]>(withQuery('/admin/enterprise-application-options', query)),
getEnterpriseApplication: (id: string) =>
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}`),
createEnterpriseApplication: (body: { tenantId: string; name: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; templateMismatchMode?: string; downstreamReceiptRetryEnabled?: boolean; downstreamUplinkRetryEnabled?: boolean; cmppAccount?: string; cmppEnterpriseCode?: string; cmppApplicationExtension?: string; cmppAccessNumberFillEnabled?: boolean; cmppAccessNumberFillPrefix?: string; passwordCipher?: string; interfaceEnabled?: boolean; interfaceType?: 'cmpp20'; cmppMaxConnections?: number; cmppWindowSize?: number; ipAllowlist?: string[] }) =>
request<EnterpriseApplication>('/admin/enterprise-applications', { method: 'POST', body: JSON.stringify(body) }),
updateEnterpriseApplication: (id: string, body: { name?: string; scene?: string; dailyLimit?: number; customerUnitPrice?: number; queuePriority?: 'normal' | 'priority'; templateMismatchMode?: string; downstreamReceiptRetryEnabled?: boolean; downstreamUplinkRetryEnabled?: boolean; cmppAccount?: string; cmppEnterpriseCode?: string; cmppApplicationExtension?: string; cmppAccessNumberFillEnabled?: boolean; cmppAccessNumberFillPrefix?: string; passwordCipher?: string; interfaceEnabled?: boolean; interfaceType?: 'cmpp20'; cmppMaxConnections?: number; cmppWindowSize?: number; ipAllowlist?: string[] }) =>
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}`, { method: 'PUT', body: JSON.stringify(body) }),
getApplicationDeactivationPreview: (id: string) =>
request<ApplicationDeactivationPreview>(`/admin/enterprise-applications/${id}/deactivation-preview`),
changeApplicationStatus: (id: string, status: string, reason?: string, force = false) =>
request<EnterpriseApplication>(`/admin/enterprise-applications/${id}/status`, {
method: 'POST',
body: JSON.stringify({ status, reason, force }),
}),
listApplicationConnections: (applicationId: string) =>
request<ApplicationConnectionsResponse>(`/admin/enterprise-applications/${applicationId}/connections`),
listApplicationReportFields: (applicationId: string, reportType?: 'signature' | 'drainage') =>
request<ApplicationReportField[]>(withQuery(`/admin/enterprise-applications/${applicationId}/report-fields`, { reportType })),
listCommonApplicationReportFields: (reportType: 'signature' | 'drainage') =>
request<ApplicationReportField[]>(withQuery('/admin/report-fields/common', { reportType })),
getApplicationCmppParams: (applicationId: string) =>
request<ApplicationCmppParams>(`/admin/enterprise-applications/${applicationId}/cmpp-params`),
getApplicationHttpApiConfig: (applicationId: string) => request<HttpApiConfigResponse>(`/admin/enterprise-applications/${applicationId}/http-api`),
updateApplicationHttpApiConfig: (applicationId: string, body: Partial<HttpApiConfig> & { ipAllowlist?: string[] }) => request<HttpApiConfigResponse>(`/admin/enterprise-applications/${applicationId}/http-api`, { method: 'PUT', body: JSON.stringify(body) }),
listApplicationHttpWebhooks: (applicationId: string) =>
request<HttpWebhookEndpoint[]>(`/admin/enterprise-applications/${applicationId}/http-api/webhooks`),
saveApplicationHttpWebhook: (
applicationId: string,
eventType: 'receipt' | 'uplink',
body: { url: string; rotateSecret?: boolean; status?: string },
) => request<HttpWebhookEndpoint>(`/admin/enterprise-applications/${applicationId}/http-api/webhooks/${eventType}`, {
method: 'PUT',
body: JSON.stringify(body),
}),
};
+70
View File
@@ -0,0 +1,70 @@
import { request, requestBlob, requestForm, withQuery } from '../core/httpClient';
import type { BatchRequeueResponse, BatchTaskMessagePage, DailyProfitReport, DailyQualityReport, DailyReconciliationReport, DashboardResponse, DownstreamDeliveryDashboard, DownstreamDeliveryRecord, DownstreamRecoveryStatusExportQuery, DownstreamRecoveryStatusResponse, GatewayDownstreamRecoveryStatus, GatewaySubmitException, GatewaySubmitExceptionResponse, OperationLogResponse, PagedResponse, PagedResult, ProtocolInteractionLogResponse, SendQualityResponse, SignatureChannelQualityResponse, SmsBatchTask, SmsMessageRecord, SmsMessageSegmentAudit, SmsUplinkMessage, SystemLogExportResult } from '../types';
// Read-heavy operations endpoints are isolated from configuration mutations.
export const adminOperationsApi = {
getDashboard: (tenantId?: string) => request<DashboardResponse>(withQuery('/admin/operations/dashboard/statistics', { tenantId })),
getSendQuality: (date?: string) => request<SendQualityResponse>(withQuery('/admin/operations/send-quality', { date })),
getSignatureQuality: (query: { date?: string; keyword?: string; page?: number; pageSize?: number } = {}) =>
request<SignatureChannelQualityResponse>(withQuery('/admin/operations/signature-quality', query)),
listSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }) =>
request<OperationLogResponse>(withQuery('/admin/system-logs', query)),
listProtocolInteractionLogs: (query: { protocol?: string; direction?: string; eventType?: string; status?: string; keyword?: string; range?: string; page?: number; pageSize?: number }) =>
request<ProtocolInteractionLogResponse>(withQuery('/admin/system-logs/protocol-interactions', query)),
exportSystemLogs: (query: { tenantId?: string; keyword?: string; level?: string; module?: string; range?: string }) =>
request<SystemLogExportResult>('/admin/system-logs/exports', { method: 'POST', body: JSON.stringify(query) }),
listReconciliationReports: (query: { dateFrom?: string; dateTo?: string; tenantId?: string; applicationId?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResponse<DailyReconciliationReport>>(withQuery('/admin/reports/reconciliation', query)),
exportReconciliationReports: (query: { dateFrom?: string; dateTo?: string; tenantId?: string; applicationId?: string } = {}) =>
requestBlob(withQuery('/admin/reports/reconciliation/export', query)),
listProfitReports: (query: { dateFrom?: string; dateTo?: string; dimensionType?: 'application' | 'channel'; tenantId?: string; applicationId?: string; channelId?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResponse<DailyProfitReport> & { dimensionType: 'application' | 'channel' }>(withQuery('/admin/reports/profit', query)),
exportProfitReports: (query: { dateFrom?: string; dateTo?: string; dimensionType?: 'application' | 'channel'; tenantId?: string; applicationId?: string; channelId?: string } = {}) =>
requestBlob(withQuery('/admin/reports/profit/export', query)),
listQualityReports: (query: { dateFrom?: string; dateTo?: string; dimensionType?: 'application' | 'channel' | 'signature' | 'drainage'; tenantId?: string; applicationId?: string; channelId?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResponse<DailyQualityReport> & { dimensionType: DailyQualityReport['dimensionType'] }>(withQuery('/admin/reports/quality', query)),
exportQualityReports: (query: { dateFrom?: string; dateTo?: string; dimensionType?: 'application' | 'channel' | 'signature' | 'drainage'; tenantId?: string; applicationId?: string; channelId?: string } = {}) =>
requestBlob(withQuery('/admin/reports/quality/export', query)),
listAdminBatchTasks: (query: { tenantId?: string; status?: string } = {}) =>
request<SmsBatchTask[]>(withQuery('/admin/send/batch-tasks', query)),
listAdminBatchTasksPage: (query: { tenantId?: string; status?: string; keyword?: string; enterpriseKeyword?: string; applicationKeyword?: string; createdAtFrom?: string; createdAtTo?: string; page: number; pageSize: number }) =>
request<PagedResult<SmsBatchTask>>(withQuery('/admin/send/batch-tasks', query)),
listAdminBatchTaskMessages: (id: string, query: { phone?: string; page?: number; pageSize?: number } = {}) =>
request<BatchTaskMessagePage>(withQuery(`/admin/send/batch-tasks/${id}/messages`, query)),
terminateAdminBatchTask: (id: string) =>
request<SmsBatchTask>(`/admin/send/batch-tasks/${id}/terminate`, { method: 'POST', body: JSON.stringify({}) }),
listAdminMessages: (query: { tenantId?: string; applicationId?: string; channelId?: string; taskId?: string; phoneNumber?: string; status?: string } = {}) =>
request<SmsMessageRecord[]>(withQuery('/admin/send/messages', query)),
listMessageSegmentAudits: (query: { messageId?: string; messageRecordId?: string }) =>
request<SmsMessageSegmentAudit[]>(withQuery('/admin/operations/message-segment-audits', query)),
listOperationMessages: (query: { tenantId?: string; applicationId?: string; channelId?: string; channelKeyword?: string; taskId?: string; messageId?: string; phoneNumber?: string; contentKeyword?: string; carrier?: string; status?: string; queuedAtFrom?: string; queuedAtTo?: string; page?: number; pageSize?: number } = {}) =>
request<PagedResult<SmsMessageRecord>>(withQuery('/admin/operations/messages', query)),
exportOperationMessages: (query: { tenantId?: string; applicationId?: string; channelId?: string; channelKeyword?: string; phoneNumber?: string; contentKeyword?: string; carrier?: string; status?: string; queuedAtFrom?: string; queuedAtTo?: string } = {}) =>
requestBlob(withQuery('/admin/operations/messages/export', query)),
listAdminUplinkMessages: (query: { tenantId?: string; channelId?: string } = {}) =>
request<SmsUplinkMessage[]>(withQuery('/admin/operations/uplink-messages', query)),
listAdminUplinkMessagesPage: (query: { tenantId?: string; channelId?: string; phoneNumber?: string; keyword?: string; startTime?: string; endTime?: string; page: number; pageSize: number }) =>
request<PagedResult<SmsUplinkMessage>>(withQuery('/admin/operations/uplink-messages', query)),
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)),
listDownstreamRecoveryStatuses: (query: { tenantId?: string; applicationId?: string; state?: string; failureCategory?: string; keyword?: string; updatedAtFrom?: string; updatedAtTo?: string; page?: number; pageSize?: number } = {}) =>
request<DownstreamRecoveryStatusResponse>(withQuery('/admin/operations/downstream-recovery-statuses', query)),
getDownstreamRecoveryStatus: (id: string) =>
request<GatewayDownstreamRecoveryStatus>(`/admin/operations/downstream-recovery-statuses/${id}`),
exportDownstreamRecoveryStatuses: (query: DownstreamRecoveryStatusExportQuery = {}) =>
requestBlob(withQuery('/admin/operations/downstream-recovery-statuses/export', query)),
listDownstreamDeliveries: (query: { tenantId?: string; applicationId?: string; deliveryType?: string; status?: string; keyword?: string; page?: number; pageSize?: number; createdAtFrom?: string; createdAtTo?: string } = {}) =>
request<PagedResponse<DownstreamDeliveryRecord>>(withQuery('/admin/operations/downstream-deliveries', query)),
requeueDownstreamDelivery: (id: string) =>
request<DownstreamDeliveryRecord>(`/admin/operations/downstream-deliveries/${id}/requeue`, { method: 'POST', body: JSON.stringify({}) }),
batchRequeueDownstreamDeliveries: (ids: string[]) =>
request<BatchRequeueResponse>('/admin/operations/downstream-deliveries/requeue', { method: 'POST', body: JSON.stringify({ ids }) }),
};
+14
View File
@@ -0,0 +1,14 @@
import { request, type SessionTiming } from '../core/httpClient';
import type { ManagedUser } from '../types';
import type { LoginSession, Portal } from '../session';
export const portalSessionApi = {
current: (portal: Portal) => request<LoginSession>(`/${portal}/auth/session`, { suppressSessionRedirect: true }),
touch: (portal: Portal) => request<SessionTiming>(`/${portal}/auth/session/touch`, { method: 'POST', body: '{}' }),
lock: (portal: Portal) => request<{ locked: boolean }>(`/${portal}/auth/session/lock`, { method: 'POST', body: '{}' }),
unlock: (portal: Portal, password: string) => request<SessionTiming>(`/${portal}/auth/session/unlock`, { method: 'POST', body: JSON.stringify({ password }) }),
reauthenticate: (portal: Portal, password: string) => request<Pick<LoginSession, 'recentAuthenticationExpiresAt'>>(`/${portal}/auth/reauthenticate`, { method: 'POST', body: JSON.stringify({ password }), reauthenticationAttempted: true }),
logout: (portal: Portal) => request<{ success: boolean }>(`/${portal}/auth/logout`, { method: 'POST', body: '{}' }),
changeOwnPassword: (portal: Portal, body: { currentPassword: string; password: string }) =>
request<ManagedUser>(`/${portal}/auth/password`, { method: 'POST', body: JSON.stringify(body) }),
};
+16 -2204
View File
File diff suppressed because it is too large Load Diff
+162
View File
@@ -0,0 +1,162 @@
import { request, requestBlob, requestForm, withQuery } from '../core/httpClient';
import type { ApplicationCmppParams, CaptchaResponse, ClientApplicationReportField, ClientSignatureWorkspace, ClientSmsApplication, ClientSmsSignatureView, ClientSmsTemplate, DashboardResponse, DeleteTargetRequest, DeletionPreflight, DeletionResult, DeletionTargetType, EnterpriseCertification, FileObject, HttpApiConfigResponse, HttpApiCredential, HttpApiRequestLog, HttpWebhookDelivery, HttpWebhookEndpoint, ImportPreviewResponse, ManagedUser, OperationLogResponse, PagedResult, RechargeOrder, SmsBatchTask, SmsDrainageInfo, SmsMessageRecord, SmsUplinkMessage, SystemLogExportResult, UserPayload } from '../types';
import { assertUploadFileSize } from '@/utils/fileUpload';
import type { LoginSession } from '../session';
import { clearSession, dispatchSessionEvent, getSessionTenantId, hasRecentUserActivity, readSession, redirectToPortalLogin } from '../session';
import { readErrorBody } from '../core/httpClient';
import { DEFAULT_CLIENT_TENANT_ID } from '../types';
// Client methods moved intact during R1; tenant and session behavior still flows
// through the shared HTTP client and the existing upload path below.
export const clientApi = {
getCaptcha: () => request<CaptchaResponse>('/client/auth/captcha'),
login: (body: { login: string; password: string; captchaId: string; captchaText: string }) =>
request<LoginSession>('/client/auth/login', { method: 'POST', body: JSON.stringify(body) }),
listUsers: (
query: { displayName?: string; login?: string; status?: string } = {},
tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID,
) => request<ManagedUser[]>(withQuery('/client/users', query), { tenantId }),
createUser: (body: UserPayload, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ManagedUser>('/client/users', { method: 'POST', tenantId, body: JSON.stringify(body) }),
updateUser: (id: string, body: Omit<UserPayload, 'password'>, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ManagedUser>(`/client/users/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
changeUserStatus: (id: string, status: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ManagedUser>(`/client/users/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status, operatorId }) }),
deleteUser: (id: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ManagedUser>(`/client/users/${id}`, { method: 'DELETE', tenantId, body: JSON.stringify({ operatorId }) }),
changeUserPassword: (id: string, password: string, operatorId?: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ManagedUser>(`/client/users/${id}/password`, { method: 'POST', tenantId, body: JSON.stringify({ password, operatorId }) }),
getDashboard: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<DashboardResponse>('/client/operations/dashboard', { tenantId }),
listEnterpriseCertifications: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<EnterpriseCertification[]>('/client/enterprise-certification', { tenantId }),
submitEnterpriseCertification: (body: { companyName: string; licenseNo?: string; contactName?: string; contactPhone?: string; materials?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<EnterpriseCertification>('/client/enterprise-certification', {
method: 'POST',
tenantId,
body: JSON.stringify({ ...body, tenantId }),
}),
listSystemLogs: (query: { keyword?: string; level?: string; module?: string; range?: string; page?: number; pageSize?: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<OperationLogResponse>(withQuery('/client/operations/system-logs', query), { tenantId }),
exportSystemLogs: (query: { keyword?: string; level?: string; module?: string; range?: string }) =>
request<SystemLogExportResult>('/client/operations/system-logs/exports', { method: 'POST', body: JSON.stringify(query) }),
listOrders: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<RechargeOrder[]>('/client/billing/orders', { tenantId }),
listOrdersPage: (query: { page: number; pageSize: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<PagedResult<RechargeOrder>>(withQuery('/client/billing/orders', query), { tenantId }),
listApplications: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsApplication[]>('/client/applications', { tenantId }),
listApplicationsPage: (query: { page: number; pageSize: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<PagedResult<ClientSmsApplication>>(withQuery('/client/applications', query), { tenantId }),
listApplicationOptions: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsApplication[]>('/client/application-options', { tenantId }),
getApplicationCmppParams: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ApplicationCmppParams>(`/client/applications/${applicationId}/cmpp-params`, { tenantId }),
getApplicationHttpApiConfig: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiConfigResponse>(`/client/applications/${applicationId}/http-api`, { tenantId }),
listHttpApiCredentials: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiCredential[]>(`/client/applications/${applicationId}/http-api/credentials`, { tenantId }),
createHttpApiCredential: (applicationId: string, body: { name?: string; expiresAt?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiCredential>(`/client/applications/${applicationId}/http-api/credentials`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
revokeHttpApiCredential: (applicationId: string, credentialId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<{ id: string; status: string }>(`/client/applications/${applicationId}/http-api/credentials/${credentialId}/revoke`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
listHttpWebhooks: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookEndpoint[]>(`/client/applications/${applicationId}/http-api/webhooks`, { tenantId }),
saveHttpWebhook: (applicationId: string, eventType: 'receipt' | 'uplink', body: { url: string; rotateSecret?: boolean; status?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookEndpoint>(`/client/applications/${applicationId}/http-api/webhooks/${eventType}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
listHttpApiRequests: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpApiRequestLog[]>(`/client/applications/${applicationId}/http-api/requests`, { tenantId }),
listHttpWebhookDeliveries: (applicationId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<HttpWebhookDelivery[]>(`/client/applications/${applicationId}/http-api/webhook-deliveries`, { tenantId }),
retryHttpWebhookDelivery: (applicationId: string, deliveryId: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) => request<{ id: string; status: string }>(`/client/applications/${applicationId}/http-api/webhook-deliveries/${deliveryId}/retry`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
listApplicationReportFields: (applicationId: string, reportType: 'signature' | 'drainage' = 'drainage', tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientApplicationReportField[]>(withQuery(`/client/applications/${applicationId}/report-fields`, { reportType }), { tenantId }),
listCommonApplicationReportFields: (reportType: 'signature' | 'drainage', tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientApplicationReportField[]>(withQuery('/client/report-fields/common', { reportType }), { tenantId }),
listSignatures: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsSignatureView[]>('/client/signatures', { tenantId }),
listSignatureOptions: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsSignatureView[]>('/client/signature-options', { tenantId }),
getSignatureWorkspace: (query: { keyword?: string; applicationId?: string; status?: string; page?: number; pageSize?: number } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSignatureWorkspace>(withQuery('/client/signatures-workspace', query), { tenantId }),
createSignature: (body: { tenantId?: string; applicationId?: string; name: string; purpose?: string; drainageInfo?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsSignatureView>('/client/signatures', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
updateSignature: (id: string, body: { applicationId?: string; name?: string; purpose?: string; drainageInfo?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsSignatureView>(`/client/signatures/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
submitSignature: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsSignatureView>(`/client/signatures/${id}/submit`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
changeSignatureStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsSignatureView>(`/client/signatures/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
createSignatureMaterial: (id: string, body: { fileObjectId?: string; materialType: string; title: string; description?: string }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<Record<string, unknown>>(`/client/signatures/${id}/materials`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
createDrainageInfo: (signatureId: string, body: { siteName: string; url: string; remark?: string; reportValues?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<SmsDrainageInfo>(`/client/signatures/${signatureId}/drainage-infos`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
updateDrainageInfo: (id: string, body: { siteName?: string; url?: string; remark?: string; reportValues?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
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; 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 }),
listTemplatesPage: (query: { keyword?: string; includeHistory?: boolean; page: number; pageSize: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<PagedResult<ClientSmsTemplate>>(withQuery('/client/templates', {
keyword: query.keyword,
includeHistory: query.includeHistory === undefined ? undefined : String(query.includeHistory),
page: query.page,
pageSize: query.pageSize,
}), { 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) =>
request<ClientSmsTemplate>(`/client/templates/${id}`, { method: 'PUT', tenantId, body: JSON.stringify(body) }),
submitTemplate: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsTemplate>(`/client/templates/${id}/submit`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
changeTemplateStatus: (id: string, status: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ClientSmsTemplate>(`/client/templates/${id}/status`, { method: 'POST', tenantId, body: JSON.stringify({ status }) }),
getDeletionPreflight: (type: Exclude<DeletionTargetType, 'channel'>, id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<DeletionPreflight>(`/client/deletions/${type}/${id}/preflight`, { tenantId }),
deleteGovernedTarget: (type: Exclude<DeletionTargetType, 'channel'>, id: string, body: DeleteTargetRequest, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<DeletionResult>(`/client/deletions/${type}/${id}`, { method: 'POST', tenantId, body: JSON.stringify(body) }),
listBatchTasks: (query: { status?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<SmsBatchTask[]>(withQuery('/client/send/batch-tasks', query), { tenantId }),
listBatchTasksPage: (query: { status?: string; keyword?: string; applicationKeyword?: string; createdAtFrom?: string; createdAtTo?: string; page: number; pageSize: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<PagedResult<SmsBatchTask>>(withQuery('/client/send/batch-tasks', query), { tenantId }),
cancelBatchTask: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<SmsBatchTask>(`/client/send/batch-tasks/${id}/cancel`, { method: 'POST', tenantId, body: JSON.stringify({}) }),
createBatchTask: (body: { applicationId?: string; templateId?: string; content: string; category?: string; phones: string[]; sendMode?: 'immediate' | 'scheduled'; scheduledAt?: string; variables?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<SmsBatchTask>('/client/send/batch-tasks', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
previewImport: (body: { applicationId?: string; content: string; fileName?: string; delimiter?: ',' | '\t'; requiredVariables?: string[] }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ImportPreviewResponse>('/client/send/imports/preview', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
confirmImport: (body: { applicationId?: string; templateId?: string; content: string; category?: string; importContent: string; sendMode?: 'immediate' | 'scheduled'; scheduledAt?: string; requiredVariables?: string[]; variables?: Record<string, unknown> }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<SmsBatchTask>('/client/send/imports/confirm', { method: 'POST', tenantId, body: JSON.stringify({ ...body, tenantId }) }),
listBatchTaskMessages: (id: string, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<SmsMessageRecord[]>(`/client/send/batch-tasks/${id}/messages`, { tenantId }),
listMessages: (query: { applicationId?: string; taskId?: string; messageId?: string; phoneNumber?: string; contentKeyword?: string; status?: string; queuedAtFrom?: string; queuedAtTo?: string; page?: number; pageSize?: number } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<PagedResult<SmsMessageRecord>>(withQuery('/client/operations/messages', query), { tenantId }),
listUplinkMessages: (query: { channelId?: string; applicationId?: string; phoneNumber?: string; keyword?: string; startTime?: string; endTime?: string } = {}, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<SmsUplinkMessage[]>(withQuery('/client/operations/uplink-messages', query), { tenantId }),
listUplinkMessagesPage: (query: { channelId?: string; applicationId?: string; phoneNumber?: string; keyword?: string; startTime?: string; endTime?: string; page: number; pageSize: number }, tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<PagedResult<SmsUplinkMessage>>(withQuery('/client/operations/uplink-messages', query), { tenantId }),
uploadFileObject: async (file: File, body: { purpose: string; prefix?: string }) => {
assertUploadFileSize(file);
const form = new FormData();
form.set('file', file);
form.set('purpose', body.purpose);
if (body.prefix) {
form.set('prefix', body.prefix);
}
const headers = new Headers();
const session = readSession('client');
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
const response = await fetch('/api/client/files/upload', { method: 'POST', headers, body: form, credentials: 'same-origin' });
if (response.status === 401 && session) {
const error = await readErrorBody(response.clone());
if (error.code === 'SESSION_LOCKED') {
dispatchSessionEvent('client', 'locked', { message: error.message });
} else {
clearSession('client');
dispatchSessionEvent('client', 'logout', { code: error.code, message: error.message });
redirectToPortalLogin('client');
}
}
if (!response.ok) {
throw new Error(await response.text());
}
return response.json() as Promise<FileObject>;
},
};
+174
View File
@@ -0,0 +1,174 @@
import {
clearSession,
currentRouteForPortal,
dispatchSessionEvent,
getSessionTenantId,
hasRecentUserActivity,
portalFromPath,
readSession,
redirectToPortalLogin,
requestReauthentication,
saveSessionRecovery,
type LoginSession,
type Portal,
} from '../session';
type RequestOptions = RequestInit & {
tenantId?: string;
reauthenticationAttempted?: boolean;
suppressSessionRedirect?: boolean;
};
type ApiErrorBody = { message?: string | string[]; error?: string; code?: string };
export async function readErrorBody(response: Response): Promise<ApiErrorBody> {
const text = await response.text();
if (!text) return {};
try {
return JSON.parse(text) as ApiErrorBody;
} catch {
return { message: text };
}
}
export type SessionTiming = Pick<LoginSession, 'idleTimeoutSeconds' | 'lockRecoverySeconds' | 'absoluteExpiresAt' | 'lastActivityAt' | 'recentAuthenticationExpiresAt'>;
// Authentication failures are handled centrally so every domain API keeps the
// same lock, recovery and redirect behavior as the original adminApi facade.
function requestPortal(path: string): Portal | undefined {
return portalFromPath(path);
}
async function handleSessionFailure(response: Response, portal: Portal | undefined, suppressRedirect = false) {
if (!portal) return false;
const session = readSession(portal);
const body = await readErrorBody(response.clone());
if (body.code === 'SESSION_LOCKED' && session) {
dispatchSessionEvent(portal, 'locked', { message: body.message });
throw new Error(typeof body.message === 'string' ? body.message : '由于长时间未操作,会话已安全锁定');
}
if (suppressRedirect) return false;
if (session) {
saveSessionRecovery(portal, {
returnUrl: currentRouteForPortal(portal),
code: body.code,
message: typeof body.message === 'string' ? body.message : '登录会话已失效,请重新登录',
});
}
clearSession(portal);
dispatchSessionEvent(portal, 'logout', { code: body.code, message: body.message });
redirectToPortalLogin(portal);
throw new Error('登录会话已失效,请重新登录');
}
async function readErrorMessage(response: Response) {
const fallback = `请求失败(${response.status}`;
const text = await response.text();
if (!text) return fallback;
try {
const parsed = JSON.parse(text) as { message?: string | string[]; error?: string };
if (Array.isArray(parsed.message)) return parsed.message.join('');
if (parsed.message) return parsed.message;
if (parsed.error) return parsed.error;
} catch {
return text;
}
return text;
}
export async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const headers = new Headers(options.headers);
headers.set('Content-Type', 'application/json');
const portal = requestPortal(path);
const session = portal ? readSession(portal) : null;
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
const tenantId = options.tenantId ?? (path.startsWith('/client') ? getSessionTenantId() : undefined);
if (tenantId) {
headers.set('x-tenant-id', tenantId);
}
const response = await fetch(`/api${path}`, { ...options, headers, credentials: 'same-origin' });
const isLoginAttempt = path === '/admin/auth/login' || path === '/client/auth/login';
if (response.status === 401 && !isLoginAttempt) {
await handleSessionFailure(response, portal, options.suppressSessionRedirect);
}
if (response.status === 403 && session && !options.reauthenticationAttempted) {
const body = await readErrorBody(response.clone());
if (body.code === 'RECENT_AUTHENTICATION_REQUIRED') {
await requestReauthentication();
return request<T>(path, { ...options, reauthenticationAttempted: true });
}
}
if (!response.ok) {
throw new Error(await readErrorMessage(response));
}
return response.json() as Promise<T>;
}
export async function requestBlob(path: string, options: RequestOptions = {}): Promise<Blob> {
const headers = new Headers(options.headers);
const portal = requestPortal(path);
const session = portal ? readSession(portal) : null;
if (session && hasRecentUserActivity()) headers.set('x-session-activity', 'user');
const tenantId = options.tenantId ?? (path.startsWith('/client') ? getSessionTenantId() : undefined);
if (tenantId) {
headers.set('x-tenant-id', tenantId);
}
const response = await fetch(`/api${path}`, { ...options, headers, credentials: 'same-origin' });
if (response.status === 401) {
await handleSessionFailure(response, portal, options.suppressSessionRedirect);
}
if (response.status === 403 && session && !options.reauthenticationAttempted) {
const body = await readErrorBody(response.clone());
if (body.code === 'RECENT_AUTHENTICATION_REQUIRED') {
await requestReauthentication();
return requestBlob(path, { ...options, reauthenticationAttempted: true });
}
}
if (!response.ok) {
throw new Error(await readErrorMessage(response));
}
return response.blob();
}
export async function requestForm<T>(path: string, form: FormData, reauthenticationAttempted = false): Promise<T> {
const headers = new Headers();
const portal = requestPortal(path);
const session = portal ? readSession(portal) : null;
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) {
await handleSessionFailure(response, portal);
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 function withQuery(path: string, query: Record<string, string | number | undefined>) {
const params = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value !== undefined && value !== '' && value !== 'all') {
params.set(key, String(value));
}
});
const suffix = params.toString() ? `?${params}` : '';
return `${path}${suffix}`;
}
export function fileDownloadUrl(fileObjectId: string, disposition: 'attachment' | 'inline' = 'attachment', portal: Portal = 'admin') {
return `/api/${portal}/files/${encodeURIComponent(fileObjectId)}/download?disposition=${disposition}`;
}
+310
View File
@@ -0,0 +1,310 @@
// R1 compatibility types. Keep public names re-exported from src/api/adminApi.ts.
import type { ClientSmsApplication, CmppConnectionState, SmsDrainageInfo, TenantOption } from './identity-config';
export type AdminChannel = {
id: string;
code: string;
name: string;
carrier?: string | null;
sendRegion?: string | null;
gatewayHost: string;
gatewayPort: number;
enterpriseCode?: string | null;
account: string;
srcId: string;
cmppVersion?: '2.0' | '3.0' | string | null;
rateLimitPerSecond: number;
unitPrice: number;
status: string;
config?: { desiredConnections?: number; windowSize?: number; extensionDigits?: number; heartbeatIntervalSeconds?: number; heartbeatMissThreshold?: number; [key: string]: unknown } | null;
connectionStates?: CmppConnectionState[];
};
export type ChannelConnectionLogResponse = {
channelId: string;
connectionStates: CmppConnectionState[];
logs: Array<{
id: string;
time: string;
event: string;
action: string;
resourceId?: string;
detail?: unknown;
}>;
};
export type ChannelTestResponse = {
channelId: string;
status: string;
testNo: string;
submitted: number;
messages: Array<{
phoneNumber: string;
messageRecordId: string;
submitId: string;
streamMessageId?: string;
}>;
queuedAt: string;
};
export type DictionaryItem = Record<string, unknown> & {
id: string;
status?: string;
createdAt?: string;
updatedAt?: string;
};
export type ChannelGroup = DictionaryItem & {
code: string;
name: string;
carrier: 'mobile' | 'unicom' | 'telecom';
description?: string | null;
retryEnabled?: boolean;
retryTimeLimitHours?: number;
retryTimeLimitMinutes?: number;
items?: ChannelGroupItem[];
};
export type ChannelGroupItem = DictionaryItem & {
groupId: string;
channelId: string;
carrier?: 'mobile' | 'unicom' | 'telecom' | null;
province?: string | null;
priority: number;
weight?: number;
isBackup?: boolean;
channel?: AdminChannel;
};
export type ChannelReportField = DictionaryItem & {
channelId: string;
drainageFieldId?: string | null;
reportType?: 'signature' | 'drainage' | 'both';
code: string;
name: string;
fieldType: string;
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 ReportMaterialBatch = {
id: string;
batchNo: string;
status: string;
selectedCount: number;
channelCount: number;
fileCount: number;
reportTotal: number;
successCount: number;
successRate: number;
createdAt: string;
completedAt?: string | null;
exportFiles: Array<{ id: string; fileObjectId?: string | null; fileName: string; rowCount: number; channelId?: string | null }>;
};
export type ReportImportReviewItem = {
id: string;
rowNumber: number;
reportType: 'signature' | 'drainage';
operation: 'create' | 'update' | 'invalid';
targetId?: string | null;
status: string;
payload: Record<string, unknown>;
originalSnapshot?: Record<string, unknown> | null;
errorMessage?: string | null;
reviewReason?: string | null;
reviewedAt?: string | null;
reviewer?: { id: string; username: string; displayName: string } | null;
};
export type ReportImportReviewBatch = {
id: string;
tenantId: string;
applicationId?: string | null;
fileName: string;
reportType: 'signature' | 'drainage';
status: string;
rowCount: number;
successCount: number;
failedCount: number;
createdAt: string;
reviewedAt?: string | null;
tenant?: { id: string; name: string } | null;
application?: { id: string; name: string } | null;
reviewer?: { id: string; username: string; displayName: string } | null;
items: ReportImportReviewItem[];
};
export type ReportMaterialPreflightTarget = {
id: string;
name: string;
carrier: string;
businessKey: string;
eligible: boolean;
blockedReasons: string[];
duplicateBatchId?: string;
};
export type ReportMaterialPreflightItem = {
id: string;
reportType: 'signature' | 'drainage';
signatureId: string;
drainageItemId?: string;
materialVersion: number;
name: string;
tenantName: string;
applicationId?: string;
applicationName: string;
eligible: boolean;
blockedReasons: string[];
targets: ReportMaterialPreflightTarget[];
};
export type ReportMaterialBatchPreflight = {
checkedAt: string;
eligible: boolean;
eligibleItemCount: number;
blockedItemCount: number;
eligibleTargetCount: number;
skippedTargetCount: number;
items: ReportMaterialPreflightItem[];
};
export type ReportMaterialBatchResult = Record<string, unknown> & {
id: string;
batchNo: string;
status: string;
operationId: string;
replayed: boolean;
result: { successCount: number; skippedCount: number; failedCount: number; items: ReportMaterialPreflightItem[] };
};
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;
name: string;
fieldType: string;
required: boolean;
description?: string | null;
reportTypes: string[];
commonReportTypes?: Array<'signature' | 'drainage'>;
channels: Array<{ id: string; code: string; name: string; groupId: string; groupName: string; required: boolean; reportType: 'signature' | 'drainage' | 'both'; source?: 'common' | 'channel' | 'both' }>;
};
export type ClientApplicationReportField = Omit<ApplicationReportField, 'channels' | 'commonReportTypes'>;
export type CommonReportField = DictionaryItem & {
drainageFieldId: string;
reportType: 'signature' | 'drainage';
required: boolean;
sortOrder: number;
drainageField: DictionaryItem & { code?: string; name?: string; fieldType?: string; description?: string | null };
};
export type ReportTask = DictionaryItem & {
tenantId: string;
signatureId: string;
channelId: string;
reportType?: 'signature' | 'drainage';
drainageItemId?: string | null;
status: string;
signature?: {
id: string;
name: string;
purpose?: string | null;
drainageInfo?: Record<string, unknown> | null;
tenant?: { id: string; name: string };
application?: { id: string; name: string } | null;
};
drainageInfo?: SmsDrainageInfo | null;
channel?: { id: string; name: string; code: string };
reason?: string | null;
createdAt?: string;
updatedAt?: string;
exportItems?: Array<{
id: string;
rowNumber: number;
exportFile: { id: string; fileObjectId?: string | null; fileName: string; rowCount: number; batchId?: string | null };
batchItem: { id: string; materialVersion: number; batch: { id: string; batchNo: string; createdAt: string } };
}>;
records?: Array<{
id: string;
action: string;
statusBefore?: string | null;
statusAfter: string;
reason?: string | null;
createdAt: string;
}>;
deliveryStats?: {
total: number;
acceptedCount: number;
submitFailureCount: number;
submitFailureRate: number;
successCount: number;
successRate: number;
unknownCount: number;
unknownRate: number;
failureCount: number;
failureRate: number;
};
lastSuccessfulSentAt?: string | null;
};
export type ReportRecord = DictionaryItem & {
taskId: string;
channelId: string;
action: string;
statusBefore?: string | null;
statusAfter?: string | null;
reason?: string | null;
sourceEntry?: 'system' | 'legacy' | 'enterprise_signature' | 'report_task' | 'channel_report';
channel?: AdminChannel;
task?: ReportTask;
};
+64
View File
@@ -0,0 +1,64 @@
// R1 compatibility types. Keep public names re-exported from src/api/adminApi.ts.
export const DEFAULT_CLIENT_TENANT_ID = 'tenant-a';
export type DeletionTargetType = 'channel' | 'signature' | 'template';
export type DeletionDependency = { kind: string; label: string; count: number; items: string[] };
export type DeletionPreflight = {
type: DeletionTargetType;
id: string;
expectedUpdatedAt: string;
identity: Record<string, string>;
dependencies: DeletionDependency[];
impacts: string[];
blockedReasons: string[];
allowedActions: Array<'delete'>;
recoverability: { mode: 'soft_delete'; description: string };
};
export type DeleteTargetRequest = { expectedUpdatedAt: string; idempotencyKey: string; reason: string };
export type DeletionResult = { operationId: string; status: 'deleted'; replayed: boolean };
export type PagedResult<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
};
export type FileObject = {
id: string;
tenantId?: string | null;
bucket: string;
objectKey: string;
fileName: string;
contentType: string;
sizeBytes: string | number;
purpose: string;
createdAt: string;
};
export type FileRef = {
fileObjectId: string;
fileName: string;
contentType?: string;
};
export type PagedResponse<T> = {
items: T[];
total: number;
page: number;
pageSize: number;
};
export type CursorPage<T> = {
items: T[];
pageSize: number;
hasMore: boolean;
nextCursor: string | null;
};
+96
View File
@@ -0,0 +1,96 @@
// R1 compatibility types. Keep public names re-exported from src/api/adminApi.ts.
export type RiskReviewTask = {
id: string;
tenantId: string;
applicationId?: string | null;
templateId?: string | null;
taskNo: string;
sourceType?: string;
contentHash?: string | null;
windowStartedAt?: string | null;
windowEndsAt?: string | null;
content: string;
category?: string | null;
phoneTotal: number;
uniquePhoneTotal: number;
duplicateRatio: number;
illegalRatio: number;
blacklistHitRatio: number;
variableIssues?: unknown;
status: string;
riskDecision: string;
reviewReason?: string | null;
rejectReason?: string | null;
createdAt: string;
reviewedAt?: string | null;
tenant?: { id: string; name: string } | null;
application?: { id: string; name: string } | null;
reviewedBy?: { id: string; username: string; displayName: string } | null;
riskHits?: Array<{ id: string; ruleName: string; reason: string }>;
_count?: { messageRecords: number };
};
export type RiskRuleItem = {
id: string;
tenantId?: string | null;
applicationId?: string | null;
code: 'MAX_PHONES_PER_TASK' | 'NON_WORKING_MARKETING_BULK' | 'TASK_CREATE_FREQUENCY' | 'PHONE_FREQUENCY_24H' | 'PHONE_FREQUENCY_5M';
name: string;
description?: string | null;
metric: string;
thresholdValue: number;
action: 'block' | 'manual_review';
status: 'active' | 'inactive';
priority: number;
config?: { startTime?: string; endTime?: string; timeZone?: string; periodSeconds?: number; alignment?: string } | null;
application?: { id: string; name: string; tenantId: string; tenant?: { id: string; name: string } } | null;
updatedAt: string;
};
export type PhoneFrequencyHit = {
id: string;
tenantId: string;
applicationId: string;
ruleCode: 'PHONE_FREQUENCY_24H' | 'PHONE_FREQUENCY_5M';
ruleName: string;
phoneNumber: string;
thresholdValue: number;
actualValue: number;
windowStartedAt: string;
windowEndsAt: string;
releasedAt?: string | null;
releaseReason?: string | null;
createdAt: string;
tenant: { id: string; name: string };
application: { id: string; name: string };
releasedBy?: { id: string; username: string; displayName: string } | null;
};
export type PhoneFrequencyWhitelistItem = {
id: string;
phoneNumber: string;
status: 'active' | 'inactive' | 'deleted';
reason: string;
remark?: string | null;
deletedAt?: string | null;
createdAt: string;
updatedAt: string;
createdBy: { id: string; username: string; displayName: string };
updatedBy: { id: string; username: string; displayName: string };
};
export type RiskTaskMessagePage = {
items: Array<{
id: string;
phoneNumber: string;
province?: string | null;
carrier?: string | null;
status: string;
}>;
total: number;
page: number;
pageSize: number;
};
export type BatchTaskMessagePage = RiskTaskMessagePage;
+496
View File
@@ -0,0 +1,496 @@
// R1 compatibility types. Keep public names re-exported from src/api/adminApi.ts.
import type { AdminChannel, ReportTask } from './channels-reports';
import type { ApplicationDeactivationPreview, SmsMessageRecord, TenantAccount } from './operations';
export type EnterpriseCertification = {
id: string;
tenantId: string;
companyName: string;
licenseNo?: string | null;
contactName?: string | null;
contactPhone?: string | null;
materials?: Record<string, unknown> | null;
status: string;
rejectReason?: string | null;
submittedAt: string;
reviewedAt?: string | null;
reviewer?: { id: string; username: string; displayName: string } | null;
tenant?: { id: string; name: string; code: string };
};
export type AuditRecord = {
id: string;
tenantId?: string | null;
targetType: string;
targetId: string;
action: string;
statusBefore?: string | null;
statusAfter: string;
reason?: string | null;
reviewerId?: string | null;
reviewer?: { id: string; username: string; displayName: string } | null;
createdAt: string;
};
export type SmsTemplateAudit = {
id: string;
tenantId: string;
applicationId: string;
name: string;
content: string;
category?: string | null;
auditStatus: string;
rejectReason?: string | null;
createdAt: string;
updatedAt: string;
application?: { name: string };
tenant?: { name: string };
};
export type ReviewPreflight = {
type: 'signature' | 'template';
id: string;
tenantId: string;
status: string;
expectedUpdatedAt: string;
identity: Record<string, string>;
impacts: string[];
materialSummary: Record<string, string | number>;
blockedReasons: string[];
allowedActions: Array<'approve' | 'reject'>;
};
export type ReviewDecisionResult = {
operationId: string;
replayed: boolean;
decision: 'approve' | 'reject';
status: string;
item: ClientSmsSignature | SmsTemplateAudit;
};
export type TenantOption = {
id: string;
name: string;
code: string;
status: string;
enterpriseProfile?: {
creditCode?: string;
province?: string;
city?: string;
address?: string;
contactName?: string;
contactIdCard?: string;
contactPhone?: string;
contactEmail?: string;
photoFileObjectId?: string;
} | null;
};
export type TenantManagementRow = TenantOption & {
account?: TenantAccount | null;
todaySpendCents: number;
todayRefundCents: number;
};
export type CaptchaResponse = {
captchaId: string;
challenge: string;
expiresInSeconds: number;
};
export type ManagedUser = {
id: string;
tenantId?: string | null;
username: string;
email?: string | null;
phone?: string | null;
displayName: string;
status: string;
failedLoginCount: number;
lockedUntil?: string | null;
lastLoginAt?: string | null;
createdAt: string;
tenant?: TenantOption | null;
roles: Array<{ role: { code: string; name: string; scope: string } }>;
};
export type UserPayload = {
tenantId?: string | null;
username?: string;
email?: string | null;
phone?: string | null;
displayName: string;
password?: string;
status?: string;
roleCode: 'platform_admin' | 'enterprise_admin';
operatorId?: string;
};
export type DashboardResponse = {
taskCount: number;
messageStatus: Array<{ status: string; _count: { _all: number }; _sum: { amountCents?: number | null; billingUnits?: number | null } }>;
today: { sent: number; delivered: number; failed: number; unknown: number; successRate: number; spendCents: number; returnedCents: number; billingUnits: number };
uplinkCount: number;
billing: { _count: { _all: number }; _sum: { amountCents?: number | null; billingUnits?: number | null } };
transactions: { _count: { _all: number }; _sum: { amountCents?: number | null } };
gatewayConnections: Array<{ status: string; _count: { _all: number }; _sum: { currentConnections?: number | null; desiredConnections?: number | null } }>;
pendingAuditCount: number;
pendingAudits: { enterpriseCertifications: number; smsAudits: number; templates: number; signatures: number; drainageInfos: number; total: number };
hourlySendTrend: Array<{ hour: number; label: string; submittedCount: number; successCount: number }>;
auditProcessingSpeed: Array<{ category: string; label: string; count: number; averageProcessingMs: number | null }>;
downstreamDeliverySummary?: {
pending: number;
failed: number;
delivered: number;
stalledPending: number;
stalledAck: number;
recentFailed: number;
alertCount: number;
};
accounts: Array<{ id: string; tenantId: string; balanceCents: number; creditCents: number; status: string; tenant?: TenantOption }>;
enterpriseSpendRanks: Array<{ tenantId: string; tenantName: string; todaySpendCents: number; balanceCents: number; creditCents: number }>;
recentTasks: Array<Record<string, unknown>>;
recentRecharges: Array<RechargeOrder>;
clientOverview?: {
enterpriseName: string | null;
certificationStatus: 'certified' | 'uncertified';
signatureCount: number;
pendingBatchTaskCount: number;
};
};
export type RechargeOrder = {
id: string;
tenantId: string;
orderNo: string;
amountCents: number;
status: string;
payMethod?: string | null;
paidAt?: string | null;
operatorId?: string | null;
remark?: string | null;
balanceAfterCents?: number | null;
createdAt: string;
tenant?: TenantOption;
};
export type ManualRechargePreflight = {
tenant: Pick<TenantOption, 'id' | 'name' | 'code'>;
accountId: string;
expectedAccountUpdatedAt: string;
balanceCents: number;
creditCents: number;
amountCents: number;
balanceAfterCents: number;
direction: 'topup' | 'correction';
allowedActions: Array<'confirm'>;
blockedReasons: string[];
};
export type ManualRechargeResult = RechargeOrder & {
balanceAfterCents: number;
operationId: string;
replayed: boolean;
};
export type ClientSmsApplication = {
id: string;
tenantId: string;
name: string;
scene?: string | null;
customerUnitPrice?: number | null;
queuePriority?: 'normal' | 'priority' | string | null;
status: string;
dailyLimit?: number | null;
createdAt?: string;
updatedAt?: string;
sentToday?: number;
deliveryRate?: number;
cmppStatus?: 'connected' | 'degraded' | 'disconnected' | 'inactive';
interfaceEnabled?: boolean | null;
cmppConnections?: CmppDownstreamConnection[];
httpConfig?: HttpApiConfig | null;
};
export type ClientSmsSignature = {
id: string;
tenantId: string;
applicationId?: string | null;
name: string;
purpose?: string | null;
drainageInfo?: Record<string, unknown> | null;
auditStatus: string;
rejectReason?: string | null;
createdAt: string;
updatedAt: string;
materials?: Array<Record<string, unknown>>;
tenant?: TenantOption;
application?: ClientSmsApplication | null;
reportStatus?: string;
reportTasks?: Array<ReportTask & { channel?: AdminChannel }>;
reportTargets?: Array<{ channel: AdminChannel; channelId: string; status: string; taskId?: string }>;
carrierReportSummary?: Record<'mobile' | 'unicom' | 'telecom', { status: string; approved: number; total: number }>;
drainageReportTargets?: Record<string, Array<{ channel: AdminChannel; channelId: string; status: string; taskId?: string }>>;
drainageCarrierReportSummary?: Record<string, Record<'mobile' | 'unicom' | 'telecom', { status: string; approved: number; total: number }>>;
};
export type ClientSmsSignatureView = Pick<ClientSmsSignature,
'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>;
drainageInfo: { links: Array<{
id: string;
siteName: string;
url: string;
remark?: string | null;
reportValues: Record<string, unknown>;
auditStatus: string;
rejectReason?: string | null;
submittedAt: string;
reviewedAt?: string | null;
createdAt: string;
updatedAt: string;
}> };
};
export type ClientSignatureWorkspace = {
items: ClientSmsSignatureView[];
summary: { total: number; pending: number; approved: number; rejected: number; draft: number };
total: number;
page: number;
pageSize: number;
};
export type SmsDrainageInfo = {
id: string;
tenantId: string;
signatureId: string;
applicationId?: string | null;
siteName: string;
url: string;
remark?: string | null;
reportValues?: Record<string, unknown> | null;
auditStatus: string;
rejectReason?: string | null;
submittedAt: string;
reviewedAt?: string | null;
createdAt: string;
updatedAt: string;
tenant?: TenantOption;
signature?: ClientSmsSignature;
application?: ClientSmsApplication | null;
reportTasks?: ReportTask[];
};
export type ClientSmsTemplate = {
id: string;
tenantId: string;
applicationId: string;
signatureId?: string | null;
name: string;
content: string;
category?: string | null;
auditStatus: string;
rejectReason?: string | null;
createdAt: string;
updatedAt: string;
variables?: Array<{ name: string; example?: string | null; required?: boolean }>;
application?: { id: string; name: string };
signature?: { id: string; name: string; purpose?: string | null; drainageInfo?: Record<string, unknown> | null };
tenant?: TenantOption;
};
export type SmsBatchTask = {
id: string;
tenantId: string;
taskNo: string;
sourceType?: string;
contentHash?: string | null;
windowStartedAt?: string | null;
windowEndsAt?: string | null;
applicationId?: string | null;
templateId?: string | null;
content: string;
category?: string | null;
phoneTotal: number;
status: string;
auditStatus?: string | null;
reviewReason?: string | null;
rejectReason?: string | null;
progressTotal: number;
progressSent?: number;
progressDelivered?: number;
progressFailed?: number;
submittedTotal?: number;
successTotal?: number;
failedTotal?: number;
unknownTotal?: number;
timeoutTotal?: number;
scheduledAt?: string | null;
canceledAt?: string | null;
createdAt: string;
tenant?: TenantOption;
application?: { id: string; name: string };
template?: { id: string; name: string; content: string; billingUnits?: number };
messages?: SmsMessageRecord[];
messageStats?: Array<{
batchTaskId?: string | null;
carrier?: string | null;
province?: string | null;
status: string;
_count: { _all: number };
_sum: { billingUnits?: number | null };
}>;
};
export type ImportPreviewResponse = {
fileName?: string;
encoding: string;
totalRows: number;
validCount: number;
errorCount: number;
phones: string[];
errors: Array<{ rowNumber: number; phoneNumber?: string; reason: string }>;
};
export type HttpApiConfig = {
enabled: boolean;
sendEnabled: boolean;
messageQueryEnabled: boolean;
receiptWebhookEnabled: boolean;
uplinkWebhookEnabled: boolean;
uplinkQueryEnabled: boolean;
credentialSelfServiceEnabled: boolean;
qpsLimit: number;
timestampToleranceSeconds: number;
maxCredentialCount: number;
uplinkRetentionDays: number;
maxQueryRangeDays: number;
maxPageSize: number;
receiptDeliveryMode: 'cmpp' | 'http' | 'both' | 'none';
uplinkDeliveryMode: 'cmpp' | 'http' | 'both' | 'none';
webhookRetryEnabled: boolean;
webhookMaxAttempts: number;
webhookTimeoutSeconds: number;
requireHttps: boolean;
allowClientManualRetry: boolean;
allowClientTest: boolean;
};
export type HttpApiConfigResponse = { applicationId: string; applicationName?: string; config: HttpApiConfig | null; ipAllowlist: string[] };
export type HttpApiCredential = { id: string; name: string; accessKey: string; secretLast4: string; secret?: string; secretShownOnce?: boolean; status: string; expiresAt?: string | null; lastUsedAt?: string | null; lastUsedIp?: string | null; createdAt: string };
export type HttpWebhookEndpoint = { id: string; eventType: 'receipt' | 'uplink'; url: string; secretLast4: string; secret?: string; secretShownOnce?: boolean; status: string; lastTestAt?: string | null; lastTestStatus?: string | null; updatedAt: string };
export type HttpApiRequestLog = { id: string; requestId: string; clientMessageId?: string | null; sourceIp?: string | null; httpStatus?: number | null; businessCode?: string | null; status: string; durationMs?: number | null; createdAt: string; completedAt?: string | null };
export type HttpWebhookDelivery = { id: string; status: string; attemptCount: number; lastHttpStatus?: number | null; lastError?: string | null; createdAt: string; event: { eventId: string; eventType: string; messageId?: string | null }; endpoint: { eventType: string; url: string } };
export type EnterpriseApplication = {
id: string;
tenantId: string;
name: string;
scene?: string | null;
status: string;
disablingAt?: string | null;
autoDisableAt?: string | null;
disableReason?: string | null;
deactivation?: ApplicationDeactivationPreview | null;
dailyLimit?: number | null;
customerUnitPrice?: number | null;
queuePriority?: 'normal' | 'priority' | string | null;
templateMismatchMode?: string | null;
downstreamReceiptRetryEnabled?: boolean | null;
downstreamUplinkRetryEnabled?: boolean | null;
cmppAccount?: string | null;
cmppEnterpriseCode?: string | null;
cmppApplicationExtension?: string | null;
cmppAccessNumberFillEnabled?: boolean | null;
cmppAccessNumberFillPrefix?: string | null;
cmppClientSrcId?: string | null;
interfaceEnabled?: boolean | null;
interfaceType?: 'cmpp20' | string | null;
cmppMaxConnections?: number | null;
cmppWindowSize?: number | null;
ipAllowlist?: Array<{ id: string; ipCidr: string; remark?: string | null }>;
httpConfig?: HttpApiConfig | null;
tenant?: TenantOption;
sentToday?: number;
deliveryRate?: number;
cmppStatus?: 'connected' | 'degraded' | 'disconnected' | 'inactive';
cmppConnections?: CmppDownstreamConnection[];
};
export type CmppDownstreamConnection = {
id: string;
tenantId: string;
applicationId: string;
account: string;
enterpriseCode: string;
connectionId: string;
remoteIp?: string | null;
protocol?: string | null;
status: string;
connectedAt: string;
lastHeartbeatAt?: string | null;
lastSubmitAt?: string | null;
lastDeliverAt?: string | null;
disconnectedAt?: string | null;
lastError?: string | null;
updatedAt: string;
};
export type CmppConnectionState = {
id: string;
tenantId?: string | null;
applicationId?: string | null;
channelId: string;
connectionId: string;
status: string;
desiredConnections: number;
currentConnections: number;
lastConnectedAt?: string | null;
lastDisconnectedAt?: string | null;
lastHeartbeatAt?: string | null;
reconnectCount: number;
lastReconnectAttemptAt?: string | null;
nextReconnectAt?: string | null;
lastErrorCategory?: string | null;
lastError?: string | null;
updatedAt: string;
channel?: AdminChannel;
};
export type ApplicationConnectionsResponse = {
application: EnterpriseApplication;
connections: CmppDownstreamConnection[];
summary: { desiredConnections: number; currentConnections: number; status: string };
};
export type ApplicationCmppParams = {
applicationId: string;
applicationName: string;
tenantName: string;
appCode: string;
gatewayHost: string;
gatewayPort: number;
enterpriseCode: string;
account: string;
passwordCipher: string;
srcId: string;
applicationExtension?: string | null;
accessNumberFillEnabled?: boolean;
accessNumberFillPrefix?: string | null;
interfaceEnabled?: boolean;
interfaceType?: string;
maxConnections: number;
heartbeatSeconds: number;
windowSize: number;
protocolVersion: string;
};
+5
View File
@@ -0,0 +1,5 @@
export * from './common';
export * from './identity-config';
export * from './channels-reports';
export * from './operations';
export * from './governance';
+580
View File
@@ -0,0 +1,580 @@
// R1 compatibility types. Keep public names re-exported from src/api/adminApi.ts.
import type { AdminChannel } from './channels-reports';
import type { PagedResponse } from './common';
import type { EnterpriseApplication, TenantOption } from './identity-config';
export type ChannelQualityStat = {
channelId: string;
channelName: string;
total: number;
acceptedCount: number;
submitFailureCount: number;
submitFailureRate: number;
successCount: number;
unknownCount: number;
failureCount: number;
successRate: number;
unknownRate: number;
failureRate: number;
averageArrivalMs?: number | null;
};
export type SignatureQualityStat = {
id: string;
signatureId: string;
signatureName: string;
tenantId: string;
tenantName: string;
hasDrainage: boolean;
total: number;
acceptedCount: number;
submitFailureCount: number;
successCount: number;
unknownCount: number;
failureCount: number;
successRate: number;
averageArrivalMs?: number | null;
};
export type SignatureChannelCarrierQualityStat = {
signatureId: string;
channelId: string;
channelName: string;
carrier: string;
total: number;
acceptedCount: number;
submitFailureCount: number;
successCount: number;
unknownCount: number;
failureCount: number;
successRate: number;
averageArrivalMs?: number | null;
};
export type SignatureCarrierBusinessQualityStat = {
signatureId: string;
carrier: string;
businessMessageCount: number;
finalSuccessCount: number;
finalSuccessRate: number;
averageArrivalMs?: number | null;
};
export type SignatureChannelQualityItem = {
signatureId: string;
signatureName: string;
tenantId: string;
tenantName: string;
applicationNames?: string | null;
total: number;
acceptedCount: number;
submitFailureCount: number;
successCount: number;
unknownCount: number;
failureCount: number;
successRate: number;
averageArrivalMs?: number | null;
channelSubmitTotal: number;
carrierOverview: SignatureCarrierBusinessQualityStat[];
breakdowns: SignatureChannelCarrierQualityStat[];
};
export type SignatureChannelQualityResponse = {
date: string;
items: SignatureChannelQualityItem[];
total: number;
page: number;
pageSize: number;
};
export type DailySendSummary = {
total: number;
successCount: number;
unknownCount: number;
failureCount: number;
successRate: number;
};
export type ApplicationQualityStat = DailySendSummary & {
applicationId: string;
applicationName: string;
tenantId: string;
tenantName: string;
};
export type SendQualityResponse = {
date: string;
summary: DailySendSummary;
channels: ChannelQualityStat[];
signatures: SignatureQualityStat[];
applications: ApplicationQualityStat[];
};
export type SmsMessageRecord = {
id: string;
tenantId?: string | null;
batchTaskId?: string | null;
applicationId?: string | null;
channelId?: string | null;
messageId: string;
phoneNumber: string;
carrier?: string | null;
province?: string | null;
content: string;
clientSrcId?: string | null;
applicationExtension?: string | null;
billingUnits: number;
amountCents: number;
status: string;
errorMessage?: string | null;
errorCode?: string | null;
queuedAt: string;
submittedAt?: string | null;
deliveredAt?: string | null;
receiptStatus?: string | null;
submitStatus?: string | null;
channel?: AdminChannel | null;
tenant?: TenantOption | null;
application?: { id: string; name: string };
submitRecords?: SmsSubmitRecord[];
receiptRecords?: SmsReceiptRecord[];
downstreamDeliveries?: Array<{
id: string;
deliveryType: string;
status: string;
deliveredAt?: string | null;
lastError?: string | null;
}>;
};
export type SmsSubmitRecord = {
id: string;
channelId: string;
channelGroupName?: string | null;
submitId: string;
sequenceId?: number | null;
gatewayMessageId?: string | null;
submitStatus: string;
errorCode?: string | null;
errorMessage?: string | null;
submittedAt?: string | null;
createdAt: string;
channel?: AdminChannel | null;
channelGroup?: { id: string; name: string } | null;
};
export type SmsReceiptRecord = {
id: string;
channelId?: string | null;
messageId: string;
gatewayMessageId: string;
sequenceId?: number | null;
receiptStatus: string;
rawStatus: string;
errorCode?: string | null;
deliveredAt: string;
createdAt: string;
channel?: AdminChannel | null;
};
export type SmsMessageSegmentAudit = {
id: string;
tenantId: string;
batchTaskId?: string | null;
messageRecordId: string;
submitRecordId?: string | null;
channelId?: string | null;
submitId: string;
attempt: number;
segmentTotal: number;
segmentIndex: number;
sequenceId?: number | null;
gatewayMessageId?: string | null;
submitStatus: string;
receiptStatus?: string | null;
rawStatus?: string | null;
compensationType?: string | null;
errorCode?: string | null;
errorMessage?: string | null;
submittedAt?: string | null;
deliveredAt?: string | null;
createdAt: string;
updatedAt: string;
channel?: AdminChannel | null;
};
export type SmsUplinkMessage = {
id: string;
tenantId?: string | null;
channelId: string;
applicationId?: string | null;
messageRecordId?: string | null;
messageId?: string | null;
sequenceId?: number | null;
phoneNumber: string;
destId: string;
content: string;
matchStatus?: string;
matchReason?: string | null;
receivedAt: string;
createdAt: string;
tenant?: TenantOption | null;
application?: { id: string; name: string } | null;
messageRecord?: SmsMessageRecord | null;
channel?: AdminChannel | null;
matchCandidates?: SmsUplinkMatchCandidate[];
};
export type SmsUplinkMatchCandidate = {
id: string;
uplinkMessageId: string;
tenantId: string;
applicationId: string;
messageRecordId?: string | null;
matchSource: string;
confidence: number;
reason?: string | null;
status: string;
claimedAt?: string | null;
claimedById?: string | null;
createdAt: string;
updatedAt: string;
tenant?: TenantOption | null;
application?: { id: string; name: string } | null;
messageRecord?: SmsMessageRecord | null;
};
export type TenantAccount = {
id: string;
tenantId: string;
balanceCents: number;
creditCents: number;
status: string;
updatedAt?: string;
tenant?: TenantOption;
};
export type OperationLogItem = {
id: string;
time: string;
level: 'info' | 'success' | 'warning' | 'error';
tenant: string;
module: string;
operator: string;
action: string;
resourceId: string;
detail: Record<string, unknown>;
ip: string;
userAgent: string;
};
export type OperationLogResponse = {
items: OperationLogItem[];
total: number;
page: number;
pageSize: number;
modules: string[];
};
export type ProtocolInteractionLogItem = {
id: string;
protocol: 'cmpp' | 'http';
direction: 'client_to_platform' | 'platform_to_channel' | 'channel_to_platform' | 'platform_to_client';
eventType: string;
status: 'received' | 'accepted' | 'success' | 'failed' | 'retrying';
tenantId?: string | null;
applicationId?: string | null;
channelId?: string | null;
account?: string | null;
messageId?: string | null;
gatewayMessageId?: string | null;
traceId?: string | null;
requestId?: string | null;
phoneMasked?: string | null;
resultCode?: string | null;
durationMs?: number | null;
payloadBytes?: number | null;
retryCount?: number | null;
detail?: Record<string, unknown> | null;
createdAt: string;
};
export type ProtocolInteractionLogResponse = {
items: ProtocolInteractionLogItem[];
total: number;
page: number;
pageSize: number;
eventTypes: string[];
};
export type SystemLogExportResult = {
operationId: string;
status: 'completed';
fileName: string;
recordCount: number;
truncated: boolean;
content: string;
filters: { keyword?: string; level?: string; module?: string; range?: string };
};
export type DailyReconciliationReport = {
id: string;
reportDate: string;
tenantId: string;
tenantName: string;
applicationId: string;
applicationName: string;
submittedUnits: number;
sentUnits: number;
unknownUnits: number;
successUnits: number;
failedUnits: number;
generatedAt: string;
updatedAt: string;
};
export type DailyProfitReport = {
id: string;
reportDate: string;
dimensionType: 'application' | 'channel';
dimensionId: string;
dimensionName: string;
tenantId?: string | null;
tenantName?: string | null;
applicationId?: string | null;
channelId?: string | null;
submittedUnits: number;
sentUnits: number;
unknownUnits: number;
successUnits: number;
failedUnits: number;
revenueCents: number;
refundCents: number;
costCents: number;
profitCents: number;
profitRateBps: number;
generatedAt: string;
updatedAt: string;
};
export type DailyQualityReport = {
id: string;
reportDate: string;
dimensionType: 'application' | 'channel' | 'signature' | 'drainage';
dimensionId: string;
dimensionName: string;
tenantId?: string | null;
tenantName?: string | null;
applicationId?: string | null;
channelId?: string | null;
signatureId?: string | null;
drainageInfoId?: string | null;
submittedUnits: number;
sentUnits: number;
unknownUnits: number;
successUnits: number;
failedUnits: number;
successRateBps: number;
avgArrivalMs?: number | null;
generatedAt: string;
updatedAt: string;
};
export type DownstreamDeliveryRecord = {
id: string;
tenantId: string;
applicationId: string;
messageRecordId?: string | null;
messageId?: string | null;
deliveryType: string;
status: string;
payload: Record<string, unknown>;
retryCount: number;
manualRetryCount: number;
lastRetriedAt?: string | null;
retryEnabled: boolean;
nextRetryAt?: string | null;
sentAt?: string | null;
acknowledgedAt?: string | null;
ackDeadlineAt?: string | null;
ackResult?: number | null;
ackSequenceId?: string | null;
ackMessageId?: string | null;
connectionId?: string | null;
deliveredAt?: string | null;
lastError?: string | null;
createdAt: string;
updatedAt: string;
tenant?: TenantOption | null;
application?: EnterpriseApplication | null;
messageRecord?: SmsMessageRecord | null;
attempts?: Array<{
id: string;
attemptNo: number;
connectionId?: string | null;
sequenceId?: string | null;
messageId?: string | null;
status: string;
sentAt?: string | null;
ackDeadlineAt?: string | null;
acknowledgedAt?: string | null;
ackResult?: number | null;
failureType?: string | null;
errorMessage?: string | null;
createdAt: string;
updatedAt: string;
}>;
};
export type ApplicationDeactivationPreview = {
status: string;
reason?: string | null;
disablingAt?: string | null;
autoDisableAt?: string | null;
awaitingSupplierReceipt: number;
waitingToSend: number;
awaitingClientAck: number;
retryableFailures: number;
pendingUplinks: number;
activeConnections: number;
totalOutstanding: number;
};
export type BatchRequeueResponse = {
total: number;
successCount: number;
failedCount: number;
results: Array<{ id: string; status: 'success' | 'failed'; errorMessage?: string }>;
};
export type DownstreamDeliveryDashboard = {
summary: {
total: number;
pending: number;
awaitingAck: number;
delivered: number;
failed: number;
unconfirmed: number;
rejected: number;
stalledPending: number;
stalledAck: number;
recentFailed: number;
alertCount: number;
};
typeBreakdown: Array<{
deliveryType: string;
total: number;
pending: number;
awaitingAck: number;
delivered: number;
failed: number;
unconfirmed: number;
rejected: number;
}>;
retryBuckets: Array<{
label: string;
count: number;
}>;
topApplications: Array<{
applicationId: string;
name: string;
pending: number;
awaitingAck: number;
failed: number;
unconfirmed: number;
rejected: number;
delivered: number;
alertCount: number;
}>;
};
export type GatewayDownstreamRecoveryStatus = {
id: string;
account: string;
tenantId?: string | null;
applicationId?: string | null;
gatewayInstanceId?: string | null;
state: string;
lockOwner?: string | null;
lockExpiresAt?: string | null;
lastAttemptAt?: string | null;
lastSuccessAt?: string | null;
lastFailureAt?: string | null;
nextRetryAt?: string | null;
attemptCount: number;
failureCategory?: string | null;
lastError?: string | null;
lastSkipReason?: string | null;
createdAt: string;
updatedAt: string;
tenant?: TenantOption | null;
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;
running: number;
success: number;
failed: number;
waitingConnection: number;
backoff: number;
failureCategories: Array<{ category: string; count: number }>;
};
};
export type DownstreamRecoveryStatusExportQuery = {
tenantId?: string;
applicationId?: string;
state?: string;
failureCategory?: string;
keyword?: string;
updatedAtFrom?: string;
updatedAtTo?: string;
};