# CMPP 接入号上下游配置与匹配流程设计 ## 1. 文档目的 本文梳理当前 CMPP 短信平台接入号的真实代码、生产数据和协议字段使用情况,并给出客户侧下游 CMPP 接入号、运营商侧上游 CMPP 接入号、号码映射、普通上行匹配和历史追溯的目标设计。 本文仅描述设计结论,不代表相关改造已经完成。后续实现仍必须基于真实 NestJS API、Prisma/PostgreSQL、Go Gateway、Redis 和生产 CMPP 链路验证,不允许使用前端静态数据、localStorage 或 mock 代替业务完成。 ## 2. 结论摘要 当前系统已经具备“通道接入号下发、上行接收、模糊匹配、人工认领”的第一版链路,但尚未形成真正的应用级接入号分配和上下游映射模型。 现状核心问题如下: 1. 上游通道接入号和客户应用接入号混用。 2. 客户侧展示的接入地址、端口和接入号来自任意一条上游通道,而不是平台客户接入端点和本应用号码。 3. 客户 CMPP SUBMIT 中的 `Src_Id` 虽已传给 NestJS,但未校验是否属于当前应用,也未参与后续通道号码映射。 4. 多个应用绑定同一通道组时,上行 `Dest_Id` 会匹配到该组全部应用,无法唯一定位。 5. `extensionDigits` 目前只是配置和队列元数据,Go Gateway 没有根据它生成或匹配应用扩展号。 6. 上游 CMPP SUBMIT 的 `MsgSrc` 当前使用通道登录账号,而不是通道企业代码。 7. 普通上行没有完整保留 `Service_Id`、`LinkID`、原始接入号和规范化接入号。 8. 应用级 `cmppEnterpriseCode` 当前允许最长 32 字符,与 CMPP SUBMIT 中 `MsgSrc` 固定 6 字节的字段边界不一致。 生产只读检查显示: - 当前 3 条通道的 `srcId` 均为 `1069999999`。 - 其中 2 条通道为 active,1 条为 disabled。 - 生产通道未配置 `serviceId` 和有效扩展位数。 - 4 个真实应用路由到同一个通道组和同一基础接入号。 - 当前 2 条普通上行的 `destId` 都是 `1069999999`,匹配状态均为 `ambiguous`。 - 生产存在大量批量验证应用,不应自动分配正式生产接入号。 因此,当前共享基础接入号只能支撑模糊候选和人工认领,不能作为多应用上行自动路由的生产方案。 ## 3. CMPP 字段边界 接入号设计必须区分以下概念: | 概念 | CMPP 字段 | 含义 | 当前状态 | | --- | --- | --- | --- | | 登录账号 | CONNECT `Source_Addr` | 建立 CMPP TCP 会话的账号,协议字段为 6 字节 | 上下游已有账号字段 | | 企业代码 | SUBMIT `MsgSrc` | SP 身份、地址翻译、计费和结算标识,协议字段为 6 字节 | 下游应用允许过长;上游错误使用登录账号 | | 业务代码 | `Service_Id` | 业务类型,最长 10 字节 | 藏在通道 JSON 中,生产未配置 | | 下发接入号 | SUBMIT `Src_Id` | 服务代码或以服务代码为前缀的长号码,最长 21 字节 | 当前只有通道级基础号码 | | 上行接入号 | DELIVER `Dest_Id` | 用户实际回复到的服务代码或长号码 | 当前仅按通道基础号反查路由组 | | 上行手机号 | DELIVER `Src_Terminal_Id` | 发送普通上行的用户手机号 | 当前已处理 | | 点播关联标识 | `LinkID` | 点播类业务的关联标识 | 当前未传入 NestJS | CMPP 2.0/3.0 协议要求: - `MsgSrc` 和 CONNECT 登录账号是两个不同概念,即使运营商配置值相同,也必须在数据模型中分开保存。 - `Src_Id` 可以是基础服务代码,也可以是以服务代码为前缀的长号码。 - 普通上行 DELIVER 中,`Dest_Id` 是用户回复的接入号,`Src_Terminal_Id` 是用户手机号。 - 状态报告 DELIVER 和普通上行 DELIVER 必须通过 `Registered_Delivery` 严格分流。 - 普通上行自身的 `Msg_Id` 不能默认当作原下发消息 ID。 协议参考: - [CMPP 2.0 协议](https://www.kannel.org/~tolj/specs/CMPP2/CMPP-2.0.pdf) - [CMPP 3.0 协议](https://www.kannel.org/~tolj/specs/CMPP2/CMPP-v30.pdf) 行业监管要求端口类短信按照批准的码号结构、位长、用途、使用范围和期限使用,并保存发送时间、接收时间、发送端、接收端和内容等记录。因此系统不能因为协议字段最大长度为 21 字节,就允许任意拼接扩展码;扩展长度必须依据具体运营商合同和报备结果配置。 行业规则参考: - [通信短信息服务管理规定](https://www.miit.gov.cn/zwgk/zcwj/flfg/art/2020/art_77bc7219833c4a08b4563ba42ad23e1f.html) - [电信网编号计划(2017 年版)](https://www.miit.gov.cn/jgsj/xgj/wjfb/art/2020/art_eb0adf5b6e7148cbb70802b264878b1e.html) - [电信网码号资源审批服务指南](https://qhca.miit.gov.cn/zwgk/txfz/hmzy/art/2021/art_754e60bc98664ad4a91748a37d29c6bc.html) ## 4. 当前真实实现 ### 4.1 当前下发流程 1. 应用通过页面、HTTP API 或客户侧 CMPP 提交短信。 2. NestJS 根据应用、运营商、省份和通道组选择上游通道。 3. `SubmitCommand.cmpp.srcId` 直接取 `SmsChannel.srcId`。 4. Go Gateway 构造运营商侧 CMPP SUBMIT。 5. 上游 `MsgSrc` 当前取通道登录账号。 6. 上游 `Src_Id` 取通道基础接入号。 7. 所有走同一通道的应用对外使用同一基础号码。 相关代码: - `api/src/send-chain/send-chain.service.ts`:生成 `SubmitCommand`。 - `gateway/internal/upstream/manager.go`:构造 CMPP 2.0/3.0 SUBMIT 报文。 ### 4.2 当前客户 CMPP 入站流程 1. 客户通过 6 位账号 CONNECT。 2. Gateway 根据会话账号定位应用,并校验应用企业代码。 3. 客户 SUBMIT 的 `Src_Id` 被传给 NestJS。 4. NestJS 未校验该号码是否分配给当前应用。 5. NestJS 未把客户 `Src_Id` 保存到短信记录或提交记录。 6. 后续上游发送重新取通道 `srcId`,客户提交号码被丢弃。 相关代码: - `gateway/internal/inbound/server.go`:客户 CMPP CONNECT/SUBMIT 解析。 - `api/src/send-chain/send-chain.service.ts`:`submitInboundMessage`。 ### 4.3 当前普通上行流程 1. 运营商通过 CMPP DELIVER 返回用户手机号和 `Dest_Id`。 2. Gateway 将手机号、接入号和内容回调 NestJS。 3. NestJS 尝试按 messageId 匹配。 4. 未匹配时,根据“上行通道所属通道组”查询所有绑定该组的应用。 5. 多应用共用通道组时生成 `ambiguous` 候选。 6. 无接入号候选时,再按手机号和最近 72 小时下发记录匹配。 7. 最后进入人工认领。 当前流程能避免多候选时误推客户,但不能提供生产级接入号唯一路由。 相关代码: - `gateway/internal/upstream/manager.go`:解析普通上行 DELIVER。 - `api/src/send-chain/send-chain.service.ts`:`resolveUplinkMatch`。 ### 4.4 当前客户 CMPP 参数接口问题 `SmsApplication` 没有应用接入号字段。`getApplicationCmppParams` 会查询最新一条未删除的 `SmsChannel`,将该上游通道的地址、端口和 `srcId` 返回给客户。 这会产生以下错误: - 客户看到运营商侧上游网关地址,而不是平台客户接入地址。 - 不同应用得到同一个任意通道接入号。 - 上游通道新增、删除或排序变化可能改变客户展示参数。 - 应用接入参数与实际客户连接到 `8.160.169.106:17890` 的生产事实不一致。 相关代码:`api/src/sms-config/sms-config.service.ts` 的 `getApplicationCmppParams`。 ## 5. 目标数据模型 目标模型分为平台接入端点、上游号码能力、应用虚拟接入号和上下游绑定四层。 ### 5.1 `CmppDownstreamEndpoint` 保存客户连接本平台的公共端点: - `id` - `name` - `gatewayHost` - `gatewayPort` - `supportedVersions` - `heartbeatSeconds` - `status` - `effectiveFrom` - `effectiveTo` 生产客户参数应返回平台端点 `8.160.169.106:17890` 或后续正式域名/TLS 地址,不再读取任意上游通道。 ### 5.2 `UpstreamAccessNumber` 表示运营商或上游供应商在某条通道上实际开通的号码能力: - `id` - `channelId` - `enterpriseCode`:上游 `MsgSrc`,严格 6 字节。 - `serviceId`:最长 10 字节。 - `baseNumber` - `extensionMode`:`none/fixed/allocated/shared`。 - `allowedExtensionLengths`:由合同或报备决定。 - `maxTotalLength`:不得超过 21。 - `uplinkReturnMode`:`full/base_only/truncated/custom`。 - `replySupported` - `reportStatus` - `effectiveFrom` - `effectiveTo` - `status` 不能把扩展位数设成全平台统一常量。不同运营商、供应商和通道允许的扩展位数可能不同,应由通道号码能力明确配置。 ### 5.3 `ApplicationAccessNumber` 表示客户应用看到、提交和收到普通上行时使用的号码: - `id` - `tenantId` - `applicationId` - `endpointId` - `accessNumber` - `serviceId` - `isDefault` - `direction`:`mt/mo/both`。 - `replyEnabled` - `shareMode`:`exclusive/shared`。 - `effectiveFrom` - `effectiveTo` - `status` 约束: 1. 同一活动号码原则上只能属于一个应用。 2. 允许共享时必须显式标记 `shared`。 3. 共享号码不能仅按接入号直接自动投递。 4. 应用开放 CMPP 提交前必须至少有一个默认活动号码。 5. 客户 SUBMIT 中的 `Src_Id` 必须属于当前已鉴权应用。 ### 5.4 `AccessNumberBinding` 表示客户应用号码与各上游通道号码之间的真实映射: - `id` - `applicationAccessNumberId` - `channelId` - `upstreamAccessNumberId` - `upstreamFullNumber` - `upstreamExtension` - `serviceIdOverride` - `carrier` - `province` - `priority` - `matchMode` - `effectiveFrom` - `effectiveTo` - `status` 关键约束: - 可自动回复的号码,活动状态下 `(channelId, upstreamFullNumber)` 必须唯一。 - 多应用共享同一上游号码时,必须标记共享并禁止仅凭接入号自动投递。 - 激活绑定前校验号码前缀、长度、运营商、报备状态和通道状态。 - 每个需要发送的运营商/省份路由必须有可用号码绑定,否则该通道不是有效候选。 ### 5.5 短信和提交快照 在 `SmsMessageRecord` 或 `SmsSubmitRecord` 中增加不可变快照: - `applicationAccessNumberId` - `accessNumberBindingId` - `clientSrcId` - `upstreamSrcId` - `upstreamMsgSrc` - `upstreamServiceId` - `upstreamChannelId` 这样即使以后修改应用号码、通道或绑定,历史回执、上行和审计仍可按提交当时的号码关系追溯。 ## 6. 目标下发流程 ```mermaid flowchart LR A["客户应用提交短信"] --> B["CONNECT账号定位应用"] B --> C["校验MsgSrc等于应用企业代码"] C --> D["校验或补全客户Src_Id"] D --> E["运营商、省份和通道组路由"] E --> F["查询应用号码到候选通道的有效绑定"] F --> G["生成上游MsgSrc、Service_Id、Src_Id"] G --> H["保存客户号和上游号快照"] H --> I["Gateway发送上游CMPP SUBMIT"] ``` 详细规则: 1. CONNECT 账号只用于定位应用。 2. 客户 `MsgSrc` 必须与应用企业代码严格匹配;建议统一为 6 位 ASCII,现有超长数据需迁移或重新分配。 3. 客户 `Src_Id`: - 未填且应用只有一个默认号码时,自动补全默认号码。 - 已填写时,必须精确属于当前应用。 - 号码停用、过期、未分配或不允许下发时,返回明确的 `Src_Id` 非法错误。 - 校验失败不得进入计费、任务和上游发送队列。 4. 选出上游通道后,再查找应用号码在该通道上的有效绑定。 5. 主通道没有有效号码绑定时,可按通道组规则选择下一候选,但不能退化使用通道基础号码。 6. 上游 CMPP 报文必须使用: - `MsgSrc = UpstreamAccessNumber.enterpriseCode` - `Service_Id = binding.serviceIdOverride` 或上游号码默认值 - `Src_Id = AccessNumberBinding.upstreamFullNumber` - `Dest_Terminal_Id = 用户手机号` 7. 最终客户号、上游号、企业代码、业务代码和绑定 ID 必须持久化为提交快照。 ## 7. 目标普通上行流程 ```mermaid flowchart TD A["收到上游普通DELIVER"] --> B["保存Channel、原始Dest_Id、Service_Id、LinkID"] B --> C["按通道规则规范化Dest_Id"] C --> D{"channelId和完整接入号唯一绑定?"} D -- "是" --> E["定位应用和客户接入号"] D -- "否" --> F{"通道明确配置截断或基础号回传?"} F -- "是" --> G["手机号和近期下发号码快照辅助匹配"] F -- "否" --> H["生成候选"] G --> I{"得到唯一高置信结果?"} I -- "是" --> E I -- "否" --> H H --> J["ambiguous或unmatched,进入人工认领"] E --> K["转换为客户侧CMPP DELIVER"] ``` 匹配优先级: 1. `(channelId, rawDestId)` 精确匹配活动绑定。 2. 按该通道明确配置的规范化、截断或别名规则匹配。 3. 手机号、通道和近期下发记录中的 `upstreamSrcId` 快照匹配。 4. 使用 `Service_Id`、`LinkID`、运营商、时间窗口作为辅助条件。 5. 多候选进入人工认领。 6. 完全无候选标记 `unmatched`。 禁止: - 仅因为多个应用绑定同一通道组,就把全部应用视为接入号候选。 - 选择任意 active 应用兜底。 - 对所有通道统一执行前缀匹配或截断。 - 把普通上行自身的 `Msg_Id` 当成原下发消息 ID。 - 在没有其他证据时将共享接入号自动投递给某个客户。 匹配成功后,客户侧普通上行 DELIVER 应使用: - `Dest_Id = ApplicationAccessNumber.accessNumber` - `Src_Terminal_Id = 用户手机号` - `Service_Id = 客户侧业务代码` - `LinkID = 可用时保留关联值` 原始上游号码仍需保存在数据库和运营审计中,但不能直接作为转换后的客户号码。 ## 8. 配置界面设计 ### 8.1 平台客户接入端点 独立配置: - 公网域名/IP - 端口 - CMPP 版本 - TLS 状态 - 心跳间隔 - 启停状态 ### 8.2 上游通道页面 通道连接配置和号码能力分区展示: - 登录账号 `Source_Addr` - 企业代码 `MsgSrc` - 默认 `Service_Id` - 基础接入号 - 扩展模式 - 允许扩展长度 - 最大总长度 - 是否支持上行 - 上行回传模式 - 报备状态和生效时间 ### 8.3 应用页面 应用 CMPP 参数区展示: - 平台接入地址和端口 - 账号 - 密码 - 企业代码 - 协议版本 - 连接数和窗口 - 已分配客户接入号列表 - 默认号码 - 上行/下发能力 - 生效状态 ### 8.4 上下游号码绑定矩阵 按应用号码、运营商、通道组和通道展示: - 客户号码 - 上游基础号 - 上游扩展码 - 上游完整号码 - `Service_Id` - 报备状态 - 是否支持上行 - 主备优先级 - 生效时间 绑定不完整时,不允许把对应通道标记为该应用的可用发送候选。 ## 9. API 建议 建议新增或调整: - `GET /api/admin/cmpp-downstream-endpoints` - `POST /api/admin/cmpp-downstream-endpoints` - `GET /api/admin/channels/:id/access-numbers` - `POST /api/admin/channels/:id/access-numbers` - `GET /api/admin/applications/:id/access-numbers` - `POST /api/admin/applications/:id/access-numbers` - `GET /api/admin/applications/:id/access-number-bindings` - `POST /api/admin/applications/:id/access-number-bindings` - `POST /api/admin/access-number-bindings/:id/activate` - `POST /api/admin/access-number-bindings/:id/disable` - `GET /api/client/applications/:id/cmpp-params` 客户参数接口返回结构建议: ```json { "gatewayHost": "8.160.169.106", "gatewayPort": 17890, "account": "123456", "enterpriseCode": "900001", "protocolVersion": "CMPP2.0", "maxConnections": 1, "windowSize": 16, "accessNumbers": [ { "accessNumber": "10699999990001", "serviceId": "NOTICE", "isDefault": true, "replyEnabled": true, "status": "active" } ] } ``` 示例号码只用于说明结构,不能在未获得上游合同和报备确认时直接用于生产配置。 ## 10. 生产数据迁移原则 1. 将现有 `SmsChannel.srcId=1069999999` 迁移为各通道的上游基础号码记录。 2. 从上游供应商合同或后台确认: - 上游企业代码。 - `Service_Id`。 - 允许扩展位数。 - 是否完整回传扩展号。 - 是否支持普通上行。 - 多连接或多通道是否共享同一号段。 3. 在未确认扩展规则前,不为多个应用自动生成扩展号码。 4. 现有 2 条 `ambiguous` 上行继续保留人工认领,不自动修改历史归属。 5. 大量批量验证应用不得自动分配正式号码。 6. 修复或迁移超过 6 字节的 `cmppEnterpriseCode`。 7. 修复客户参数接口,不再返回任意上游通道地址和号码。 8. 对现有两条 active、配置相同的复制通道确认是否属于真实独立上游连接,避免重复通道与号码能力重复生效。 ## 11. 实施优先级 ### P0:数据正确性 - 新增上游号码、应用号码和绑定表。 - 修复客户 CMPP 参数接口。 - 严格校验客户 `MsgSrc` 和 `Src_Id`。 - 上游 `MsgSrc` 改用通道企业代码。 - 持久化客户号码和上游号码快照。 - 上行事件补充 `Service_Id`、`LinkID` 和原始号码。 - 删除“通道组内所有应用都是接入号候选”的匹配路径。 ### P1:配置与运营 - 增加通道号码能力配置。 - 增加应用接入号分配。 - 增加应用号码到三网通道的绑定矩阵。 - 激活前检查报备状态和三网映射完整性。 - 人工认领可生成规则建议,但不得自动修改正式绑定。 ### P2:治理与指标 - 接入号精确匹配率、歧义率、未匹配率。 - 每个号码的下发量、上行量和最后活跃时间。 - 共享号码积压告警。 - 非法 `Src_Id`、未报备号码和异常截断告警。 - 接入号到期、报备失效和停用后的发送阻断。 当前数据规模不需要为接入号表提前做分区;优先保证唯一约束、有效期索引、通道和号码组合索引,以及历史快照完整性。 ## 12. 验收清单 后续实现至少覆盖以下真实链路用例: 1. 客户参数接口只返回平台 CMPP 端点和当前应用号码。 2. 客户提交未分配 `Src_Id` 时被拒绝,且不计费、不入队。 3. 客户不填 `Src_Id` 且应用只有一个默认号时自动补全。 4. 同一客户号码在移动、联通、电信映射为不同上游号码。 5. 主备通道切换后使用各自绑定的上游 `Src_Id`。 6. 上游 `MsgSrc` 与通道登录账号不同时仍能正常提交。 7. 完整扩展号上行能唯一匹配应用。 8. 运营商只返回基础号或截断号时,严格按该通道规则辅助匹配。 9. 两个应用共享号码时不得自动误投。 10. 普通上行和状态报告严格分流。 11. 长上行完成重组后仍按完整接入号匹配。 12. 修改号码配置后,历史消息仍按提交快照关联。 13. 接入号停用、过期或报备失效后阻断新发送,但历史记录仍可查询。 14. 上行匹配失败时真实写入 PostgreSQL 候选或未匹配记录,并支持人工认领。 15. Gateway、NestJS、PostgreSQL 和客户 CMPP 连接上的号码值可以相互核对。 ## 13. 后续落地建议 建议后续另开实现批次,按以下顺序完成: 1. 先取得上游供应商的真实企业代码、业务代码、扩展位数和上行回传规则。 2. 建立 Prisma 模型、迁移和唯一约束。 3. 修复客户 CMPP 参数接口和应用企业代码校验。 4. 改造下发路由,在选中通道后解析号码绑定并保存快照。 5. 扩展 Gateway 上行事件字段。 6. 重写普通上行匹配优先级。 7. 补齐运营端配置页面和人工认领页面。 8. 使用真实 PostgreSQL、Redis、Gateway 和生产同类 CMPP 测试对端执行完整验收。