chore: 初始开源发布

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
This commit is contained in:
MiFaZhan
2026-10-22 15:42:29 +08:00
commit 96b1edff45
191 changed files with 30805 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# Apika mobile shared-token channel
Use `temp_model=shared_token` with template key `sim_onelink_kedang_v1`.
Set `token.baseUrl` to `http://test.iotapi.apika.cn` in the test environment
or `https://iotapi.apika.cn` in production. The token path is
`/iot-api/iot-card-info/getRefreshToken`.
Channel fields:
- `app_key`: Apika `appKey`, sent in the request header and body.
- `app_secret`: Apika `appSecret`, sent in the request header and body and used
to calculate the signature.
- `chan_config`: `{"appId":"..."}`; `appId` is sent as JSON field `appid`.
The request sends `appKey`, `appSecret`, and `signature` in both the headers
and JSON body. The JSON body also contains `appid`.
The signature is calculated by building `{appKey}:{appSecret}` with the
literal braces, hashing its UTF-8 bytes with SHA-256, converting the digest to
uppercase hexadecimal, and Base64-encoding that hexadecimal string.
The returned `data.shareToken` is cached for five minutes and refreshed thirty
seconds before expiry. The returned `transid` is not used as the OneLink
business request transaction ID.
+1
View File
@@ -0,0 +1 @@
{}
+8
View File
@@ -0,0 +1,8 @@
{
"api": {
"baseUrl": "http://iotapi.bjmqtx.com/openapi/wlw/v3/cool",
"timeout": "10s",
"enableOperations": false,
"resumePath": ""
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"token": {
"provider": "apika",
"baseUrl": "http://test.iotapi.apika.cn",
"path": "/iot-api/iot-card-info/getRefreshToken"
},
"api": {
"baseUrl": "https://api.iot.10086.cn/v5/",
"timeout": "15s"
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"token": {
"provider": "henan_lelian",
"baseUrl": "http://api.wwlelianiot.com",
"path": "/api/v2/getShareToken",
"params": {
"loginPath": "/api/v2/loginVerify"
}
}
}
@@ -0,0 +1,7 @@
{
"token": {
"provider": "mobile_public",
"baseUrl": "https://REPLACE_WITH_TOKEN_HOST",
"path": "/api/get/yd/api/msg"
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"token": {
"provider": "tengyu",
"baseUrl": "http://www.sdtywl.cn/",
"path": "/prod-api/open/api/oneLinkToken"
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"token": {
"provider": "xuzhou_mobile",
"baseUrl": "http://iot.yuanjianiot.com",
"path": "/admin/api/get/token",
"params": {
"carrierType": 1
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"api": {
"baseUrl": "https://cmp-api.ctwing.cn:20164",
"enableOperations": false
},
"http": {
"timeout": "10s"
}
}
+214
View File
@@ -0,0 +1,214 @@
# Mingqian v3 integration
## Scope and deployment
This is a third-party platform adapter, not a shared-token adapter. Use
`temp_key=sim_mingqian_v3` and `temp_model=direct`. Deploy BOTH the gateway
and proxy builds containing this key, then save/refresh the channel in both
services. A configuration refresh cannot load a template into an old binary.
Worktrees for this feature:
- Gateway: `F:\Work\iotlcm-upstream-chan`
- Proxy: `F:\Work\iotlcm-upstream-proxy`
- Branch in each repository: `feat/mingqian-api-integration`
The LCM response model update lives in `F:\Work\lcm-mingqian`, branch
`feat/mingqian-flow-mapping`, based on `origin/master`. Deploy this LCM build
as well to preserve package flow fields in the channel test page. The page
already renders the returned JSON; no frontend changes are required. Without
the LCM model update, its typed response parser discards the new fields.
Do not deploy the old checkout by mistake. This feature does not edit the
existing runtime service address, production database, or operator settings.
Point the proxy's `gateway.address` at the actual gateway HTTP listener.
Do not configure the proxy to call Mingqian directly.
## Admin configuration
Create a template named Mingqian third-party v3:
- Template key: `sim_mingqian_v3`
- Model: `direct`
- Status: Enabled (select the UI option; do not assume the numeric enum)
- Template JSON: copy `docs/config/mingqian_v3.json`
Create a channel:
- Channel key: a unique value such as `mingqian_001`
- Template: select the template above
- AppKey: the upstream **appId**
- AppSecret: the upstream signing **APPKEY** (NOT a shared token)
- Channel JSON: `{}` (`docs/config/mingqian_channel.json`)
- Status: Enabled
- Carrier: select the actual carrier of the cards/account; this document
covers three carriers and does not establish which one your account uses.
Credentials are read from channel AppKey/AppSecret only. Channel JSON does
not override template configuration. Never commit real credentials.
## Supported standard gateway capabilities
| Capability | Upstream relative path | Behavior |
| --- | --- | --- |
| Basic information | `/simlist/get-state-by-number` | ICCID, MSISDN, raw state and description only |
| Card lifecycle status | `/simlist/get-state-by-number` | Convert upstream states to gateway enums |
| Current-month usage | `/simlist/get-flow-use-by-number` | `use` KB / 1024 -> `dataUsage` MB |
| Package flow | `/use/get-card-use-by-number` | `totalpackage` and `targetpackageusage` MB -> separate `packageFlow` in the same real-time flow response |
| Stop | `/simlist/get-sim-switchoff-by-number` | Disabled unless `api.enableOperations=true` |
| Resume | Confirmed `api.resumePath` | Disabled by default; no guessed endpoint |
Queries split into at most 50 identifiers. Status changes split into at most
20 identifiers. Responses are matched by the requested ICCID or MSISDN, not
by array position. Missing/duplicate cards, missing/negative usage, missing
status and per-card operation failures are returned as failures, never as
successful zero usage. IMSI-only requests are rejected before network access.
Basic information is partial: no fake IMSI, activation date, or opening date.
The upstream `activedate` is a PACKAGE effective date, not card activation.
Use the admin test's direct-single-identifier mode. Three-code completion
cannot succeed unless all three codes are available from another source.
State mapping: 1 -> pending(1), 2 -> active(2), 3 -> stopped(4),
4 -> test(6), 5 -> stock(7), 6 -> cancelled(9). States 4 and 5 appear in the
inventory example; the global table omits them. State 7 (expired) and other
unmapped states return an explicit error rather than treating expiry as
permanent cancellation. Raw state remains available in basic information.
Not implemented as standard gateway capabilities: account-wide inventory
sync, IMEI lookup, APN/session/device/binding/stop-reason diagnostics, historical
daily/monthly usage, real-name status, voice-number
whitelists, SMS, account balance, recharge and order operations. Unsupported
standard methods return explicit errors without contacting upstream. An IMEI
field in the account inventory must not imply that binding is enabled.
The inventory endpoint is account-wide and paginated; this adapter deliberately
does not scan all pages on every single-card query.
## Flow mapping and partial failures
Select the admin test's real-time flow item (`simDataUsage`), or POST to the
gateway/proxy `/sim/query/sim-datausage` with the normal `simKeys` and empty
`reqData`. For each batch of at most 50 identifiers the adapter calls both
read-only flow endpoints, sharing the same account rate limiter. Results from
each endpoint are matched independently by ICCID/MSISDN, not by response order.
For 51 cards this makes four upstream calls, not two.
Example `resData`, all units MB:
```json
{
"dataUsage": 1,
"packageFlow": {
"totalFlow": 10240,
"dataUsage": 8192,
"remainingFlow": 2048
}
}
```
- `dataUsage`: current-month usage, from `use / 1024`. Existing monthly
synchronization continues using this field only.
- `packageFlow.totalFlow`: upstream `totalpackage`, already MB.
- `packageFlow.dataUsage`: upstream `targetpackageusage`, already MB and
cumulative over the package period, not necessarily the current month.
- `packageFlow.remainingFlow`: derived as `max(totalpackage - targetpackageusage, 0)`.
Overage is retained in cumulative usage, but remaining flow never becomes negative.
- The legacy top-level `totalFlow` remains absent for Mingqian. Do not subtract
current-month usage from a cross-month package total.
Missing/null/negative/non-numeric amounts are failures, not successful zeroes.
Actual zero values are valid. When monthly usage succeeds but package flow
fails, the card retains its successful monthly reading, omits `packageFlow`,
and includes `packageFlowError` plus a warning in the per-card `message`.
If monthly usage fails, the per-card code remains failure even if a package
snapshot is present; never write the package cumulative amount as monthly usage.
The channel test's monthly and daily items request historical periods. The
current Mingqian documentation provides neither historical endpoint, so these
remain explicitly unsupported rather than returning current-period numbers.
Real-time sessions, online status, APN and IP likewise remain unsupported;
neither lifecycle state nor nonzero usage proves a currently active session.
This update exposes package flow in API/channel-test results. It does not
persist package snapshots, change card-list balance calculations, or change
charging, quotas, or monthly synchronization semantics.
## Wire contract and safety
JSON POST envelope:
```json
{
"body": {
"appId": "YOUR_APP_ID",
"reqtime": "2026-09-17",
"timestamp": "CURRENT_UNIX_MILLISECONDS_PLUS_60000",
"transid": "yyyyMMddHHmmssfff_PLUS_SIX_RANDOM_HEX_CHARACTERS",
"version": "3.0",
"sign": "COMPUTED_SIGNATURE"
},
"data": "{\"numberArray\":[\"YOUR_TEST_ICCID\"]}"
}
```
The `data` value is a serialized JSON STRING. Dates use UTC+8. The timestamp
is current time plus one minute, in milliseconds. Sign excludes `sign` itself:
sort common parameter keys, concatenate `key_value` for each without additional
separators, prepend/append APPKEY, and produce lowercase MD5.
This is the literal interpretation of the signing prose and example casing;
no official complete credential/sign test vector was supplied. Local tests
verify this interpretation, not acceptance by the upstream server. Confirm
against one official sample or a permitted read-only query before release.
Successful business code is 200 (not zero). HTTP errors, non-200 business
codes, malformed envelopes and absent data fail closed. No automatic retries
or HTTP redirects, especially for state-changing requests. Raw response bodies
and secrets are not logged. Client response size is bounded at 4 MiB.
An in-process limiter shared by base URL + appId spaces calls at least 60ms
apart (~166/10s, below the documented 200/10s). It does NOT coordinate separate
gateway replicas or other applications using the same account. Use a shared
egress limit or allocate a per-replica budget before scaling those accounts.
## Documentation contradictions requiring upstream confirmation
1. Stop and resume pages both list `/simlist/get-sim-switchoff-by-number`.
Never enable resume using that path. Obtain the real resume path, configure
`api.resumePath`, then explicitly enable operations when authorized.
2. The state page example says `state=1` with an active description; the global
table says 1=pending, 2=active. Mapping follows the table, not that example.
3. Confirm signature concatenation/casing with an official sample.
## Verification
Use only a sandbox or local `httptest` for stop/resume. No real stop, resume,
SMS, purchase or recharge request was sent during implementation.
Gateway targeted tests:
```text
go test ./internal/service/mingqian
go test ./internal/logic/upstreamimpl -run TestMingqianRegistration
go build ./...
```
Proxy targeted tests:
```text
go test ./internal/logic/upstreamimpl -run TestMingqianProxyTemplate
go test ./internal/logic/upstreamimpl -run TestMingqianProxyPreservesFlowFields
go build ./...
```
LCM response-model tests (run in `F:\Work\lcm-mingqian\server`):
```text
go test ./internal/model -run SimDataUsage
go build ./...
```
Initial live verification should select ONLY basic info, card status and
current-month usage using a permitted ICCID/MSISDN. Do not use real operations
to validate connectivity. A successful mock test is not proof of live access.
+24
View File
@@ -0,0 +1,24 @@
# 移动公共 Token 共享通道
## 模板
- 模板编号:`sim_onelink_daliuliang_v1`(开发库当前使用的标识)
- 模板名称:移动公共 Token 共享 OneLink v1
- 模板模型:`shared_token`
- 模板配置:`docs/config/onelink_mobile_public_v1.json`
`token.baseUrl` 必须填写上游提供的 Token 服务主机地址;当前协议只提供路径 `/api/get/yd/api/msg`,不能使用移动 OneLink 业务地址代替。
## 通道管理
- 通道名称:移动大流量插拔实名卡-OU2003457135923
- 通道标识(`chan_key`):`daliuliang_001`
- `temp_key`:`sim_onelink_daliuliang_v1`
- `chan_config`:`{"userId":"102369"}`
- `app_secret`:上游提供的签名密钥
创建实例前,需要启用通道和模板,并确保通道的 `is_normal=0`;`is_normal` 为 `NULL` 或 `1` 时,服务不会创建运行实例。通道服务与代理服务都需要部署包含该模板编号注册的版本。
不要把签名密钥写入 `chan_config`、模板配置或日志。Token 获取请求使用当前 Unix 秒时间戳,并按 `md5(secret + userid + time + secret)` 计算小写签名。
上游响应的 `expire_time` 作为 Unix 秒绝对过期时间保存;服务会在到期前 30 秒刷新 Token,并在同一通道的并发请求间合并刷新动作。
+119
View File
@@ -0,0 +1,119 @@
# 通道模板模型设计
## 1. 设计结论
通道配置分成三层:
```text
temp_model 接入方式大类
temp_key 具体上游协议模板
chan_config 具体通道的账号凭证和运行时状态
```
`temp_model` 只使用两个值:
```text
direct 直接调用上游 API
shared_token 先获取共享 Token,再调用业务 API
```
`direct` 是分类值,不是通用实现。普通模板必须通过 `temp_key` 找到精确注册的协议实现;不能仅凭 `direct` 推断运营商或请求协议。
`shared_token` 是共享 Token 模型的路由值,可以由共享实现工厂创建具体通道。
## 2. Mermaid 架构图
```mermaid
flowchart TB
DB["模板数据"] --> TEMPLATE["通道模板"]
TEMPLATE --> KEY["temp_key\n具体协议模板"]
TEMPLATE --> MODEL["temp_model\n接入方式"]
MODEL --> DIRECT["direct\n直接调用上游 API"]
MODEL --> SHARED["shared_token\n先获取共享 Token"]
KEY --> TY["sim_recharge_sdtywl_v1\n腾宇直连"]
KEY --> YD["sim_yd_v5\n中国移动官方"]
KEY --> DX["sim_dx_v1\n中国电信官方"]
KEY --> TY_SHARED["sim_onelink_tengyu_v1\n腾宇获取 Token + OneLink"]
KEY --> XZ_SHARED["sim_onelink_xuzhou_mobile_v1\n徐州移动获取 Token + OneLink"]
TY --> DIRECT_IMPL["精确 temp_key 注册的 direct 实现"]
YD --> DIRECT_IMPL
DX --> DIRECT_IMPL
TY_SHARED --> SHARED_IMPL["shared_token 模型工厂\n复用 OneLink 业务能力"]
XZ_SHARED --> SHARED_IMPL
SHARED_IMPL --> PROVIDER["Token Provider"]
PROVIDER --> TOKEN["共享 Token"]
TOKEN --> ONELINK["OneLink 业务 API"]
CREATE["新建通道"] --> SELECT["选择已有 temp_key"]
SELECT --> CREDENTIALS["填写通道凭证"]
CREDENTIALS --> CHANNEL["t_sim_channels"]
RULES["规则"] --> R1["temp_model 只有 direct / shared_token"]
RULES --> R2["temp_key 决定具体协议"]
RULES --> R3["direct 必须有精确注册"]
RULES --> R4["shared_token 使用共享工厂"]
```
## 3. 模板示例
```text
sim_yd_v5
temp_model = direct
中国移动官方 OneLink
sim_yd_mock_v5
temp_model = direct
中国移动 Mock OneLink
sim_dx_v1
temp_model = direct
中国电信 CMP
sim_recharge_sdtywl_v1
temp_model = direct
腾宇直连 API
sim_onelink_tengyu_v1
temp_model = shared_token
腾宇获取 Token + OneLink
sim_onelink_xuzhou_mobile_v1
temp_model = shared_token
徐州移动获取 Token + OneLink
```
## 4. 路由规则
运行时始终先按 `temp_key` 查找精确实现。精确注册优先于模型工厂,因此现有直连通道的协议行为不会因为模型分类改变。
当没有精确 `temp_key` 注册时:
- `temp_model=shared_token` 使用共享 Token 模型工厂。
- `temp_model=direct` 返回明确错误,因为 direct 本身不描述具体协议。
- 空值、未知值或旧的具体实现名称返回明确配置错误。
## 5. 新增三方协议流程
1. 实现三方协议的请求、签名、响应映射、错误处理和业务能力。
2. 增加新的 `temp_key` 并注册精确实现。
3. 为模板设置 `temp_model=direct`;如果需要共享 Token,则设置 `temp_model=shared_token`。
4. 在模板数据中保存公共配置,通道数据中保存账号凭证。
5. 业务人员创建通道时选择已有 `temp_key`,只填写该通道自己的凭证。
## 6. 配置边界
模板配置适合保存公共 API 地址、固定路径、超时时间和其他非账号协议参数。
通道配置适合保存 appKey、appSecret、pwdKey、channelNo、appId、账号密码及该通道独立的 Token 状态。
配置不能试图描述完整协议;新增协议通常仍需要修改代码。
## 7. 迁移约束
- 新代码只接受 `direct` 和 `shared_token` 两个模型值。
- 部署前通过 DBX 将现有普通模板修正为 `direct`,共享 Token 模板修正为 `shared_token`。
- 旧的 `onelink`、`onelink_mock`、`telecom`、`shared_token_onelink` 不作为新配置值保留。
- 发现无法根据 `temp_key` 判断类型的模板时,停止自动迁移并人工确认。
- 本仓库不提交数据库修复 SQL;数据库变更由 DBX 直接执行。
@@ -0,0 +1,130 @@
# 电信 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、频控、运营商订单完成状态及多副本热刷新仍需部署后联调。没有修改数据库、切换通道或调用真实停复机/断网。
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# 物联网卡卡状态查询 — 请求体与 internal/model/sim_chan_api.go 中 SimCardStatusInput 一致(simKeys)
# 参数说明见 docs/测试资源.md
# 用法:在工程根目录执行
# chmod +x docs/scripts/query_sim_card_status.sh
# ./docs/scripts/query_sim_card_status.sh
# 可选环境变量:BASE_URL、CHAN_KEY、ICCID(多个用逗号分隔)
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:8110}"
PATH_QUERY="/query/sim-status"
CHAN_KEY="${CHAN_KEY:-YiDong_EC}"
if [[ -n "${ICCID:-}" ]]; then
IFS=',' read -r -a ICCIDS <<< "${ICCID}"
else
ICCIDS=("898604F4152391115473" "898604F4152391115474")
fi
json_simkeys="["
sep=""
for id in "${ICCIDS[@]}"; do
id="$(echo -n "$id" | tr -d '[:space:]')"
[[ -z "$id" ]] && continue
json_simkeys+="${sep}{\"chanKey\":\"${CHAN_KEY}\",\"iccid\":\"${id}\"}"
sep=","
done
json_simkeys+="]"
body="{\"simKeys\":${json_simkeys}}"
echo "# POST ${BASE_URL}${PATH_QUERY}" >&2
echo "# body: ${body}" >&2
echo >&2
curl -sS -X POST "${BASE_URL}${PATH_QUERY}" \
-H 'Content-Type: application/json; charset=utf-8' \
-d "${body}"
echo
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# 查询 SIM 卡会话 — 请求体与 internal/model/sim_chan_api.go 中 SimSessionInput 一致(simKeys)
# 参数说明见 docs/测试资源.md
# 用法:在工程根目录执行
# chmod +x docs/scripts/query_sim_session.sh
# ./docs/scripts/query_sim_session.sh
# 可选环境变量:BASE_URL、CHAN_KEY、ICCID(多个用逗号分隔)
set -euo pipefail
BASE_URL="${BASE_URL:-http://127.0.0.1:8110}"
PATH_QUERY="/query/sim-session"
CHAN_KEY="${CHAN_KEY:-YiDong_EC}"
# 默认 ICCID 来自测试资源(两张移动卡)
if [[ -n "${ICCID:-}" ]]; then
IFS=',' read -r -a ICCIDS <<< "${ICCID}"
else
ICCIDS=("898604F4152391115473" "898604F4152391115474")
fi
json_simkeys="["
sep=""
for id in "${ICCIDS[@]}"; do
id="$(echo -n "$id" | tr -d '[:space:]')"
[[ -z "$id" ]] && continue
json_simkeys+="${sep}{\"chanKey\":\"${CHAN_KEY}\",\"iccid\":\"${id}\"}"
sep=","
done
json_simkeys+="]"
body="{\"simKeys\":${json_simkeys}}"
echo "# POST ${BASE_URL}${PATH_QUERY}" >&2
echo "# body: ${body}" >&2
echo >&2
curl -sS -X POST "${BASE_URL}${PATH_QUERY}" \
-H 'Content-Type: application/json; charset=utf-8' \
-d "${body}"
echo
+65
View File
@@ -0,0 +1,65 @@
# 电信 V3 APN 状态与开停适配
## 接口依据
查询依据《中国电信天翼物联5GCMP能力调用参考文档-V3 (1).pdf》159–164 页;
开停依据 CTIOT_5GCMP_BA004(添加或取消单独断网,PDF p217-219)。
### 查询:GET /api/v3/getApnInfo
- msisdn/iccid/imsi 至少一项;读取 result.apnInfoList。
- 查询表格使用 status,示例使用 apnStatus,网关兼容两者;0 表示停用,1 表示正常。
- 两字段冲突、缺失或不在 0/1 范围时,状态保留为空,不推断为正常。
### 开停:GET /5gcmp/openapi/v1/common/singleCutNet
- 参数 access_number(物联网接入号,必传)+ action(ADD/DEL)。
- **整卡数据开停语义**,电信侧没有「按 APN 定向」这一维度。
- action 语义与平台相反,必须转换、禁止透传(见 `convertDxApnOperType`):
- 平台 0(开通数据) → `DEL`(取消单独断网,恢复联网)
- 平台 1(停用数据) → `ADD`(单独断网)
- 忽略调用方携带的 ApnName:预警自动断网、批量数据开停都会固定带 `apnName="CMIOT"`,
电信卡不含该 APN,若据此拒绝会直接打断主流程。
- 响应为 XML:`SvcCont > Response > RspCode`,`0000` 表示成功接收;
`GROUP_TRANSACTIONID` 为流水号,透传为 orderNum。
- 缺返回码、非 `0000`、缺流水号、HTTP 失败均不视为成功,不自动重试以免重复操作。
> 注意:`/api/v3/updateApnFunctionStatus` 是另一个接口(单卡 APN 开停,按 APN 定向),
> 与「数据服务开停」业务语义不匹配,v1 不再使用。
> 历史:2026-10-10 `b3f00a0` 曾把 v1 改成该接口,导致电信数据开停因 APN 名不匹配而全部失败,已回退。
## 读取链路
智能诊断展示与 APN 开关查询分离:
- 电信:小程序/Web 后端复用卡状态查询 → 请求代理 → 网关 sim_dx_v1 → /openapi/v1/prodinst/queryCardMainStatus。
- 网关透传独立 netBlockStatus(0 未断网、1 已断网)和 netBlockTypes,诊断返回 dataServiceSource=network_block、netBlockStatus、netBlockReason。
- 电信 dataStatus 保持空值,不将断网状态转换成 APN 开关,也不以此驱动 APN 开停按钮;电信卡片排查不展示 APN 开停操作。
- 未返回有效断网状态时显示 -;已断网时展示全部有效断网原因并去重,不推测原因。
- 卡主状态查询失败时数据服务模块同步标记失败,不能展示本地旧状态。
- 电信在线状态继续独立查询 /api/v1/onlineSessionOpen/qryOnlineSataus;APN、IP、IMEI、时间等会话字段不能推断 APN 开启。
- 电信会话 netModel=1/2 统一为 4G/5G,不再透传数字让前端按移动编码误解释。
- 移动数据服务仍使用 ec/query/apn-info 的 CMIOT 状态;移动和电信的会话状态都从会话接口获取。
- 独立 APN 查询 /sim/query/sim-apn-info 仍使用 CM001 /api/v3/getApnInfo,但不再作为电信智能诊断数据服务断网展示的来源。
两端对非移动 APN 列表采用保守一致性判定:单个 APN 或所有 APN 的有效状态一致时才给出开/停;空列表、缺名称、缺状态、混合状态均不选第一项代替整体状态,界面无值显示 -。
这不是多 APN 的业务目标选择规则;若要显示某个专用业务 APN,需进一步明确目标并让查询和操作使用同一配置。
## 开停保护与剩余事项
- 开停走整卡语义的 singleCutNet,只要求物联网接入号(MSISDN);缺接入号时明确失败。
- 不将 CMIOT 自动改成 ctnet 或 CTIOT,也不猜选唯一 APN:
指定 APN 开停需要另接 `/api/v3/updateApnFunctionStatus`,属独立业务契约,未接入。
- 当前小程序/Web 批次入口仍默认写入 CMIOT;singleCutNet 忽略该字段,不影响执行。
- 小程序操作预检仍读取本地 data_service_status,可能与实时状态不同;后续应与上游校验统一。
- Worker 目前依据网关 code=0 判成功并更新本地状态。电信返回流水号表示订单受理,最终生效状态需要后续查单或实时 APN 查询核实,尚未做真实业务验收。
- 若启停请求发送后网络中断或成功响应缺流水号,结果未确认,不应盲目重试以免重复操作;先核对上游状态。
- singleCutNet 的 GET/POST 以网关实测为准:文档写 POST/GET,现网 v1 走 GET;
v3 适配器走 POST(`sim_dx_v3.go`),尚未实卡验收。
## 验证边界
回归测试采用 httptest 本地模拟上游,覆盖:查询字段兼容/缺字段/错误码;
开停的 ADD/DEL 方向、参数编码、XML 解析、缺结果码、缺流水号、非 0000、HTTP 失败保护,
并断言不携带 APN 定向参数。
测试不对真实卡发起启停,不替代生产通道权限、配置及 Worker 全链路验收。