Files
junhong_cmp_fiber/scripts/migration/README.md
2026-06-09 11:29:40 +08:00

12 KiB
Raw Blame History

奇成数据迁移脚本

详细方案见 docs/脚本/奇成数据迁移方案.md

1. 四步走

业务方           我们                                  运维
  │              │                                     │
  │ cards.csv    │ scan_legacy.py → mapping.yaml      │
  │ devices.csv  │   ↓ 人工填 target_*                │
  ├─────────────▶│ migrate_assets.py → step1_*.sql    │
  │              │ migrate_runtime.py → step2_*.sql   │
  │              ├────────────────────────────────────▶│ psql -1 -f step*.sql
  │              │                                     │ migration-finalize
  │              │                                     │ 重建轮询队列/缓存

2. 文件清单

scripts/migration/
├── README.md
├── requirements.txt
├── scan_legacy.py             ← 第1步:扫描奇成,生成 mapping.yaml 模板
├── migrate_assets.py          ← 第2步:生成资产 SQL
├── migrate_runtime.py         ← 第3步:生成关联数据 SQL
├── lib/                       ← 公共库
├── resources/
│   ├── README.md
│   ├── cards.csv.example      ← 业务方提供:iccid + 可选覆盖列
│   └── devices.csv.example    ← 业务方提供:设备 + 卡槽
├── config/
│   ├── mapping.yaml.example   ← 唯一映射配置(全局 + carrier + package + series + agent)
│   └── legacy_dsn.yaml.example← 奇成 MySQL 只读 DSN
└── output/                    ← 生成的 SQL 与日志

3. 详细流程

3.1 业务方提供数据

把两个 CSV 放到 resources/ 下(命名为 cards.csv / devices.csv):

  • cards.csv:iccid_19/iccid_20 至少填一个;仅填 iccid_20 时脚本自动派生前 19 位,但奇成主表只按 20 位精确查询;is_industry 可选
  • devices.csv:imei / virtual_no 至少填一个;device_name / device_model / device_type / max_sim_slots 可选;sim_iccid_* 引用 cards.csv 中的 ICCID(可填 19 或 20 位;若 19 位前缀对应多张 20 位卡,必须填完整 20 位)

字段说明详见 resources/README.md

3.2 我们扫描 + 填映射

pip install -r requirements.txt
cp config/legacy_dsn.yaml.example config/legacy_dsn.yaml  # 填真实 DSN
cp config/mapping.yaml.example config/mapping.yaml        # 第一次跑要有这个文件

# 扫描奇成,把发现的 carrier/package/agent 写入 mapping.yaml
python3 scan_legacy.py

# 看 config/mapping_todo.txt,把 target_* 全部填好
vim config/mapping.yaml

scan_legacy.py 可以反复跑(增量合并),已填的 target_* 不会被覆盖。

3.3 生成 SQL

python3 migrate_assets.py   # → output/step1_*.sql + errors.csv
python3 migrate_runtime.py  # → output/step2_*.sql + warnings.csv

如果 errors.csv 有内容,意味着 mapping.yaml 还没填完整、或者某些 ICCID 在奇成查不到,处理完再重跑。

3.4 测试环境 / 线上执行

# 按编号顺序执行
psql -h $HOST -1 -f output/step1_01_iot_cards.sql
psql -h $HOST -1 -f output/step1_02_devices.sql
psql -h $HOST -1 -f output/step1_03_asset_identifiers.sql
psql -h $HOST -1 -f output/step1_04_asset_wallets.sql
psql -h $HOST -1 -f output/step1_05_sim_bindings.sql
psql -h $HOST -1 -f output/step1_06_shop_allocations.sql

psql -h $HOST -1 -f output/step2_01_migration_orders.sql
psql -h $HOST -1 -f output/step2_02_package_usages.sql
psql -h $HOST -1 -f output/step2_03_card_data_usage_update.sql
psql -h $HOST -1 -f output/step2_04_agent_wallet_init.sql
psql -h $HOST -1 -f output/step2_05_agent_wallet_transactions.sql
psql -h $HOST -1 -f output/step2_06_asset_series_update.sql

-1 = 单事务,失败回滚整文件;ON CONFLICT DO NOTHING 保证重跑安全。

SQL 导入完成后必须按批次执行迁移收尾。SQL 只负责落库,不会触发 Service 生命周期, 也不会自动写入 Redis 轮询队列或卡缓存:

# 先看本批次会处理多少卡、会写哪些轮询任务,不会写 Redis
go run ./cmd/migration-finalize --batch-no MIGRATION-QICHENG-YYYYMMDD

# dry-run 无误后再执行,会按 tb_polling_config 重建本批卡的轮询队列和卡缓存
go run ./cmd/migration-finalize --batch-no MIGRATION-QICHENG-YYYYMMDD --apply

4. 配置说明 (mapping.yaml)

唯一需要人工填写的文件:

migration_user_id: 0                         # 后台超级管理员 ID,作为所有 creator/updater
migration_batch_no: "MIGRATION-QICHENG-YYYYMMDD" # 本次迁移批次号,线上每批必须唯一

carriers:              # 由 scan_legacy 自动发现 legacy_*,你只填 target_*
  - legacy_account_id: "72493DB64A2A4F01B5E59E49D58A2A34"
    legacy_account_name: "联通10-1"
    legacy_category: 9                 # 奇成运营商大类,仅作辅助判断
    legacy_category_name: "中国联通"
    target_carrier_id: 1585       ← 必填:新库 tb_carrier.id
    target_carrier_type: CUCC     ← 必填:CMCC/CUCC/CTCC/CBN
    target_carrier_name: "联通22-..."

packages:              # 同理
  - legacy_meal_id: "D6554244711498752"
    legacy_meal_name: "三网充电宝全国畅玩年卡套餐..."
    target_package_id: 2          ← 必填:新库 tb_package.id

series:                # 仅保留奇成套餐系列快照,生成器不要求填写
  - legacy_series_id: "..."
    legacy_series_name: "国内卡 无预存30天"
    target_series_id: null

agents:                # 决定所有卡/设备的店铺归属
  - legacy_agent_id: "D15EC779767B412A82B8448AC2324F04"
    legacy_agent_name: "叶发伟"
    target_shop_code: "SHOP20260428103424CZZQ"  # 留 null = 该代理的卡进平台库存

店铺归属是怎么决定的

CSV 里没有 target_shop_code 列,所有归属都从 mapping.agents 推导:

资产 归属规则
独立卡 奇成 tbl_card.agent_idmapping.agents[].target_shop_code
设备 主卡(sim_iccid_1)的 agent_id → 同上
设备上的卡 跟随设备所属店铺(shop_id 与设备一致,设备分配时卡状态同步为已分销)

如果 agent 在 mapping.agentstarget_shop_code 留空 → 该资产进平台库存。

5. 识别迁移数据(后台报表过滤用)

数据 识别条件
迁移伪订单 WHERE order_no LIKE 'MIG-%'
迁移分配记录 WHERE allocation_no LIKE 'MIG-ALLOC-%'
迁移钱包流水 WHERE metadata->>'source' = 'qicheng'

迁移伪订单号固定为 MIG-CARD-<ICCID>:

  • 20 位卡总长 29,19 位卡总长 28,符合新库 order_no varchar(30) 限制。
  • 订单号不包含生成时间,step2_01_migration_orders.sqlstep2_02_package_usages.sql 分开重跑也能稳定匹配。

6. 流量用量迁移规则(真用量 / 虚用量)

奇成与新系统的"虚流量"模型口径不同,必须做一次反算才能落对地方。

6.1 两边的口径差异

奇成 新系统
用量存储 tbl_card.total_bytes_cnt(varchar 小数 MB)= 虚用量(真 × (1 + flow_add_discount/100)) tb_iot_card.data_usage_mb(BIGINT MB)= 真用量(网关返回的物理读数)
加成配置 tbl_set_meal.flow_add_discount(0~100 百分比,作用于"已用量") tb_package.enable_virtual_data + virtual_data_mb + virtual_ratio(= real_data_mb / virtual_data_mb)
用户看到的"已用" = 数据库 total_bytes_cnt(已加成) 前端用 data_usage_mb × display_gain_ratio_snapshot 现算

6.2 反算公式

迁移脚本对每张卡按下式得到真用量,再写入 data_usage_mb / current_month_usage_mb / last_gateway_reading_mb:

real_used_mb = total_bytes_cnt / (1 + flow_add_discount / 100)

其中 flow_add_discount 取自该卡在 tbl_card_life(status=1, expire_date 最大)对应的当前生效套餐。

6.3 精度与近似

  • 跨多个套餐周期的累计:total_bytes_cnt 是按各时段对应套餐虚比加权累加的,这里只用"当前生效套餐"的虚比反算,有误差但可接受(奇成没有按套餐分段的用量明细,无更好办法)。
  • 无当前生效套餐却有 total_bytes_cnt:按 flow_add_discount=0 处理(真=虚),并写入 warnings.csv(kind=usage_without_active_package)供人工核查。
  • 整数取整规则(data_usage_mb 是 BIGINT):
    • real_used_mb = 0 → 写 0
    • 0 < real_used_mb < 1上取整为 1,避免"明明用过的卡显示 0"
    • real_used_mb ≥ 1 → 向下取整
  • 小数字段保留精度:current_month_usage_mb(numeric)与 last_gateway_reading_mb(double)直接写 Decimal 真值,作为下次轮询增量计算的基线,避免轮询第一次回写时产生跳变

6.4 套餐使用记录的虚流量快照

tb_package_usagevirtual_total_mb_snapshot / display_gain_ratio_snapshot / enable_virtual_data_snapshot 三个字段全部从新系统 tb_package 当场快照,与奇成无关:

快照字段 取值
virtual_total_mb_snapshot 启用虚:virtual_data_mb;否则:real_data_mb
display_gain_ratio_snapshot 启用虚:virtual_ratio(即 real_data_mb/virtual_data_mb);否则:1.0
enable_virtual_data_snapshot 启用虚:TRUE;否则:FALSE
data_limit_mb 始终 real_data_mb(真总量)
data_usage_mb LEAST(real_used_mb_floor, virtual_total_mb_snapshot)(防卫上限)

6.5 设备绑定卡的套餐归属

设备上的卡在新系统运行态会按设备维度判断有效套餐和复机条件,因此迁移时只用设备当前槽位卡(默认 sim_iccid_1)作为奇成套餐来源,生成一条 usage_type=devicedevice_id=设备ID 的套餐使用记录。其他槽位绑定卡不重复生成主套餐,避免同一设备出现多条并列生效主套餐。

设备和卡的 series_id 以新系统目标套餐为准:脚本通过 mapping.packages[].target_package_id 对应的 tb_package.series_id 写入资产,并额外生成 step2_06_asset_series_update.sql,用于修复已经导入但因 ON CONFLICT DO NOTHING 没有被 step1 更新到的资产。

7. 重要约束

  • 所有奇成查询走 SET TRANSACTION READ ONLY,严禁误写。
  • 所有 INSERT 用 ON CONFLICT DO NOTHING,严禁更新已有数据。
  • 伪订单的 payment_status=2 commission_status=2 commission_result=2,避免被支付回调、佣金调度器扫到。
  • 迁移卡默认 status=1(在库) / activation_status=0(未激活),激活由后续业务流程驱动。

8. 常见问题

现象 排查
scan_legacy.pypymysql 缺失 pip install -r requirements.txt
errors.csv 出现 carrier_not_in_mapping mapping.yaml.carriers 没覆盖到这个 account_id/account_name,重跑 scan_legacy.py 自动补
errors.csv 出现 carrier_target_missing mapping.yaml.carriers 里有这一项但 target_* 还是 null,手动填
errors.csv 出现 not_in_legacy 该 ICCID 在奇成 tbl_card 查不到,业务方确认
errors.csv 出现 legacy_duplicate 同一 ICCID 在奇成命中多条 tbl_card,业务方人工确认是哪张物理卡
errors.csv 出现 carrier_length_conflict 业务方填的 iccid_20 与奇成查到的运营商不匹配(CTCC 不该有 20 位 / 非 CTCC 必须有 20 位)
errors.csv 出现 iccid_20_prefix_mismatch iccid_20 前 19 位 ≠ iccid_19,业务方校对
warnings.csv 出现 agent_no_legacy mapping.agents 配了但奇成没有 tbl_agent 记录,业务方确认
warnings.csv 出现 usage_without_active_package 该卡奇成 total_bytes_cnt > 0tbl_card_life 中没有 status=1 的当前生效套餐,无法精确反算真用量(按真=虚处理),详见 §6.3
线上报 null value in column "creator" migration_user_id 是 0,某些约束可能拒绝;改成真实管理员 ID
线上报 null shop_id mapping.agents.target_shop_code 指向的店铺在新库不存在,先建店铺