diff --git a/api/src/audit/audit.service.ts b/api/src/audit/audit.service.ts index aebd095..6cb3309 100644 --- a/api/src/audit/audit.service.ts +++ b/api/src/audit/audit.service.ts @@ -21,7 +21,6 @@ export class AuditService { return this.prisma.operationLog.findMany({ where: tenantId ? { tenantId } : undefined, orderBy: { createdAt: 'desc' }, - take: 100, }); } diff --git a/api/src/billing/billing.service.ts b/api/src/billing/billing.service.ts index 9119558..0fdc3ba 100644 --- a/api/src/billing/billing.service.ts +++ b/api/src/billing/billing.service.ts @@ -89,7 +89,7 @@ export class BillingService { constructor(private readonly prisma: PrismaService) {} listPlans() { - return this.prisma.billingPlan.findMany({ orderBy: { createdAt: 'desc' }, take: 100 }); + return this.prisma.billingPlan.findMany({ orderBy: { createdAt: 'desc' } }); } createPlan(data: CreateBillingPlanDto) { @@ -109,7 +109,6 @@ export class BillingService { return this.prisma.tenantAccount.findMany({ include: { tenant: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); } @@ -129,7 +128,6 @@ export class BillingService { where: tenantId ? { tenantId } : undefined, include: { plan: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); } @@ -141,7 +139,6 @@ export class BillingService { }, include: { plan: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); const orderIds = orders.map((order) => order.id); if (orderIds.length === 0) { @@ -328,12 +325,11 @@ export class BillingService { return this.prisma.smsBillingRecord.findMany({ where: { tenantId, taskId }, orderBy: { createdAt: 'desc' }, - take: 100, }); } listRules() { - return this.prisma.billingRule.findMany({ orderBy: { createdAt: 'desc' }, take: 100 }); + return this.prisma.billingRule.findMany({ orderBy: { createdAt: 'desc' } }); } createRule(data: CreateBillingRuleDto) { diff --git a/api/src/certification/certification.service.ts b/api/src/certification/certification.service.ts index 0f79549..4c856e5 100644 --- a/api/src/certification/certification.service.ts +++ b/api/src/certification/certification.service.ts @@ -35,7 +35,6 @@ export class CertificationService { }, include: { tenant: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); } diff --git a/api/src/channels/channels.service.spec.ts b/api/src/channels/channels.service.spec.ts index 5d44c51..f02ab74 100644 --- a/api/src/channels/channels.service.spec.ts +++ b/api/src/channels/channels.service.spec.ts @@ -715,13 +715,11 @@ describe('ChannelsService', () => { expect(prisma.cmppConnectionState.findMany).toHaveBeenCalledWith({ where: { channelId: 'channel-1' }, orderBy: { updatedAt: 'desc' }, - take: 100, }); expect(prisma.cmppConnectionState.findMany).toHaveBeenCalledWith({ where: { tenantId: 'tenant-1' }, include: { channel: true }, orderBy: { updatedAt: 'desc' }, - take: 100, }); expect(prisma.operationLog.create).toHaveBeenCalledWith({ data: expect.objectContaining({ diff --git a/api/src/channels/channels.service.ts b/api/src/channels/channels.service.ts index 0697871..ac838de 100644 --- a/api/src/channels/channels.service.ts +++ b/api/src/channels/channels.service.ts @@ -198,7 +198,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { return this.prisma.smsChannel.findMany({ include: { connectionStates: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); } @@ -531,7 +530,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { return this.prisma.cmppConnectionState.findMany({ where: { channelId }, orderBy: { updatedAt: 'desc' }, - take: 100, }); } @@ -576,7 +574,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { where: { tenantId }, include: { channel: true }, orderBy: { updatedAt: 'desc' }, - take: 100, }); } @@ -705,7 +702,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { return this.prisma.smsChannelGroup.findMany({ include: { items: { include: { channel: { include: { connectionStates: true } } }, orderBy: [{ province: 'asc' }, { priority: 'asc' }] } }, orderBy: { createdAt: 'desc' }, - take: 100, }); } @@ -857,7 +853,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { return this.prisma.channelRouteRule.findMany({ include: { group: true, channel: true }, orderBy: [{ priority: 'asc' }, { createdAt: 'desc' }], - take: 100, }); } @@ -900,7 +895,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { return this.prisma.channelReportField.findMany({ where: channelId ? { channelId } : undefined, orderBy: [{ sortOrder: 'asc' }, { createdAt: 'desc' }], - take: 200, }); } @@ -926,7 +920,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { channelId, }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -958,7 +951,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { where: { tenantId, status }, include: { signature: true, channel: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); } @@ -1021,7 +1013,6 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy { return this.prisma.channelSignatureReportRecord.findMany({ where: { taskId, channelId }, orderBy: { createdAt: 'desc' }, - take: 200, }); } diff --git a/api/src/dictionaries/dictionaries.service.ts b/api/src/dictionaries/dictionaries.service.ts index ba95c62..2ac2381 100644 --- a/api/src/dictionaries/dictionaries.service.ts +++ b/api/src/dictionaries/dictionaries.service.ts @@ -140,7 +140,6 @@ export class DictionariesService { ] : undefined, }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -173,7 +172,6 @@ export class DictionariesService { ] : undefined, }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -214,7 +212,6 @@ export class DictionariesService { }, include: { tenant: true, application: true }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -254,7 +251,7 @@ export class DictionariesService { } listDrainageFields() { - return this.prisma.drainageField.findMany({ orderBy: { createdAt: 'desc' }, take: 200 }); + return this.prisma.drainageField.findMany({ orderBy: { createdAt: 'desc' } }); } createDrainageField(data: CreateDrainageFieldDto) { diff --git a/api/src/files/files.service.ts b/api/src/files/files.service.ts index 482c2de..446f208 100644 --- a/api/src/files/files.service.ts +++ b/api/src/files/files.service.ts @@ -37,7 +37,6 @@ export class FilesService { return this.prisma.fileObject.findMany({ where: tenantId ? { tenantId } : undefined, orderBy: { createdAt: 'desc' }, - take: 100, }).then((items) => items.map(serializeFileObject)); } diff --git a/api/src/operations/operations.service.spec.ts b/api/src/operations/operations.service.spec.ts index 12db554..8366048 100644 --- a/api/src/operations/operations.service.spec.ts +++ b/api/src/operations/operations.service.spec.ts @@ -202,7 +202,6 @@ describe('OperationsService', () => { }, include: { tenant: true, application: true, channel: true, submitRecords: true, receiptRecords: true }, orderBy: { queuedAt: 'desc' }, - take: 500, }); }); @@ -229,7 +228,6 @@ describe('OperationsService', () => { }, }, orderBy: { receivedAt: 'desc' }, - take: 500, }); }); diff --git a/api/src/operations/operations.service.ts b/api/src/operations/operations.service.ts index d817456..7cc4d6c 100644 --- a/api/src/operations/operations.service.ts +++ b/api/src/operations/operations.service.ts @@ -81,7 +81,6 @@ export class OperationsService { where: { tenantId: query.tenantId, status: query.status }, include: { apiRequests: true }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -90,7 +89,6 @@ export class OperationsService { where: messageWhere(query), include: { tenant: true, application: true, channel: true, submitRecords: true, receiptRecords: true }, orderBy: { queuedAt: 'desc' }, - take: 500, }); } @@ -112,7 +110,6 @@ export class OperationsService { }, }, orderBy: { receivedAt: 'desc' }, - take: 500, }); } @@ -303,7 +300,6 @@ export class OperationsService { return this.prisma.operationLog.findMany({ where: { tenantId: query.tenantId, userId: query.userId }, orderBy: { createdAt: 'desc' }, - take: 500, }); } diff --git a/api/src/risk-review/risk-review.service.ts b/api/src/risk-review/risk-review.service.ts index ebca272..87dc5ab 100644 --- a/api/src/risk-review/risk-review.service.ts +++ b/api/src/risk-review/risk-review.service.ts @@ -118,7 +118,6 @@ export class RiskReviewService { return this.prisma.riskRule.findMany({ where: tenantId ? { OR: [{ tenantId: null }, { tenantId }] } : undefined, orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }], - take: 200, }); } @@ -146,7 +145,6 @@ export class RiskReviewService { taskId, }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -158,7 +156,6 @@ export class RiskReviewService { }, include: { riskHits: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); } diff --git a/api/src/send-chain/send-chain.service.ts b/api/src/send-chain/send-chain.service.ts index ea45e11..3fd7782 100644 --- a/api/src/send-chain/send-chain.service.ts +++ b/api/src/send-chain/send-chain.service.ts @@ -331,19 +331,28 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy { return this.getBatchTask(task.id); } - listBatchTasks(tenantId?: string, status?: string) { - return this.prisma.smsBatchTask.findMany({ + async listBatchTasks(tenantId?: string, status?: string) { + const tasks = await this.prisma.smsBatchTask.findMany({ where: { tenantId, status }, include: { tenant: true, application: true, template: true, apiRequests: true, - messages: { include: { channel: true }, orderBy: { queuedAt: 'asc' }, take: 100000 }, }, orderBy: { createdAt: 'desc' }, - take: 100, }); + const taskIds = tasks.map((task) => task.id); + const messageStats = taskIds.length > 0 ? await this.prisma.smsMessageRecord.groupBy({ + by: ['batchTaskId', 'carrier', 'province', 'status'], + where: { batchTaskId: { in: taskIds } }, + _count: { _all: true }, + _sum: { billingUnits: true }, + }) : []; + return tasks.map((task) => ({ + ...task, + messageStats: messageStats.filter((item) => item.batchTaskId === task.id), + })); } getBatchTask(taskId: string) { @@ -378,7 +387,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy { receiptRecords: { include: { channel: true }, orderBy: { createdAt: 'asc' } }, }, orderBy: { queuedAt: 'desc' }, - take: 500, }); } @@ -386,7 +394,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy { return this.prisma.smsSubmitRecord.findMany({ where: { batchTaskId: taskId }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -394,7 +401,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy { return this.prisma.smsReceiptRecord.findMany({ where: { batchTaskId: taskId }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -403,7 +409,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy { where: { tenantId, channelId }, include: { application: true, channel: true, messageRecord: { include: { application: true } } }, orderBy: { receivedAt: 'desc' }, - take: 200, }); } @@ -542,7 +547,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy { const tasks = await this.prisma.smsBatchTask.findMany({ where: { status: 'scheduled', scheduledAt: { lte: now } }, orderBy: { scheduledAt: 'asc' }, - take: 100, }); const results: Array<{ taskId: string; status: string; enqueued?: number; reason?: string }> = []; for (const task of tasks) { @@ -1812,7 +1816,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy { const rules = await this.prisma.phoneCarrierRule.findMany({ where: { status: 'active' }, orderBy: [{ priority: 'asc' }, { createdAt: 'asc' }], - take: 100, }); for (const rule of rules) { try { diff --git a/api/src/sms-config/sms-config.service.spec.ts b/api/src/sms-config/sms-config.service.spec.ts index 15d6e5f..ffc2556 100644 --- a/api/src/sms-config/sms-config.service.spec.ts +++ b/api/src/sms-config/sms-config.service.spec.ts @@ -16,7 +16,6 @@ function createPrismaMock() { interfaceType: 'cmpp20', queuePriority: 'normal', tenant: { id: 'tenant-1', name: '租户A', code: 'TENANT-A' }, - messageRecords: [{ status: 'delivered' }, { status: 'undelivered' }], }]), findUnique: jest.fn().mockResolvedValue({ id: 'app-1', @@ -106,6 +105,12 @@ function createPrismaMock() { update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'downstream-1', ...data })), updateMany: jest.fn().mockResolvedValue({ count: 0 }), }, + smsMessageRecord: { + groupBy: jest.fn().mockResolvedValue([ + { applicationId: 'app-1', status: 'delivered', _count: { _all: 1 } }, + { applicationId: 'app-1', status: 'undelivered', _count: { _all: 1 } }, + ]), + }, smsChannel: { findFirst: jest.fn().mockResolvedValue({ id: 'channel-1', @@ -177,6 +182,14 @@ describe('SmsConfigService', () => { cmppConnections: [expect.objectContaining({ connectionId: 'gateway-1-1', account: '100001' })], }), ]); + expect(prisma.smsApplication.findMany).toHaveBeenCalledWith(expect.objectContaining({ + include: { tenant: true, ipAllowlist: true }, + })); + expect(prisma.smsApplication.findMany.mock.calls[0][0]).not.toHaveProperty('take'); + expect(prisma.smsMessageRecord.groupBy).toHaveBeenCalledWith(expect.objectContaining({ + by: ['applicationId', 'status'], + _count: { _all: true }, + })); }); it('returns CMPP params from persisted application and channel config', async () => { diff --git a/api/src/sms-config/sms-config.service.ts b/api/src/sms-config/sms-config.service.ts index fdd44e7..0a560c3 100644 --- a/api/src/sms-config/sms-config.service.ts +++ b/api/src/sms-config/sms-config.service.ts @@ -123,7 +123,7 @@ export class SmsConfigService { if (query.includeConnections) { await this.markTimedOutDownstreamConnections(); } - return this.prisma.smsApplication.findMany({ + const applications = await this.prisma.smsApplication.findMany({ where: { tenantId: query.tenantId, OR: query.keyword ? [ @@ -134,32 +134,36 @@ export class SmsConfigService { include: { tenant: true, ipAllowlist: true, - messageRecords: { where: { queuedAt: { gte: startOfToday() } }, take: 1000 }, }, orderBy: { createdAt: 'desc' }, - take: 100, - }).then(async (applications) => { - if (!query.includeConnections) { - return applications; - } - const applicationIds = applications.map((application) => application.id); - const connections = await this.prisma.cmppDownstreamConnection.findMany({ + }); + if (!query.includeConnections) { + return applications; + } + const applicationIds = applications.map((application) => application.id); + const [connections, messageStats] = await Promise.all([ + this.prisma.cmppDownstreamConnection.findMany({ where: { applicationId: { in: applicationIds } }, orderBy: { updatedAt: 'desc' }, - take: 500, - }); - return applications.map((application) => { - const appConnections = connections.filter((connection) => connection.applicationId === application.id); - const todayTotal = application.messageRecords.length; - const delivered = application.messageRecords.filter((message) => message.status === 'delivered').length; - return { - ...application, - cmppConnections: appConnections, - cmppStatus: normalizeApplicationCmppStatus(appConnections, application.status), - sentToday: todayTotal, - deliveryRate: todayTotal > 0 ? Number(((delivered / todayTotal) * 100).toFixed(1)) : 0, - }; - }); + }), + this.prisma.smsMessageRecord.groupBy({ + by: ['applicationId', 'status'], + where: { applicationId: { in: applicationIds }, queuedAt: { gte: startOfToday() } }, + _count: { _all: true }, + }), + ]); + return applications.map((application) => { + const appConnections = connections.filter((connection) => connection.applicationId === application.id); + const appStats = messageStats.filter((item) => item.applicationId === application.id); + const todayTotal = appStats.reduce((sum, item) => sum + item._count._all, 0); + const delivered = appStats.find((item) => item.status === 'delivered')?._count._all ?? 0; + return { + ...application, + cmppConnections: appConnections, + cmppStatus: normalizeApplicationCmppStatus(appConnections, application.status), + sentToday: todayTotal, + deliveryRate: todayTotal > 0 ? Number(((delivered / todayTotal) * 100).toFixed(1)) : 0, + }; }); } @@ -366,7 +370,6 @@ export class SmsConfigService { const connections = await this.prisma.cmppDownstreamConnection.findMany({ where: { applicationId }, orderBy: { updatedAt: 'desc' }, - take: 100, }); return { application, @@ -513,7 +516,6 @@ export class SmsConfigService { }, include: { materials: true, tenant: true, application: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); } @@ -596,7 +598,6 @@ export class SmsConfigService { }, include: { variables: true, application: true, tenant: true, signature: true }, orderBy: { createdAt: 'desc' }, - take: 100, }); } @@ -696,7 +697,6 @@ export class SmsConfigService { targetId, }, orderBy: { createdAt: 'desc' }, - take: 100, }); } diff --git a/api/src/tenants/tenants.service.spec.ts b/api/src/tenants/tenants.service.spec.ts index ab9546a..2b976b6 100644 --- a/api/src/tenants/tenants.service.spec.ts +++ b/api/src/tenants/tenants.service.spec.ts @@ -93,6 +93,8 @@ describe('TenantsService', () => { expect(prisma.tenant.findMany).toHaveBeenCalledWith(expect.objectContaining({ where: { status: { not: 'deleted' } }, })); + expect(prisma.tenant.findMany.mock.calls[0][0]).not.toHaveProperty('take'); + expect(prisma.tenantAccount.findMany).toHaveBeenCalledWith(); expect(prisma.smsMessageRecord.groupBy).toHaveBeenCalledWith(expect.objectContaining({ by: ['tenantId'], _sum: { amountCents: true }, diff --git a/api/src/tenants/tenants.service.ts b/api/src/tenants/tenants.service.ts index c462fab..d894f46 100644 --- a/api/src/tenants/tenants.service.ts +++ b/api/src/tenants/tenants.service.ts @@ -40,7 +40,6 @@ export class TenantsService { return this.prisma.tenant.findMany({ include: { enterpriseCertifications: { orderBy: { submittedAt: 'desc' }, take: 1 } }, orderBy: { createdAt: 'desc' }, - take: 100, }).then((items) => items.map(withEnterpriseProfile)); } @@ -51,9 +50,8 @@ export class TenantsService { where: { status: { not: 'deleted' } }, include: { enterpriseCertifications: { orderBy: { submittedAt: 'desc' }, take: 1 } }, orderBy: { createdAt: 'desc' }, - take: 100, }), - this.prisma.tenantAccount.findMany({ take: 200 }), + this.prisma.tenantAccount.findMany(), this.prisma.smsMessageRecord.groupBy({ by: ['tenantId'], where: { queuedAt: { gte: sinceToday } }, diff --git a/api/src/users/users.service.ts b/api/src/users/users.service.ts index 18014e5..331e356 100644 --- a/api/src/users/users.service.ts +++ b/api/src/users/users.service.ts @@ -79,7 +79,6 @@ export class UsersService { }, include: { tenant: true, roles: { include: { role: true } } }, orderBy: { createdAt: 'desc' }, - take: 200, }); } @@ -240,7 +239,6 @@ export class UsersService { return this.prisma.role.findMany({ include: { permissions: { include: { permission: true } } }, orderBy: { createdAt: 'desc' }, - take: 100, }); } @@ -256,7 +254,7 @@ export class UsersService { } listPermissions() { - return this.prisma.permission.findMany({ orderBy: { createdAt: 'desc' }, take: 200 }); + return this.prisma.permission.findMany({ orderBy: { createdAt: 'desc' } }); } createPermission(data: CreatePermissionDto) { diff --git a/docs/testing-progress.md b/docs/testing-progress.md index 5dd073e..da1badd 100644 --- a/docs/testing-progress.md +++ b/docs/testing-progress.md @@ -1590,3 +1590,12 @@ git diff --check - 短信任务进度详情中的“号码运营商分布”和“号码省份分布”均直接聚合任务内真实短信记录的 `carrier/province`。 - 已执行 `npm --prefix api test -- --runInBand send-chain.service.spec.ts`(35 项通过)、`npm --prefix api run build`、`npm run build` 和 `git diff --check`;前端仅有既有 chunk size warning。 - 已将 `7b8424d9` 部署生产,migration `20260711210000_add_message_route_identity` 成功应用。`cmpp-api`、`cmpp-gateway`、Nginx、MinIO 均为 active,`12026/17890/8090/3000` 监听、API/Gateway health 及外部 `12026` HTTP 均通过。生产 11 条已有短信已全部回填运营商和省份:移动 9 条、电信 1 条、联通 1 条,省份均为上海。 + +## 2026-07-12 全页面固定条数截断与总数口径整改 + +- 生产核查确认企业管理只返回前 100 条,但 PostgreSQL 实际有 106 个企业(未删除 105 个);企业应用只返回前 100 条,实际有 204 条。页面将截断后的数组长度展示为“企业总数/共找到”,属于真实数据口径 Bug。 +- 已系统审计页面主列表 API,移除会把前 `100/200/500` 条直接作为页面全量的固定截断。覆盖企业、企业应用、企业认证、用户/角色/权限、签名、模板、通道/通道组/路由规则、报备字段/材料/任务/记录、账务、文件、审计、风控、敏感词/黑名单/引流字段、短信任务/记录/提交/回执/上行等真实列表。 +- 企业应用的“今日发送/到达率”不再每应用加载最多 1000 条 `SmsMessageRecord` 后用数组长度计算,改为 PostgreSQL 按 `applicationId/status` 的 `groupBy` 全量聚合。 +- 短信任务进度不再为每个任务最多加载 100000 条短信后统计,改为 PostgreSQL 按 `batchTaskId/carrier/province/status` 聚合总数、成功数和计费条数,任务运营商/省份分布不再受 10 万条截断影响。 +- 保留的固定条数均属于明确的近期日志/监控窗口、超时扫描单批、导出保护、单消息重试尝试或唯一候选判定,这些响应不被页面展示为业务总数。 +- 已执行 API 全量测试(13 suites、122 项通过),及受影响服务定向测试(5 suites、91 项通过)、API build、前端 build 与 `git diff --check`;前端仅有既有 chunk size warning。本轮尚未提交或部署生产。 diff --git a/src/api/adminApi.ts b/src/api/adminApi.ts index 12f5a84..2de61de 100644 --- a/src/api/adminApi.ts +++ b/src/api/adminApi.ts @@ -330,6 +330,14 @@ export type SmsBatchTask = { 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 = { diff --git a/src/apps/admin/AdminSmsTaskProgressPage.tsx b/src/apps/admin/AdminSmsTaskProgressPage.tsx index 6b7bdef..29aba2c 100644 --- a/src/apps/admin/AdminSmsTaskProgressPage.tsx +++ b/src/apps/admin/AdminSmsTaskProgressPage.tsx @@ -125,6 +125,31 @@ function buildRegionStats(messages: SmsMessageRecord[] | undefined): RegionStat[ return Array.from(stats.values()).sort((a, b) => b.total - a.total); } +function buildCarrierStatsFromAggregates(stats: SmsBatchTask['messageStats']): CarrierStat[] { + const totals = new Map(); + (stats ?? []).forEach((item) => { + const carrier = item.carrier ?? 'unknown'; + const meta = carrierLabels[carrier] ?? { label: carrier || '未识别', tone: 'mobile' as const }; + const current = totals.get(carrier) ?? { name: meta.label, total: 0, success: 0, tone: meta.tone }; + current.total += item._count._all; + if (item.status === 'delivered') current.success += item._count._all; + totals.set(carrier, current); + }); + return Array.from(totals.values()); +} + +function buildRegionStatsFromAggregates(stats: SmsBatchTask['messageStats']): RegionStat[] { + const totals = new Map(); + (stats ?? []).forEach((item) => { + const region = item.province ?? '未识别省份'; + const current = totals.get(region) ?? { region, total: 0, success: 0 }; + current.total += item._count._all; + if (item.status === 'delivered') current.success += item._count._all; + totals.set(region, current); + }); + return Array.from(totals.values()).sort((a, b) => b.total - a.total); +} + function mapTask(task: SmsBatchTask): SmsTask { const messages = task.messages ?? []; const submittedStatuses = ['submitted', 'delivered', 'failed', 'unknown', 'timeout', 'submit_failed']; @@ -134,7 +159,8 @@ function mapTask(task: SmsBatchTask): SmsTask { const failedCount = task.failedTotal ?? countMessages(messages, failedStatuses); // submittedTotal already includes unknown and timeout records, so never add them again. const processedCount = Math.max(submittedCount, successCount + failedCount + (task.unknownTotal ?? 0)); - const billingCount = messages.reduce((sum, message) => sum + (message.billingUnits ?? 0), 0) + const billingCount = (task.messageStats ?? []).reduce((sum, item) => sum + (item._sum.billingUnits ?? 0), 0) + || messages.reduce((sum, message) => sum + (message.billingUnits ?? 0), 0) || task.phoneTotal * (task.template?.billingUnits ?? Math.max(1, Math.ceil([...task.content].length / 67))); return { @@ -156,8 +182,8 @@ function mapTask(task: SmsBatchTask): SmsTask { failedCount, status: normalizeTaskStatus(task.status), rawStatus: task.status, - carriers: buildCarrierStats(messages), - regions: buildRegionStats(messages), + carriers: task.messageStats ? buildCarrierStatsFromAggregates(task.messageStats) : buildCarrierStats(messages), + regions: task.messageStats ? buildRegionStatsFromAggregates(task.messageStats) : buildRegionStats(messages), }; }