feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -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)),
|
||||
};
|
||||
@@ -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>;
|
||||
},
|
||||
};
|
||||
@@ -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' }),
|
||||
};
|
||||
@@ -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),
|
||||
}),
|
||||
};
|
||||
@@ -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 }) }),
|
||||
};
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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>;
|
||||
},
|
||||
};
|
||||
@@ -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}`;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './common';
|
||||
export * from './identity-config';
|
||||
export * from './channels-reports';
|
||||
export * from './operations';
|
||||
export * from './governance';
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user