96b1edff45
iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。 - 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件), yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方 gogf/gf/contrib/registry/consul/v2(API 兼容) - Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan - 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除, 测试断言按当前实现语义重写(不改产品行为) - 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、 内网域名、真实手机号、注释里的真实 token/transid - 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档 - Dockerfile 改用公共基础镜像,移除写死的 registry 凭据 - 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
131 lines
15 KiB
Markdown
131 lines
15 KiB
Markdown
# 电信 v1 现状、v3 并行接入与切换
|
||
|
||
调查日期:2026-10-10。代码依据为当前本地三个服务仓库;数据库通过 DBX 只读查询 `开发环境(192.0.2.29).db_iotlcm`。未修改数据库、未部署、未调用电信真实查询或操作接口。
|
||
|
||
接口依据:用户提供的《中国电信天翼物联5GCMP能力调用参考文档-V3.pdf》,464 页,封面版本 `V3.2026.1.7`;《iotlcm上游API静态审计明细(41条审计记录).xlsx》,其中 21 条为电信记录,其余为移动记录。推荐路径以 PDF 接口定义为准;示例与参数表冲突的地方单独记录。
|
||
|
||
## 1. 原有设计
|
||
|
||
`sim_dx_v1` 是协议模板标识,不是某一个账号通道编号。模板表中的名称确实是“电信BOSS卡API”。同一模板被多个独立账号通道复用。
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
CARD["卡: chan_key"] --> CHANNEL["t_sim_channels: chan_key / temp_key / 凭证"]
|
||
CHANNEL --> TEMPLATE["t_sim_channel_template: temp_key / temp_model / temp_config"]
|
||
TEMPLATE --> PROXY["upstream-proxy: 注册模板并转发统一请求"]
|
||
PROXY --> CHAN["upstream-chan: 创建协议实例"]
|
||
CHAN --> V1["sim_dx_v1: 旧接口"]
|
||
CHAN --> V3["sim_dx_v3: 新接口"]
|
||
```
|
||
|
||
运行时按 `chan_key` 路由,按模板 `temp_key` 精确创建实例。`temp_model=direct` 只是接入类型,不能代替具体协议实现。对外仍然使用统一 `SimCardChannelI`,批量能力使用 `SimCardBatchQueryI`,因此业务调用者无需知道电信 URL 或版本。
|
||
|
||
主要代码位置:
|
||
|
||
| 模块 | 代码 | 职责 |
|
||
| --- | --- | --- |
|
||
| chan | `internal/logic/upstreamimpl/sim_dx.go`、`sim_dx_batch.go` | 原 v1 请求、XML/JSON 解析与状态转换 |
|
||
| chan | `internal/logic/upstreamimpl/dx/client.go`、`sign.go` | AppKey、Sign、GMT Timestamp 鉴权 |
|
||
| chan/proxy | `internal/logic/channelconfig/channel_config.go` | 读取数据库配置、计算版本、缓存快照 |
|
||
| chan/proxy | `internal/logic/upstreamapi/upstreamapi.go` | 模板注册、按通道创建实例、热刷新 |
|
||
| proxy | `internal/logic/upstreamimpl/sim_dx.go` | 同路径转发统一接口给 chan,实际运营商调用在 chan |
|
||
| admin | `server/internal/logic/iot/t_sim_channels.go` | 保存通道后发布刷新消息 |
|
||
| worker | `internal/consumer/rabbitmq/channel_refresh.go` | 消费刷新消息,先刷新 gateway,再刷新 proxy |
|
||
|
||
v1 的网关地址优先取 `temp_config.api.baseUrl`,其次兼容通道配置 `baseURL/url`,最后取 `dependencies.sim_dx_v1.url` 或默认 `https://cmp-api.ctwing.cn:20164`。凭证来自通道列 `app_key/app_secret`;批量接口客户编码来自 `chan_config.custNumber`。
|
||
|
||
### 开发库现状
|
||
|
||
- 仅有电信模板 `sim_dx_v1`,`temp_model=direct`,网关地址为上述默认地址;查询时 `temp_status=0`。运行时创建的主要条件是 `is_normal=0`,不能只根据模板状态推断是否调用上游。
|
||
- 19 个通道使用 v1,全部有非空 AppKey/AppSecret。16 个 `is_normal=0`、2 个 `is_normal=1`、1 个为 NULL。后两类不能创建正常运行实例。
|
||
- 9 个通道 `chan_config` 含 `custNumber`,10 个为空对象;未确认这些客户编码是否对应新版 `custNbr`,不能仅凭名称自动替换实际值。
|
||
- 19 个通道的 `sync_enabled=1`。当前字典语义为 0=允许轮询、1=不允许,切换模板不应顺带开启批量同步。
|
||
- 17 个通道 `operator=3`、2 个为 1;开发库 `operators/t_operators` 字典均为 1=移动、2=联通、3=电信,而 worker 原部分辅助逻辑把 2 当电信。此次只对已知电信模板优先识别,其他运营商字典问题需单独处理。
|
||
- 按租户和 `chan_key` 关联 `t_sim_card_info` 后,19 个通道卡量均为 0;这只是开发库快照,不代表生产无卡。真实切换前须单独核实目标环境库存卡、业务卡与在途任务。
|
||
|
||
## 2. 新增实现与接口对应
|
||
|
||
新增 `sim_dx_v3.go`、`sim_dx_v3_batch.go`。保留 `sim_dx_v1` 注册和所有旧路径。proxy 同时注册两个模板;v3 使用独立适配器,不把 v1 路径机械替换成 `/v3/`。
|
||
|
||
v3 同样使用 AppKey、Sign、Timestamp。文档第 10-12 页 Java 示例为:排序查询参数,拼接原样 body、SecretKey、GMT 时间戳,先生成小写 MD5 字符串,再把该字符串字节做十六进制编码并转大写。新增 `BuildSignV3` 显式遵循该例;v1 签名保持原样。业务 envelope 使用 `code/message/result`,逐卡 batch 的 `result=0` 表示成功。HTTP 799 的错误码从 `gw-errorCode` header 保留到统一 `errCode`。
|
||
|
||
| 功能 | v1 实际路径/行为 | v3 当前接入 | PDF 页 |
|
||
| --- | --- | --- | --- |
|
||
| 单卡资料 | `/openapi/v1/prodinst/getSIMList` | GET `/api/v3/getSimInfo` | 14-23 |
|
||
| 停机原因 | 复用基础资料 StopTypeList | 读取 getSimInfo 的 stopStatusName/stopTypeName,兼容 stopTypeList | 15-16 |
|
||
| 会话 | 多种 qryOnlineSataus/qryOnlineStatus 组合 | GET `/api/v3/getOnlineStatus`;1=在线、2=离线 | 31-35 |
|
||
| IMEI | `/openapi/v1/prodinst/gete2einfo` | GET `/api/v3/getImei` | 44-45 |
|
||
| 单卡实时当月流量 | `/api/v1/openbill/queryTraffic`,XML | GET `/api/v3/getDataUsage`;dataAmount 单位 MB | 107-110 |
|
||
| 历史月流量 | queryTrafficByDate | GET `/api/v3/getDataUsageByMonth`,month=yyyyMM | 111-114 |
|
||
| 历史日流量 | queryTrafficByDate | GET `/api/v3/getDataUsageBetweenDate`,beginDate=endDate=yyyyMMdd | 129-132 |
|
||
| 批量日流量 | batchQryFlowByDate | POST `/api/v3/getDataUsageByDayBatch` | 144-146 |
|
||
| 批量月流量 | batchQryFlowByMonth | POST `/api/v3/getDataUsageByMonthBatch` | 147-149 |
|
||
| APN 列表 | 复用会话 ApnId | GET `/api/v3/getApnInfo`,apnInfoList,0停用/1正常 | 159-161 |
|
||
| 终端开关机 | POST `/api/v1/prod/getDevicePowerStatus` | **GET** `/api/v3/getDevicePowerStatus`;仅 3G,保持统一设备健康未知。⚠️网关实测只接受 GET(官方文档写 POST 与网关不符;同一网关下 v1 的同名接口反而只接受 POST) | 190-192 |
|
||
| 整卡数据开停 | GET singleCutNet,DEL开/ADD停 | POST `/api/v3/singleCutNet`;平台开0→上游1、停1→上游2 | 198-200 |
|
||
| 卡状态 | queryCardMainStatus | GET `/api/v3/getSimStatus` | 317-321 |
|
||
| 停复机 | disabledNumberService,orderTypeId=19/20 | GET `/api/v3/updateSimStatus`,operType=19/20 | 321-323 |
|
||
| 批量卡状态 | batchQrySimStatus,旧分块10000 | POST `/api/v3/getSimStatusBatch`,新分块最多100 | 328-333 |
|
||
| 机卡绑定 | chkDeviceBindingInfo | GET `/api/v3/getSimBindingInfo` | 374-376 |
|
||
| 区域限制 | qryAreaLimitInfo | GET `/api/v3/getSimRegionLimitStatus`,含 restrictedArea | 381-383 |
|
||
| 冻结状态 | code=0 + “不支持查询” | code=0 + “不支持查询”,与 v1 对齐 | 电信无独立冻结查询接口 |
|
||
| 达量限速诊断 | code=5000 + “暂不提供” | code=5000 + “暂不提供”,使诊断页降级为跳过 | 电信无达量限速档位查询 |
|
||
|
||
批量请求使用 `msisdnList` 和 `custNbr`,按 MSISDN 匹配返回数据并保持输入顺序;漏卡、重复卡、逐卡失败、日期错配和非 MB 单位返回失败。业务失败保留运营商原始错误码;缺失 code/result、无效金额不会被当成成功或零流量。
|
||
|
||
停复机与整卡数据开停默认由 `api.enableOperations=false` 关闭。开启后仅返回交易受理号,不代表已完成运营商状态变更;完成状态须按订单/状态查询确认。本次没有新增订单轮询工作流。
|
||
|
||
### 明确保留的限制
|
||
|
||
1. 批量资料 `batchQrySimInfo` 的参数表写 result、逐卡成功1,但示例使用 data.qrySimInfoList、请求示例又写 batchQrySimStatus。暂不调用这个有冲突的端点,基础资料走单卡 getSimInfo。新版基础资料与旧 batch 的全部扩展字段不完全一致,迁移验收须核对订购产品、停机原因码、区域/绑定信息完整性。
|
||
2. 上层 `/sim/query/sim-datausage-monthly-batch` 仍保留原“仅当月”的校验;v3 适配器可处理近6个月,但对外历史月查询目前继续使用单卡 getDataUsageByMonth。扩展公共 batch 历史窗口须另行调整通道能力校验,不能直接扩大 v1 的窗口。
|
||
3. 历史日接口要求已结束日期;batch 只查近7个已结束日。当日接口 getDataUsageOfToday 不接入当前历史日契约。文档示例 totalUsageAmount 与参数表 dataAmount 不一致,金额解析兼容两者。
|
||
4. 指定 APN 开停、单卡多 APN 模板操作与“整卡单独断网”语义不同:`singleCutNet` 只有整卡数据开停语义,没有“按 APN 定向”维度。与 v1 一致,忽略请求中的 `ApnName`(预警自动断网、批量数据开停都会固定携带 `apnName="CMIOT"`,若据此拒绝会直接打断现有主流程)。新增 APN 管理需独立确认业务契约。
|
||
(2026-10-14 更正:v1 在 2026-10-10 `b3f00a0` 曾被改成按 APN 定向的 `/api/v3/updateApnFunctionStatus`,导致电信数据开停全部失败,已回退为 singleCutNet;此处“与 v1 一致”现重新成立。)
|
||
5. 新版终端开关机仅3G,不能据此宣称4G/5G终端健康;机卡绑定断网状态与分离状态也不能互换语义。
|
||
6. 不自动从 v3 回退并重发 v1 写操作。网络超时可能已经受理,重复调用会产生重复操作。
|
||
7. getSimInfo 参数表含 ICCID/IMSI,但示例只返回 MSISDN。基础资料查询允许返回部分三码且不编造缺失值;现有后台单码补全流程仍要求完整三码,真实响应不足时会明确失败。必须通过授权测试卡核实这项差异后再迁移依赖三码补全的流程。
|
||
|
||
## 3. 快速切换与回退
|
||
|
||
复用通道模板作为版本开关:保留同一个 `chan_key`,把目标通道的 `temp_key` 从 `sim_dx_v1` 改成 `sim_dx_v3`。不用迁移卡表、库存关联、ICCID→通道 Redis 映射,也不需要在切换时重新部署。不同通道可选择不同版本,但同一通道全部卡共用该版本。
|
||
|
||
上线前一次性部署两个模板都存在的新 chan/proxy 及 worker。随后在后台新增模板:`temp_key=sim_dx_v3`、`temp_model=direct`,配置采用 `docs/config/sim_dx_v3.json`。通道凭证仍放 `app_key/app_secret`;客户编码用 `chan_config.custNbr`,代码兼容旧字段 custNumber。未把任何 v3 模板或通道记录写入开发库。
|
||
|
||
切换步骤:
|
||
|
||
1. 保存目标通道的原 temp_key、app_key/app_secret、chan_config、is_normal、同步配置快照到受控存储;快照不写日志。确认账号已获 v3 能力授权、新旧凭证是否相同,以及新版 custNbr 的正确值。
|
||
2. 新建测试用 v3 通道并保持非运行状态,使用后台通道测试创建临时实例,以完整三码和单码补全验证只读查询。该测试路径允许读取非运行通道,不注册生产实例;开发库目前无关联业务卡,需准备有授权的测试卡。
|
||
3. 选择一个目标通道灰度。避免正在进行的管理操作和大批量同步;修改 temp_key,必要时同时更新新凭证/custNbr,保持 chan_key。不得顺带调整 sync_enabled 或 is_normal 来扩大操作范围。
|
||
4. 通过后台保存,使事务完成后投递 `QueueChannelRefresh`。worker 先调用 gateway 的 `POST /internal/channel/refresh?chanKey=...`,再刷新 proxy,失败会重试。后台“保存成功”只说明消息已投递,仍需核对 worker 及两个上游的刷新结果。
|
||
5. 核对只读查询、批量拆分、状态/会话映射、流量数值与单位、错误率和响应延迟。需要启用写操作时先在受控测试卡验证,再打开 enableOperations;用交易流水和状态查询确认最终结果。
|
||
6. 逐通道推广。回退时恢复完整旧快照并重新保存/刷新;如果凭证相同且无其他配置变化,可只恢复 temp_key=sim_dx_v1。
|
||
|
||
运行中的实例以互斥锁替换,已取得旧实例的请求可继续完成,新请求使用新实例。但 gateway/proxy 两个服务之间不是分布式原子切换,RabbitMQ 消息也不是多实例广播。多副本部署须刷新每个实例并确认配置版本一致,当前地址的一次 HTTP 调用不能证明所有副本已经更新。刷新端点只能在现有内部控制网络调用。
|
||
|
||
配置缓存默认全量重读2小时,实例扫描默认60秒。直接执行数据库 UPDATE 不会投递刷新消息,不能用于即时切换;必须走后台保存或显式内部刷新。此次把凭证和 token 内容加入 chan/proxy 的配置版本哈希,防止仅更换凭证时实例版本不变;哈希异常日志不输出配置及凭证。
|
||
|
||
队列任务持有投递时的 temp_key 快照,而上游运行时按最新 chan_key 配置执行;切换前宜处理在途任务,新任务重新读取通道配置。本次统一 v1/v3 的 worker 电信模板分类,涵盖限速、月流量失败分类、状态重试、失败记录和入库限速;未知未来模板仍是 generic。v3 的 S00004 错误策略沿用原 v1 策略,真实账号联调需确认上游语义和 QPS,不把默认25 QPS视作电信承诺限额。
|
||
|
||
## 4. 验证与上线边界
|
||
|
||
新增测试覆盖 v1/v3 模板并存、配置校验、签名、在线枚举、实时流量 MB、异常金额/缺失结果/卡号错配、HTTP799与业务码、101张卡拆两批、乱序/漏卡/逐卡失败、错月份、历史日参数、部分三码、APN/绑定/区域/开关机字段、操作默认关闭及新参数映射。chan/proxy 的配置版本及刷新测试通过,worker 的入库限速/月流量/状态/失败记录测试通过;三个服务入口均完成编译验证。
|
||
|
||
worker 的完整 ZSet 测试曾出现租约定时测试失败(单独重跑通过)及 `TestMonthlyChannelBlockKeepsExistingCountingPolicy` 的 Redis Lua整数参数失败。通过 Go overlay 加载 `HEAD:internal/queue/zset/run.go` 原始代码后,后者同样复现,确认不是本次队列分类改动引入。本次未修改其 Lua 协议,仍需单独排查,不能宣称全量测试通过。
|
||
|
||
验证命令:
|
||
|
||
```text
|
||
# iotlcm-upstream-chan
|
||
go test ./internal/logic/upstreamimpl ./internal/logic/upstreamimpl/dx ./internal/logic/channelconfig ./internal/logic/upstreamapi
|
||
# iotlcm-upstream-proxy
|
||
go test ./internal/logic/channelconfig ./internal/logic/upstreamapi ./internal/logic/upstreamimpl
|
||
# iotlcm-worker-svr
|
||
go test ./internal/service/inbound ./internal/service/monthlyusage ./internal/service/simstatus ./internal/queue/zset
|
||
go test ./internal/consumer/zset -run 'TestPolicyForTemplate|TestLostBatchLeasePreventsApplyAndAck' -count=1
|
||
# 三个仓库分别编译入口,不启动服务
|
||
go test -run '^$' .
|
||
```
|
||
|
||
本次为代码实现及本地契约验证。真实电信 API 的鉴权授权、字段差异、custNbr、频控、运营商订单完成状态及多副本热刷新仍需部署后联调。没有修改数据库、切换通道或调用真实停复机/断网。
|