diff --git a/docs/apika-shared-token-integration.md b/docs/apika-shared-token-integration.md deleted file mode 100644 index 820f2ea..0000000 --- a/docs/apika-shared-token-integration.md +++ /dev/null @@ -1,25 +0,0 @@ -# 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. diff --git a/docs/mingqian-integration.md b/docs/mingqian-integration.md deleted file mode 100644 index 960d3f3..0000000 --- a/docs/mingqian-integration.md +++ /dev/null @@ -1,214 +0,0 @@ -# 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. diff --git a/docs/mobile-public-token-integration.md b/docs/mobile-public-token-integration.md deleted file mode 100644 index 1d64e53..0000000 --- a/docs/mobile-public-token-integration.md +++ /dev/null @@ -1,24 +0,0 @@ -# 移动公共 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,并在同一通道的并发请求间合并刷新动作。 diff --git a/docs/plans/channel-template-model-design.md b/docs/plans/channel-template-model-design.md deleted file mode 100644 index 4720c1c..0000000 --- a/docs/plans/channel-template-model-design.md +++ /dev/null @@ -1,119 +0,0 @@ -# 通道模板模型设计 - -## 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 直接执行。 diff --git a/docs/plans/telecom-v3-integration-and-switch.md b/docs/plans/telecom-v3-integration-and-switch.md deleted file mode 100644 index eb5a7b3..0000000 --- a/docs/plans/telecom-v3-integration-and-switch.md +++ /dev/null @@ -1,130 +0,0 @@ -# 电信 v1 现状、v3 并行接入与切换 - -调查日期:2026-10-10。代码依据为当前本地三个服务仓库;数据库通过 DBX 只读查询 `开发环境(127.0.0.1).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、频控、运营商订单完成状态及多副本热刷新仍需部署后联调。没有修改数据库、切换通道或调用真实停复机/断网。 diff --git a/docs/scripts/query_sim_card_status.sh b/docs/scripts/query_sim_card_status.sh deleted file mode 100755 index 0fed31e..0000000 --- a/docs/scripts/query_sim_card_status.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -# 物联网卡卡状态查询 — 请求体与 internal/model/sim_chan_api.go 中 SimCardStatusInput 一致(simKeys) -# 请仅使用获得授权的测试卡 -# 用法:在工程根目录执行 -# 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 [[ -z "${ICCID:-}" ]]; then - echo "请设置 ICCID(仅获授权的测试卡)" >&2 - exit 2 -fi -IFS=',' read -r -a ICCIDS <<< "${ICCID}" - -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 diff --git a/docs/scripts/query_sim_session.sh b/docs/scripts/query_sim_session.sh deleted file mode 100755 index ae2149f..0000000 --- a/docs/scripts/query_sim_session.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -# 查询 SIM 卡会话 — 请求体与 internal/model/sim_chan_api.go 中 SimSessionInput 一致(simKeys) -# 请仅使用获得授权的测试卡 -# 用法:在工程根目录执行 -# 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 [[ -z "${ICCID:-}" ]]; then - echo "请设置 ICCID(仅获授权的测试卡)" >&2 - exit 2 -fi -IFS=',' read -r -a ICCIDS <<< "${ICCID}" - -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 diff --git a/docs/sql/migrate_shared_token_templates.sql b/docs/sql/migrate_shared_token_templates.sql deleted file mode 100644 index 8838052..0000000 --- a/docs/sql/migrate_shared_token_templates.sql +++ /dev/null @@ -1,37 +0,0 @@ --- Migration guide for the shared-token template split. --- Review rows before running against a production database. This script does --- not move or retain Henan Lelian credentials. -SET NAMES utf8mb4; - --- 1. Inspect the archived template and its channels. -SELECT temp_key, temp_model, temp_config -FROM t_sim_channel_template -WHERE temp_key = 'sim_onelink_shared_v5'; - -SELECT chan_key, chan_config, chan_status -FROM t_sim_channels -WHERE temp_key = 'sim_onelink_shared_v5'; - --- 2. Seed the new templates first (see seed_onelink_shared_channel.sql), --- then migrate only Tengyu rows after reviewing their provider value. --- The JSON functions require MySQL 5.7+ and intentionally preserve only the --- channel credentials. Public API/provider settings live in the new template. -UPDATE t_sim_channels -SET temp_key = 'sim_onelink_tengyu_v1', - chan_config = JSON_OBJECT( - 'credentials', JSON_OBJECT( - 'pwdKey', JSON_UNQUOTE(JSON_EXTRACT(chan_config, '$.auth.pwdKey')), - 'channelNo', JSON_UNQUOTE(JSON_EXTRACT(chan_config, '$.auth.channelNo')) - ) - ) -WHERE temp_key = 'sim_onelink_shared_v5' - AND LOWER(JSON_UNQUOTE(JSON_EXTRACT(chan_config, '$.auth.provider'))) = 'tengyu'; - --- 3. Henan Lelian rows must be disabled and manually migrated or removed. -UPDATE t_sim_channels -SET chan_status = 0 -WHERE temp_key = 'sim_onelink_shared_v5' - AND LOWER(JSON_UNQUOTE(JSON_EXTRACT(chan_config, '$.auth.provider'))) = 'henan_lelian'; - --- 4. After all rows are reviewed and migrated, remove the archived template. --- DELETE FROM t_sim_channel_template WHERE temp_key = 'sim_onelink_shared_v5'; diff --git a/docs/sql/seed_onelink_shared_channel.sql b/docs/sql/seed_onelink_shared_channel.sql deleted file mode 100644 index 579cc35..0000000 --- a/docs/sql/seed_onelink_shared_channel.sql +++ /dev/null @@ -1,65 +0,0 @@ --- Shared OneLink template/channel examples. Token endpoint and credentials --- belong to the shared-token template configuration. -SET NAMES utf8mb4; - -INSERT INTO `t_sim_channel_template` ( - `temp_key`, `temp_name`, `temp_model`, `temp_config`, `temp_status`, - `tenant_id`, `created_dept`, `created_by`, `updated_by` -) VALUES ( - 'sim_onelink_tengyu_v1', - 'Tengyu shared OneLink token v1', - 'shared_token', - '{"token":{"provider":"tengyu","baseUrl":"http://www.sdtywl.cn/","path":"/prod-api/open/api/oneLinkToken"}}', - 1, '000000', 103, 1, 1 -) -ON DUPLICATE KEY UPDATE - `temp_name` = VALUES(`temp_name`), `temp_model` = VALUES(`temp_model`), - `temp_config` = VALUES(`temp_config`), `temp_status` = VALUES(`temp_status`), - `updated_by` = VALUES(`updated_by`); - -INSERT INTO `t_sim_channels` ( - `temp_key`, `chan_key`, `chan_name`, `chan_config`, `chan_status`, - `chan_alias`, `region_code`, `app_key`, `app_secret`, `access_token`, - `refresh_token`, `tenant_id`, `created_dept`, `created_by`, `updated_by` -) VALUES ( - 'sim_onelink_tengyu_v1', - 'OneLink_TENGYU_EXAMPLE', - 'OneLink Tengyu shared-token example', - '{"credentials":{"pwdKey":"REPLACE_WITH_PWDKEY","channelNo":"REPLACE_WITH_CHANNEL_NO"}}', - 1, '', '', '', '', '', '', '000000', 103, 1, 1 -) -ON DUPLICATE KEY UPDATE - `temp_key` = VALUES(`temp_key`), `chan_key` = VALUES(`chan_key`), - `chan_config` = VALUES(`chan_config`), `chan_status` = VALUES(`chan_status`), - `updated_by` = VALUES(`updated_by`); - -INSERT INTO `t_sim_channel_template` ( - `temp_key`, `temp_name`, `temp_model`, `temp_config`, `temp_status`, - `tenant_id`, `created_dept`, `created_by`, `updated_by` -) VALUES ( - 'sim_onelink_xuzhou_mobile_v1', - 'Xuzhou Mobile shared OneLink token v1', - 'shared_token', - '{"token":{"provider":"xuzhou_mobile","baseUrl":"http://iot.yuanjianiot.com","path":"/admin/api/get/token","carrierType":1}}', - 1, '000000', 103, 1, 1 -) -ON DUPLICATE KEY UPDATE - `temp_name` = VALUES(`temp_name`), `temp_model` = VALUES(`temp_model`), - `temp_config` = VALUES(`temp_config`), `temp_status` = VALUES(`temp_status`), - `updated_by` = VALUES(`updated_by`); - -INSERT INTO `t_sim_channels` ( - `temp_key`, `chan_key`, `chan_name`, `chan_config`, `chan_status`, - `chan_alias`, `region_code`, `app_key`, `app_secret`, `access_token`, - `refresh_token`, `tenant_id`, `created_dept`, `created_by`, `updated_by` -) VALUES ( - 'sim_onelink_xuzhou_mobile_v1', - 'OneLink_XUZHOU_MOBILE_EXAMPLE', - 'OneLink Xuzhou Mobile shared-token example', - '{"credentials":{"appId":"REPLACE_WITH_APP_ID"}}', - 1, '', '', '', '', '', '', '000000', 103, 1, 1 -) -ON DUPLICATE KEY UPDATE - `temp_key` = VALUES(`temp_key`), `chan_name` = VALUES(`chan_name`), - `chan_config` = VALUES(`chan_config`), `chan_status` = VALUES(`chan_status`), - `updated_by` = VALUES(`updated_by`); diff --git a/docs/sql/seed_recharge_channel.sql b/docs/sql/seed_recharge_channel.sql deleted file mode 100644 index 1031e3d..0000000 --- a/docs/sql/seed_recharge_channel.sql +++ /dev/null @@ -1,100 +0,0 @@ -SET NAMES utf8mb4; - --- 腾宇(综合充值平台)通道初始化。 --- 字段复用约定(internal/logic/upstreamimpl/sim_recharge.go): --- t_sim_channels.app_key = pwdkey(个人中心 > 密钥 Key) --- t_sim_channels.app_secret = pwdiv(签名 IV,sign=md5(pwdkey+pwdiv+timestamp)) --- 执行前必须替换以下三个占位符。 -SET @recharge_pwdkey = 'REPLACE_WITH_PWDKEY'; -SET @recharge_pwdiv = 'REPLACE_WITH_PWDIV_OR_IV'; -SET @recharge_chan_key = 'REPLACE_WITH_LOCAL_CHAN_KEY'; - --- 上线前需在腾宇后台配置网关出口 IP 白名单,否则上游返回 407。 --- 上游单接口限流 60 次/分;全量同步触发 429 时由下轮重试兜底。 - -START TRANSACTION; - -INSERT INTO `t_sim_channel_template` ( - `temp_key`, - `temp_name`, - `temp_model`, - `temp_config`, - `temp_status`, - `tenant_id`, - `created_dept`, - `created_by`, - `updated_by` -) VALUES ( - 'sim_recharge_sdtywl_v1', - '综合充值平台 API', - '{}', - '{"baseUrl":"http://www.sdtywl.cn/"}', - 0, - '000000', - 100, - 1, - 1 -) -ON DUPLICATE KEY UPDATE - `temp_name` = VALUES(`temp_name`), - `temp_model` = VALUES(`temp_model`), - `temp_config` = VALUES(`temp_config`), - `temp_status` = VALUES(`temp_status`), - `updated_by` = VALUES(`updated_by`); - -INSERT INTO `t_sim_channels` ( - `temp_key`, - `chan_key`, - `chan_name`, - `chan_config`, - `chan_status`, - `chan_alias`, - `region_code`, - `app_key`, - `app_secret`, - `access_token`, - `access_token_created`, - `access_token_expire`, - `refresh_token`, - `login_url`, - `tenant_id`, - `created_dept`, - `created_by`, - `updated_by` -) VALUES ( - 'sim_recharge_sdtywl_v1', - @recharge_chan_key, - '仟所腾宇', - '{}', - 0, - '腾宇', - '', - @recharge_pwdkey, - @recharge_pwdiv, - NULL, - NULL, - NULL, - NULL, - 'http://www.sdtywl.cn/', - '000000', - 100, - 1, - 1 -) -ON DUPLICATE KEY UPDATE - `temp_key` = VALUES(`temp_key`), - `chan_name` = VALUES(`chan_name`), - `chan_config` = VALUES(`chan_config`), - `chan_status` = VALUES(`chan_status`), - `chan_alias` = VALUES(`chan_alias`), - `region_code` = VALUES(`region_code`), - `app_key` = VALUES(`app_key`), - `app_secret` = VALUES(`app_secret`), - `access_token` = NULL, - `access_token_created` = NULL, - `access_token_expire` = NULL, - `refresh_token` = NULL, - `login_url` = VALUES(`login_url`), - `updated_by` = VALUES(`updated_by`); - -COMMIT; diff --git a/docs/sql/seed_template_models.sql b/docs/sql/seed_template_models.sql deleted file mode 100644 index e65f9de..0000000 --- a/docs/sql/seed_template_models.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Public protocol configuration for the built-in templates. --- Run after the corresponding template rows exist. -SET NAMES utf8mb4; - -INSERT INTO t_sim_channel_template - (temp_key, temp_name, temp_model, temp_config, temp_status, tenant_id, created_dept, created_by, updated_by) -VALUES - ('sim_yd_v5', 'China Mobile OneLink v5', 'direct', - '{"api":{"baseUrl":"https://api.iot.10086.cn/v5/","timeout":"10s"},"token":{"path":"/ec/get/token"}}', - 1, '000000', 103, 1, 1), - ('sim_yd_mock_v5', 'China Mobile OneLink mock v5', 'direct', - '{"api":{"baseUrl":"http://iotlcm.example.com/onelink-mock/v5/"}}', - 1, '000000', 103, 1, 1), - ('sim_dx_v1', 'China Telecom API v1', 'direct', - '{"api":{"baseUrl":"https://cmp-api.ctwing.cn:20164"}}', - 1, '000000', 103, 1, 1) -ON DUPLICATE KEY UPDATE - temp_model = VALUES(temp_model), temp_config = VALUES(temp_config), - temp_status = VALUES(temp_status), updated_by = VALUES(updated_by); diff --git a/docs/telecom-v3-apn-integration.md b/docs/telecom-v3-apn-integration.md deleted file mode 100644 index 08c35a8..0000000 --- a/docs/telecom-v3-apn-integration.md +++ /dev/null @@ -1,65 +0,0 @@ -# 电信 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 全链路验收。