fix: accept partially reported CMPP signatures
This commit is contained in:
@@ -596,6 +596,55 @@ describe('SendChainService', () => {
|
|||||||
where: { id: 'task-1' },
|
where: { id: 'task-1' },
|
||||||
data: expect.objectContaining({ status: 'pending_review', riskTaskId: 'review-task-1', auditStatus: 'pending' }),
|
data: expect.objectContaining({ status: 'pending_review', riskTaskId: 'review-task-1', auditStatus: 'pending' }),
|
||||||
});
|
});
|
||||||
|
expect(prisma.smsSignature.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
applicationId: 'app-1',
|
||||||
|
name: '【签名】',
|
||||||
|
auditStatus: 'approved',
|
||||||
|
},
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
|
});
|
||||||
|
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts an approved bracketed signature when only part of its channels are reported', async () => {
|
||||||
|
const { service, prisma, riskReview } = createService();
|
||||||
|
prisma.smsApplication.findFirst.mockResolvedValue({
|
||||||
|
id: 'app-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
cmppAccount: '100001',
|
||||||
|
status: 'active',
|
||||||
|
interfaceEnabled: true,
|
||||||
|
templateMismatchMode: 'manual_review',
|
||||||
|
customerUnitPrice: 3,
|
||||||
|
queuePriority: 'normal',
|
||||||
|
ipAllowlist: [{ ipCidr: '127.0.0.1/32' }],
|
||||||
|
tenant: { id: 'tenant-1', status: 'active', certificationStatus: 'approved' },
|
||||||
|
});
|
||||||
|
prisma.smsTemplate.findFirst.mockResolvedValue(null);
|
||||||
|
prisma.smsSignature.findFirst.mockResolvedValue({
|
||||||
|
id: 'sig-1',
|
||||||
|
name: '【航天信息信诺网】',
|
||||||
|
auditStatus: 'approved',
|
||||||
|
reportStatus: 'reporting',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(service.submitInboundMessage({
|
||||||
|
account: '100001',
|
||||||
|
phoneNumber: '18821203795',
|
||||||
|
content: '【航天信息信诺网】您本次操作的验证码是171102,有效时10分钟。',
|
||||||
|
remoteIp: '127.0.0.1',
|
||||||
|
})).resolves.toEqual(expect.objectContaining({ accepted: true, messageRecordId: 'record-1' }));
|
||||||
|
|
||||||
|
expect(prisma.smsSignature.findFirst).toHaveBeenCalledWith({
|
||||||
|
where: {
|
||||||
|
applicationId: 'app-1',
|
||||||
|
name: '【航天信息信诺网】',
|
||||||
|
auditStatus: 'approved',
|
||||||
|
},
|
||||||
|
orderBy: { updatedAt: 'desc' },
|
||||||
|
});
|
||||||
|
expect(riskReview.aggregateTemplateMismatch).toHaveBeenCalledWith(expect.objectContaining({ signatureId: 'sig-1' }));
|
||||||
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
|
expect(prisma.smsReceiptRecord.create).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1569,7 +1569,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
} else if (!template && application.templateMismatchMode === 'manual_review') {
|
} else if (!template && application.templateMismatchMode === 'manual_review') {
|
||||||
const signature = await this.resolveInboundSignatureCandidate(application.id, data.content);
|
const signature = await this.resolveInboundSignatureCandidate(application.id, data.content);
|
||||||
if (!signature) {
|
if (!signature) {
|
||||||
await reject('SIGNATURE', '短信内容未识别到已审核且已报备的签名');
|
await reject('SIGNATURE', '短信内容未识别到已审核通过的签名');
|
||||||
} else {
|
} else {
|
||||||
const risk = await this.riskReview.evaluateTask({
|
const risk = await this.riskReview.evaluateTask({
|
||||||
tenantId: application.tenantId,
|
tenantId: application.tenantId,
|
||||||
@@ -2012,14 +2012,13 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private resolveInboundSignatureCandidate(applicationId: string, content: string) {
|
private resolveInboundSignatureCandidate(applicationId: string, content: string) {
|
||||||
const match = content.match(/^【([^】]+)】/);
|
const match = content.match(/^【[^】]+】/);
|
||||||
if (!match?.[1]) return null;
|
if (!match?.[0]) return null;
|
||||||
return this.prisma.smsSignature.findFirst({
|
return this.prisma.smsSignature.findFirst({
|
||||||
where: {
|
where: {
|
||||||
applicationId,
|
applicationId,
|
||||||
name: match[1],
|
name: match[0],
|
||||||
auditStatus: 'approved',
|
auditStatus: 'approved',
|
||||||
reportStatus: 'approved',
|
|
||||||
},
|
},
|
||||||
orderBy: { updatedAt: 'desc' },
|
orderBy: { updatedAt: 'desc' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,7 +147,7 @@
|
|||||||
12. 企业应用“不符合模板的短信”配置为 `manual_review` 时,合法的 CMPP Submit 在模板不匹配后进入人工审核;配置为 `reject` 时仍直接拒绝并返回 `REJECTD` Deliver Receipt,其他模式不得被人工审核聚合逻辑误接管。
|
12. 企业应用“不符合模板的短信”配置为 `manual_review` 时,合法的 CMPP Submit 在模板不匹配后进入人工审核;配置为 `reject` 时仍直接拒绝并返回 `REJECTD` Deliver Receipt,其他模式不得被人工审核聚合逻辑误接管。
|
||||||
13. CMPP 模板不匹配审核支持短窗口内容指纹聚合:只有同一企业应用、同一 CMPP 账号、规范化后内容 SHA-256 完全一致且位于同一时间窗口的短信才能合并为一个审核任务。默认窗口 10 秒,可通过 `CMPP_TEMPLATE_REVIEW_WINDOW_MS` 调整。
|
13. CMPP 模板不匹配审核支持短窗口内容指纹聚合:只有同一企业应用、同一 CMPP 账号、规范化后内容 SHA-256 完全一致且位于同一时间窗口的短信才能合并为一个审核任务。默认窗口 10 秒,可通过 `CMPP_TEMPLATE_REVIEW_WINDOW_MS` 调整。
|
||||||
14. 聚合审核不合并短信记录、计费或回执:每个手机号仍有独立 `SmsMessageRecord/messageId/sequenceId`。审核通过后逐条进入真实路由和上游提交;审核驳回后逐条释放冻结并产生客户侧 `REJECTD` 回执。
|
14. 聚合审核不合并短信记录、计费或回执:每个手机号仍有独立 `SmsMessageRecord/messageId/sequenceId`。审核通过后逐条进入真实路由和上游提交;审核驳回后逐条释放冻结并产生客户侧 `REJECTD` 回执。
|
||||||
15. 人工审核只覆盖模板不匹配;签名必须能从短信前缀识别且已审核/报备通过。签名不合法、风控直接拒绝或余额不足不得因内容聚合而绕过。
|
15. 人工审核只覆盖模板不匹配;签名必须以完整中文中括号前缀 `【签名】` 识别,并使用包含中括号的完整名称匹配签名库。入站候选签名只要求 `auditStatus=approved`,不得以全局 `reportStatus` 提前拒绝;报备通过状态必须在后续路由和最终提交前按具体通道校验。签名不合法、风控直接拒绝或余额不足不得因内容聚合而绕过。
|
||||||
16. 发送入队必须按短信应用的队列等级分流到优先队列或普通队列;同等条件下优先队列消息必须先于普通队列消息被 Send Worker 消费并提交 Gateway。
|
16. 发送入队必须按短信应用的队列等级分流到优先队列或普通队列;同等条件下优先队列消息必须先于普通队列消息被 Send Worker 消费并提交 Gateway。
|
||||||
17. 优先队列只能改变待发送消息的调度顺序,不得绕过企业/应用状态、签名模板审核、通道报备、余额/授信、黑名单、风控、通道组路由、通道限速和 Gateway 连接可用性校验。
|
17. 优先队列只能改变待发送消息的调度顺序,不得绕过企业/应用状态、签名模板审核、通道报备、余额/授信、黑名单、风控、通道组路由、通道限速和 Gateway 连接可用性校验。
|
||||||
18. 同一队列内部按创建时间、任务顺序和手机号拆分顺序保持 FIFO 或可解释的稳定排序;优先队列插队时必须可在 trace 或任务日志中追踪队列等级和入队时间。
|
18. 同一队列内部按创建时间、任务顺序和手机号拆分顺序保持 FIFO 或可解释的稳定排序;优先队列插队时必须可在 trace 或任务日志中追踪队列等级和入队时间。
|
||||||
|
|||||||
@@ -1139,7 +1139,20 @@
|
|||||||
- 不同应用、账号、内容指纹或窗口的短信不合并。
|
- 不同应用、账号、内容指纹或窗口的短信不合并。
|
||||||
- 应用 B 不进入人工审核,继续逐条产生 `REJECTD` Deliver Receipt。
|
- 应用 B 不进入人工审核,继续逐条产生 `REJECTD` Deliver Receipt。
|
||||||
- 审核通过后每条成员独立进入路由、提交和计费;审核驳回后每条成员独立释放冻结并向客户下发 `REJECTD`。
|
- 审核通过后每条成员独立进入路由、提交和计费;审核驳回后每条成员独立释放冻结并向客户下发 `REJECTD`。
|
||||||
- 签名无法识别/未报备、风控直接拒绝或余额不足时不进入聚合审核。
|
- 签名无法识别、未审核通过、风控直接拒绝或余额不足时不进入聚合审核;签名报备状态由后续具体通道路由校验。
|
||||||
|
|
||||||
|
### TC-SEND-039A CMPP 完整括号签名与部分通道报备放行
|
||||||
|
|
||||||
|
- 优先级:P0
|
||||||
|
- 前置条件:应用配置 `templateMismatchMode=manual_review`;签名库名称为完整的 `【航天信息信诺网】`,`auditStatus=approved`、全局 `reportStatus=reporting`;主通道签名任务 approved、备用通道签名任务 pending。
|
||||||
|
- 步骤:
|
||||||
|
1. 通过真实 CMPP 入站提交 `【航天信息信诺网】您本次操作的验证码是171102,有效时间10分钟。`。
|
||||||
|
2. 查看入站签名查询、人工审核聚合记录和失败回执。
|
||||||
|
3. 审核通过后查看短信路由与 Gateway SubmitCommand。
|
||||||
|
- 预期结果:
|
||||||
|
- 入站签名使用完整的 `【航天信息信诺网】` 查询,不剥离中括号,不以无括号名称查询。
|
||||||
|
- 全局 `reportStatus=reporting` 不在入站阶段触发 `SIGNATURE` 拒绝,短信进入真实人工审核聚合链路且不产生签名失败回执。
|
||||||
|
- 审核通过后只允许选择签名任务为 approved 的主通道,不能选择 pending 的备用通道;最终提交前继续执行同一通道级校验。
|
||||||
|
|
||||||
### TC-GW-007 CMPP 客户到上游 SMSC 完整闭环
|
### TC-GW-007 CMPP 客户到上游 SMSC 完整闭环
|
||||||
|
|
||||||
|
|||||||
@@ -1699,3 +1699,10 @@ git diff --check
|
|||||||
- 本地真实 PostgreSQL 已应用 `20260714100000_add_report_record_source_entry`,并通过编译后的 NestJS 服务对现有应用、签名、模板执行真实组合查询。API 全量测试 13 suites、141 项通过;Prisma validate、API build、前端 build、Gateway 测试和 `git diff --check` 通过。应用内浏览器确认真实鉴权跳转、页面标题、非空 DOM、无框架错误覆盖及 console 无 error/warn;因图形验证码未获授权代解,登录后页面点击验收未执行。
|
- 本地真实 PostgreSQL 已应用 `20260714100000_add_report_record_source_entry`,并通过编译后的 NestJS 服务对现有应用、签名、模板执行真实组合查询。API 全量测试 13 suites、141 项通过;Prisma validate、API build、前端 build、Gateway 测试和 `git diff --check` 通过。应用内浏览器确认真实鉴权跳转、页面标题、非空 DOM、无框架错误覆盖及 console 无 error/warn;因图形验证码未获授权代解,登录后页面点击验收未执行。
|
||||||
- 功能提交 `64216712` 与历史入口修正提交 `70991478` 均已 push,生产最终运行代码为 `709914787196072b66d293ee9290d7cd41b46990`。首次备份为 `/opt/cmpp-platform/backups/cmpp-20260714-095958.sql` 和 `source-20260714-095958.tar.gz`,最终修正部署前备份为 `/opt/cmpp-platform/backups/cmpp-20260714-100658.sql` 和 `source-20260714-100658.tar.gz`;发布包本地与服务器 SHA-256 一致。
|
- 功能提交 `64216712` 与历史入口修正提交 `70991478` 均已 push,生产最终运行代码为 `709914787196072b66d293ee9290d7cd41b46990`。首次备份为 `/opt/cmpp-platform/backups/cmpp-20260714-095958.sql` 和 `source-20260714-095958.tar.gz`,最终修正部署前备份为 `/opt/cmpp-platform/backups/cmpp-20260714-100658.sql` 和 `source-20260714-100658.tar.gz`;发布包本地与服务器 SHA-256 一致。
|
||||||
- 生产 36 条 migration 全部应用,四项服务 active,`12026/17890/8090/3000` 监听,API/Gateway health 和外部 HTTP 200,部署后 journal 无新 error。真实报备记录 API 返回 16 条且通道名称缺失数为 0;7 条系统记录为 `system`,9 条旧人工记录为 `legacy`。用生产现有数据调用独立组合查询,企业应用、模板、签名和引流信息分组均各返回 1 个精确匹配结果;企业黑名单当前为 0 条,未注入演示数据。
|
- 生产 36 条 migration 全部应用,四项服务 active,`12026/17890/8090/3000` 监听,API/Gateway health 和外部 HTTP 200,部署后 journal 无新 error。真实报备记录 API 返回 16 条且通道名称缺失数为 0;7 条系统记录为 `system`,9 条旧人工记录为 `legacy`。用生产现有数据调用独立组合查询,企业应用、模板、签名和引流信息分组均各返回 1 个精确匹配结果;企业黑名单当前为 0 条,未注入演示数据。
|
||||||
|
|
||||||
|
## 2026-07-14 CMPP 入站完整括号签名与部分通道放行修复
|
||||||
|
|
||||||
|
- 生产只读核查手机号 `18821203795` 的最近一次提交:Gateway 已接收入站,但 NestJS 在路由前以 `SIGNATURE / 短信内容未识别到已审核且已报备的签名` 拒绝,未生成通道提交记录。实际短信前缀和签名库名称均为完整的 `【航天信息信诺网】`;签名审核已通过、全局报备状态为 reporting,主通道任务 approved、备用通道任务 pending。
|
||||||
|
- 根因是入站签名解析正则取捕获组后剥离了中括号,却用无括号名称查询保存完整括号的签名库;同时查询错误要求全局 `SmsSignature.reportStatus=approved`,与“部分通道通过即可发送、路由只选通过通道”的既定规则冲突。
|
||||||
|
- 修复为从短信开头提取完整 `【签名】` 并原样查询,仅在入站候选阶段校验 `auditStatus=approved`。全局 `reportStatus` 不再作为入口门禁,具体通道的 `ChannelSignatureReportTask(reportType=signature).status=approved` 仍由路由和最终提交二次校验。
|
||||||
|
- 新增真实故障形态回归用例:完整括号签名、审核通过、全局 reporting 时可进入模板不匹配人工审核聚合,并验证查询不再携带全局报备条件、不产生签名失败回执。定向 API 测试 1 suite、41 项通过;API 全量 13 suites、142 项通过,API build、前端 build、Gateway 全量 Go 测试和 `git diff --check` 通过,前端仅有既有 Vite chunk size warning。生产部署结果待本批次完成后补录。
|
||||||
|
|||||||
Reference in New Issue
Block a user