Files
junhong_cmp_fiber/openspec/changes/add-gateway-readonly-prod-import/design.md
邹进 5cf67b98de
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m36s
feat: 网关只读资产改为数据表,只读资产只允许查询类调用
- 新增 tb_gateway_readonly_card(卡 ICCID / 设备 IMEI)与开关 JUNHONG_GATEWAY_READONLY_ENABLED,
  替换环境变量名单 JUNHONG_GATEWAY_READONLY_ICCIDS
- 只读资产只放行查卡状态、查实名、查流量、查设备信息、查设备槽位、同步设备状态;
  停机、复机、限速、实名认证链接与设备写操作在网关出口本地拦截;查表失败按拦截;不缓存
- 测试环境部署开启只读模式;新增技术方案与清理顺序文档

Change: add-gateway-readonly-prod-import

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 17:55:52 +09:00

5.2 KiB
Raw Blame History

Context

完整背景、数据规模与确认事项见 docs/测试环境导入生产数据验证流量计量技术方案.md,撤除顺序见 docs/测试环境生产卡只读模式清理顺序.md。本文只记录实现层的关键决策。

现状:

  • 提交 6266cf9d 已在 internal/gateway/readonly_guard.go 实现按环境变量 JUNHONG_GATEWAY_READONLY_ICCIDS(内存 map)拦截写操作,测试环境配置了两张卡(测试卡 12244、12245)。
  • 全部网关调用经过 internal/gateway/client.go 的 doRequest / doRequestWithoutPayloadLog;网关路径共 15 个,其中查询类 6 个:/flow-card/flow、/flow-card/status、/flow-card/realName、/device/info、/device/slot-info、/device/sync-info。
  • 生产(迁移 240):卡 98,567、设备 20,144、有效绑定 38,667、待生效 / 生效中 / 已用完套餐 29,175;测试库迁移 268。
  • 部署是否完成以 Gitea Actions 运行结果为准;数据库连接池每 5 分钟重建连接,连接建立时间不能作为重启依据。

Goals / Non-Goals

Goals:

  • 只读资产在测试环境只能被查询,任何入口都发不出写调用。
  • 承载约 12 万个卡号与设备号,写入后立即生效。
  • 全量导入后按现有轮询运行,算出新计量逻辑与生产的差距。

Non-Goals:

  • 订单、钱包、分佣、退款、个人客户、审批等数据导入与流程验证。
  • 生产发布、回滚;验证结束后的撤除(按清理文档另行执行)。
  • 修改业务代码中的停复机、套餐、轮询流程。

Decisions

D1 只在网关出口判断。 停复机、限速、设备指令的入口分散在轮询、扣减、套餐生命周期、订单、退款、后台按钮等十余处,逐处加判断容易漏;网关客户端是唯一出口,在这里判断一处覆盖全部,业务代码不动。

D2 白名单放行查询类调用,其余按表拦截。 只读资产的调用按路径分两类:6 个查询类路径(查卡状态、查实名、查流量、查设备信息、查设备槽位、同步设备状态)直接放行且不查表(轮询绝大部分调用是查询,不增加数据库压力);其他路径一律按卡号查表,命中即拦截。新增网关接口时默认被拦截,比黑名单安全。

D3 不缓存,逐次查表。 非查询类调用不频繁,按唯一索引查表约 1 ms;不缓存避免了"写入只读表到生效"的空窗,导入流程可以写表后立即导数据。网关包不依赖 GORM:以函数接缝 func(ctx, cardNo) (bool, error) 注入,存储实现放在 internal/store/postgres。

D4 查表失败按拦截。 宁可写调用失败,也不放过真卡。代价是开关开着时删表会使所有非查询调用失败,因此清理顺序要求先关开关再删表。

D5 拦截返回明确失败。 返回网关错误类别 upstream(业务拒绝),不是超时或结果未知,避免停复机进入"结果未知 → 恢复扫描"路径;拦截日志带 metric=gateway.readonly_card.blocked、路径与卡号。

D6 卡号取请求体 cardNo。 卡接口的 cardNo 是 ICCID,设备接口的 cardNo 是 IMEI;只读表按大写保存与比较。取不到卡号的非查询请求不拦截(目前没有此类调用,批量查询接口未实现)。

D7 导入用临时脚本直接写测试库。 后台导卡接口只能导卡,不能导设备与套餐,也不适合 10 万级数据。脚本只读导出生产、只允许连接 junhong_cmp_test;顺序为:写只读表 → 确认开关已部署且行数一致 → 导入卡、资产标识、设备、绑定、套餐 → 置待建起点 → 触发轮询重新加载。套餐只存真实已用量,虚流量已用量由展示倍率现算;条款快照(虚总量、展示倍率、是否启用虚流量)照原样复制。

D8 差距按增量比较。 套餐带入生产导入时刻的真实已用量,两边起点一致;差额 = 测试环境新增扣减 − 生产同期新增扣减。导入前的差距由 flowcollector 本月读数给出上限。

Risks / Trade-offs

  • [只读表写入前就导入数据,导入卡立即触发停复机] → 导入脚本先写只读表并核对行数,且只在 Gitea Actions 显示开关部署成功后执行导入。
  • [开关开着时删表,所有非查询调用失败] → 清理顺序:先处理导入资产 → 关开关并部署 → 删表。
  • [新增网关写接口忘记评估] → 白名单策略下默认拦截。
  • [导入数据量大,轮询与采集记录、日快照增长快] → 测试环境可接受;采集记录按月分区、90 天保留。
  • [导入资产的停复机失败记录大量出现] → 预期行为,用于识别"按新逻辑该停 / 该复"的资产。

Migration Plan

  1. 新增成对迁移建 tb_gateway_readonly_card(up 建表;down 删表)。
  2. 代码切换为按表判断,删除 JUNHONG_GATEWAY_READONLY_ICCIDS;部署脚本配置 JUNHONG_GATEWAY_READONLY_ENABLED=true。
  3. 部署前在测试库写入现有两张只读卡,再推送部署,避免两张卡在切换期间失去保护。
  4. 部署成功后执行全量导入(D7)。
  5. 回滚 / 撤除:按 docs/测试环境生产卡只读模式清理顺序.md。

Open Questions

(无,技术方案第 9 节已全部确认)