Files
junhong_cmp_fiber/openspec/changes/refactor-migration-ownership-package-flow/design.md
Break 1f634eb465
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m3s
init
2026-06-16 15:15:50 +08:00

7.9 KiB
Raw Blame History

Context

当前奇成迁移脚本分为 scan_legacy.pymigrate_assets.pymigrate_runtime.py。脚本可以生成卡、设备、绑定、分配、伪订单、套餐使用记录和系列回填 SQL但多个关键决策依赖奇成查询结果和脚本默认值

  • 设备归属由 sim_iccid_1 对应卡的奇成 agent_id 推导。
  • 设备套餐来源由 current_slot 决定,但 CSV 未提供时默认 1
  • 套餐迁移只解析一条当前正式套餐,无法覆盖“当前生效 + 未生效待生效”的队列。
  • 卡和设备共用一套套餐解析路径,导致设备绑定卡、独立卡、卡级系列和设备级系列边界不清。
  • 缺失代理映射或未命中奇成归属时,脚本会把资产留在平台库存,无法体现人工指定的迁移目标。

本次变更只涉及 scripts/migration/ 下的离线迁移脚本和文档,不改变线上 Go API、Handler/Service/Store/Model 分层和业务数据库结构。

Goals / Non-Goals

Goals:

  • mapping.yaml 成为迁移决策源:资产给哪个店铺、设备当前槽位、套餐来源槽位、套餐迁移范围都由配置决定。
  • 支持万级迁移的批量规则:默认店铺、设备/独立卡分支规则、按槽位规则、少量资产级覆盖。
  • 拆分单卡迁移与设备迁移分支,避免独立卡和设备绑定卡共用不清晰逻辑。
  • 迁移当前生效和未生效待生效正式套餐,分别写入 tb_package_usage.status=1status=0
  • 为每次生成 SQL 输出可审计 CSV让业务和开发能在执行前确认归属、套餐映射和跳过原因。
  • 保持奇成连接只读、SQL 幂等、人工执行和当前脚本目录内的轻量实现方式。

Non-Goals:

  • 不新增后台 API 或前端页面。
  • 不新增数据库表或修改线上业务模型。
  • 不迁移历史已过期套餐、加油包、历史订单明细或历史流量日明细。
  • 不逐资产要求配置店铺;逐资产配置仅用于异常覆盖。
  • 不新增自动化测试或 _test.go

Decisions

决策 1使用“批量规则 + 覆盖项”的配置模型

选择:在 mapping.yaml 中新增 ownership_rulespackage_rulesoverrides 三类配置。

示例:

ownership_rules:
  default_target_shop_code: KWTX
  device:
    mode: default_shop
    current_slot: 2
    package_source_slot: 2
  standalone_card:
    mode: default_shop

package_rules:
  migrate_statuses:
    - active
    - pending
  packages:
    - legacy_meal_id: D6695908379395072
      target_package_id: 40

overrides:
  devices:
    - virtual_no: "862639073940258"
      target_shop_code: OTHER
      current_slot: 1
      package_source_slot: 1
  cards:
    - iccid: "89861590172420360956"
      target_shop_code: OTHER

理由:单批上万资产不能逐条配置,默认规则覆盖 99% 场景,覆盖项只处理少量例外。

替代方案:继续使用 mapping.agents 从奇成代理推导店铺。否决原因:奇成代理不是本次迁移的权威归属,且无代理或代理未映射时会出现静默漏分配。

决策 2拆分独立卡与设备套餐分支

选择

  • 独立卡:只为未绑定设备的卡生成 usage_type='single_card'
  • 设备:只按设备的 package_source_slot 对应卡生成 usage_type='device',其他槽位卡只做绑定和卡基础信息,不重复生成主套餐。

理由:新系统运行态按设备维度判断设备套餐和复机条件,设备内多卡不能重复生成并列主套餐。独立卡和设备套餐生命周期也需要不同的资产定位字段。

替代方案:继续逐卡生成套餐后再靠 _runtime_asset_for_card() 折叠。否决原因:折叠逻辑隐蔽,难以解释某张卡为什么迁或不迁。

决策 3完整查询正式套餐生命周期并映射为 active/pending 队列

选择:新增查询函数读取每张来源卡在奇成 tbl_card_life 中所有正式套餐记录,排除 type=1 加油包和已过期记录,按当前时间和状态识别:

  • 当前生效:写入 status=1,保留 activated_atexpires_at 和已用量。
  • 未生效待生效:写入 status=0data_usage_mb=0,按开始/到期时间和队列顺序设置 priority

理由:新系统已支持 PackageUsageStatusPending=0 和队列激活能承接待生效套餐。当前脚本只取一条最大过期时间套餐解释不了“20G/100G 只迁一个”这类问题。

替代方案:只迁当前生效套餐。否决原因:业务已确认必须迁移当前生效 + 未生效待生效套餐。

决策 4生成审核 CSV 作为执行前合同

选择:每次生成 SQL 时输出:

  • ownership_resolution.csv:资产类型、资产标识、来源卡、目标店铺、决策来源、是否分配、原因。
  • package_resolution.csv资产类型、资产标识、来源卡、legacy 套餐、目标套餐、目标状态、优先级、决策、原因。
  • summary.txt:按资产类型、状态、错误类型汇总。

理由:迁移脚本是一次性高风险操作,执行前必须能从产物直接解释每个资产和套餐的处理结果。

替代方案:只写 errors.csv / warnings.csv。否决原因:只能看到异常,无法证明正常路径是否符合业务预期。

决策 5关键缺失项阻断不再静默降级

选择:以下情况必须写入 errors.csv 并跳过对应资产或套餐:

  • 目标店铺码不存在或为空且规则要求分配。
  • 设备 current_slot / package_source_slot 不在 1-4 或对应槽位无卡。
  • legacy 套餐没有 target_package_id
  • 目标套餐不存在、已删除或不是正式套餐。
  • 同一资产产生多个 active 主套餐且无法按规则裁决。

理由:静默进入平台库存或漏迁套餐会让后续人工排查成本远高于生成期阻断。

Risks / Trade-offs

  • [风险] 新配置结构与旧 mapping.yaml 不兼容 → 提供清晰错误和示例,必要时保留旧字段读取但输出升级提示。
  • [风险] 奇成套餐状态字段与真实生效窗口不一致 → 审核 CSV 同时输出奇成原始 status/type/start_date/expire_date,便于人工确认。
  • [风险] 待生效套餐优先级排序错误会影响自动激活顺序 → 按 start_date ASC, expire_date ASC, legacy_life_id ASC 生成稳定排序,并在 package_resolution.csv 输出 priority。
  • [风险] 批量默认店铺误配会影响大量资产ownership_resolution.csv 必须在执行 SQL 前人工抽查summary 输出目标店铺分布。
  • [Trade-off] 不做 UI 化配置 → 先保持脚本和 YAML满足迁移批次的速度和可审查性后续如迁移频率升高再考虑管理界面。

Migration Plan

  1. 扩展 mapping.yaml.exampleresources/README.md,明确批量规则、槽位字段和审核流程。
  2. 增强配置加载器,新增结构化配置对象和校验错误。
  3. 扩展 CSV 加载器,支持 current_slot / package_source_slot 列;未填时从 ownership_rules.device 取默认值。
  4. 扩展奇成查询,读取完整正式套餐生命周期。
  5. 重构 SQL 构造逻辑,拆分独立卡和设备套餐生成。
  6. 新增审核 CSV 与阻断错误输出。
  7. 用当前 120 台设备样例和新增小样例手动生成 SQL核对 120 台归属、4 类套餐迁移问题和待生效队列。

回滚:本变更只影响 SQL 生成脚本。若生成产物不符合预期,不执行输出 SQL 即可;已生成 SQL 可删除后用旧分支重新生成。

Open Questions

  • 是否所有迁移批次默认都只有一个目标店铺,还是需要支持按资源文件分组指定多个默认店铺?
  • 待生效套餐 activated_at 是否保留奇成 start_date,还是写 NULL 等待新系统激活时回填?
  • 同一来源卡存在多个奇成 status=1 正式套餐时,是否按时间窗口裁决一个 active其余转 pending还是直接阻断人工处理