feat: strengthen risk controls and review workflows

This commit is contained in:
hectorzhao
2026-07-26 13:08:12 +08:00
parent b461532075
commit 2ce682c3fc
34 changed files with 2167 additions and 390 deletions
+80 -8
View File
@@ -988,6 +988,36 @@ export type RiskReviewTask = {
_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';
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 } | null;
application?: { id: string; name: string; tenantId: string; tenant?: { id: string; name: string } } | null;
updatedAt: 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 TenantAccount = {
id: string;
tenantId: string;
@@ -1144,10 +1174,13 @@ export type EnterpriseApplication = {
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;
maxPhonesPerTask?: number | null;
templateMismatchMode?: string | null;
downstreamReceiptRetryEnabled?: boolean | null;
downstreamUplinkRetryEnabled?: boolean | null;
@@ -1284,6 +1317,20 @@ export type DownstreamDeliveryRecord = {
}>;
};
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;
@@ -1463,7 +1510,8 @@ export const adminApi = {
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 } = {}) => request<ManagedUser[]>(withQuery('/admin/users', query)),
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) =>
@@ -1491,14 +1539,16 @@ export const adminApi = {
request<EnterpriseApplication[]>(withQuery('/admin/enterprise-applications', 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'; maxPhonesPerTask?: number; 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[] }) =>
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'; maxPhonesPerTask?: number; 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[] }) =>
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) }),
changeApplicationStatus: (id: string, status: string, reason?: string) =>
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 }),
body: JSON.stringify({ status, reason, force }),
}),
listApplicationConnections: (applicationId: string) =>
request<ApplicationConnectionsResponse>(`/admin/enterprise-applications/${applicationId}/connections`),
@@ -1706,6 +1756,26 @@ export const adminApi = {
batchRequeueDownstreamDeliveries: (ids: string[]) =>
request<BatchRequeueResponse>('/admin/operations/downstream-deliveries/requeue', { method: 'POST', body: JSON.stringify({ ids }) }),
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) }),
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) =>
@@ -1778,8 +1848,10 @@ 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: (tenantId = getSessionTenantId() ?? DEFAULT_CLIENT_TENANT_ID) =>
request<ManagedUser[]>('/client/users', { tenantId }),
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) =>