This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 奇成数据迁移方案
|
||||
|
||||
> 目标:把奇成(旧 MySQL `kyhl` 库)的存量卡和设备迁移到新系统(PostgreSQL),输出 SQL 脚本供线上手动执行。
|
||||
> 范围:当前态资产 + 当前生效套餐 + 累计已用流量 + 代理佣金总值。**不迁移历史明细**。
|
||||
> 范围:当前态资产 + 当前生效正式套餐 + 未生效待生效正式套餐 + 累计已用流量 + 代理佣金总值。**不迁移历史明细、已过期套餐和加油包**。
|
||||
|
||||
---
|
||||
|
||||
@@ -14,12 +14,10 @@
|
||||
│ resources/devices.csv│
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
我们维护映射配置
|
||||
我们维护唯一决策配置
|
||||
┌──────────▼───────────┐
|
||||
│ config/carrier.yaml │
|
||||
│ config/series.yaml │
|
||||
│ config/package.yaml │
|
||||
│ config/shop.yaml │
|
||||
│ config/mapping.yaml │
|
||||
│ 归属/槽位/套餐映射 │
|
||||
└──────────┬───────────┘
|
||||
│
|
||||
┌──────────▼───────────┐
|
||||
@@ -45,9 +43,11 @@
|
||||
```
|
||||
|
||||
**关键约束**
|
||||
- 两个脚本都**不直接写入**线上库,只生成 `.sql` 文件
|
||||
- 两个脚本都**不直接写入**线上库,只生成 `.sql` 文件和审核 CSV
|
||||
- `config/mapping.yaml` 是归属、设备当前槽位、套餐来源槽位和套餐迁移范围的决策源;奇成只提供历史事实
|
||||
- 脚本对奇成线上库**只 SELECT**,DB 连接层强制只读事务(`SET TRANSACTION READ ONLY`)
|
||||
- 所有 INSERT 使用 `ON CONFLICT DO NOTHING`,支持重跑
|
||||
- `errors.csv` 非空时不执行 SQL,先修正配置/输入后重跑
|
||||
|
||||
---
|
||||
|
||||
@@ -84,13 +84,14 @@
|
||||
| `sim_iccid_2` | 否 | 插槽2的 iccid | `89860623...` |
|
||||
| `sim_iccid_3` | 否 | 插槽3的 iccid | |
|
||||
| `sim_iccid_4` | 否 | 插槽4的 iccid | |
|
||||
| `current_slot` | 否 | 当前使用的插槽位置(1-4),用于 `tb_device_sim_binding.is_current` | `1` |
|
||||
| `target_shop_code` | 否 | 目标店铺编码。**留空 = 进平台库存** | `SHOP20260428103424CZZQ` |
|
||||
| `current_slot` | 否 | 当前使用的插槽位置(1-4),用于 `tb_device_sim_binding.is_current`。空值按 `mapping.yaml` 批量规则或覆盖项填充 | `2` |
|
||||
| `package_source_slot` | 否 | 设备套餐来源槽位(1-4)。只迁该槽位卡的奇成正式套餐为设备套餐 | `2` |
|
||||
|
||||
**注意**
|
||||
- 设备上的卡,其 `cards.csv` 中 `target_shop_code` 必须留空(一致性校验由脚本检查)
|
||||
- `cards.csv` 必须先到位,`devices.csv` 的 `sim_iccid_*` 才能被校验
|
||||
- 业务方分别准备这两份是合理的,奇成 `tbl_card_relate`(主卡+副卡1+副卡2)的关系业务方应已掌握
|
||||
- 槽位解析优先级:`devices.csv` 行级配置 > `mapping.yaml.overrides.devices` > `mapping.yaml.ownership_rules.device` 批量默认值
|
||||
- `current_slot` 或 `package_source_slot` 对应槽位无卡时写入 `errors.csv` 并阻断该设备相关 SQL
|
||||
|
||||
---
|
||||
|
||||
@@ -123,9 +124,37 @@
|
||||
|
||||
## 四、我们维护的映射配置
|
||||
|
||||
迁移前**必须**在新系统建好店铺、运营商、套餐系列、套餐,然后填好以下映射。
|
||||
迁移前**必须**在新系统建好店铺、运营商、套餐系列、套餐,然后填好 `config/mapping.yaml`。该文件是本次迁移的唯一决策源。
|
||||
|
||||
### 4.1 `config/carrier_mapping.yaml`
|
||||
### 4.1 归属、槽位和套餐规则
|
||||
|
||||
```yaml
|
||||
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]
|
||||
|
||||
overrides:
|
||||
devices:
|
||||
- virtual_no: "862639073940258"
|
||||
target_shop_code: OTHER
|
||||
current_slot: 1
|
||||
package_source_slot: 1
|
||||
cards:
|
||||
- iccid: "89861590172420360956"
|
||||
target_shop_code: OTHER
|
||||
```
|
||||
|
||||
归属规则:独立卡按 `overrides.cards` 或 `ownership_rules.standalone_card` 决定;设备按 `overrides.devices` 或 `ownership_rules.device` 决定;设备上的卡跟随设备。奇成 `agent_id` 不再作为默认归属来源。
|
||||
|
||||
### 4.2 `config/mapping.yaml` 中的 carriers
|
||||
|
||||
```yaml
|
||||
# 奇成的运营商名称(业务方在 cards.csv 中填的)→ 新系统 carrier_id + type
|
||||
@@ -141,7 +170,7 @@ carriers:
|
||||
# ... 业务方提供的所有运营商都要列出
|
||||
```
|
||||
|
||||
### 4.2 `config/series_mapping.yaml`
|
||||
### 4.3 `config/mapping.yaml` 中的 series
|
||||
|
||||
```yaml
|
||||
# 奇成套餐系列名 → 新系统 series_id
|
||||
@@ -153,7 +182,7 @@ series:
|
||||
target_series_id: 6
|
||||
```
|
||||
|
||||
### 4.3 `config/package_mapping.yaml`
|
||||
### 4.4 `config/mapping.yaml` 中的 packages
|
||||
|
||||
```yaml
|
||||
# 奇成套餐名 → 新系统 package_id(cards.csv 的 package_name 直接对照这里)
|
||||
@@ -166,7 +195,7 @@ packages:
|
||||
|
||||
> 注:奇成的 `tbl_set_meal` 套餐表非常大,**不需要全部映射**,只映射本次迁移涉及的套餐(脚本预扫描 cards.csv 收集 package_name 集合,缺失的列出来让我们补)。
|
||||
|
||||
### 4.4 `config/shop_mapping.yaml`(可选别名)
|
||||
### 4.5 店铺编码
|
||||
|
||||
```yaml
|
||||
# 如果业务方愿意填 shop_code,本文件可以省略
|
||||
@@ -318,16 +347,15 @@ FROM c;
|
||||
```
|
||||
1. 读 cards.csv 得到 iccid 列表
|
||||
2. 连奇成(只读事务)批量查:
|
||||
- tbl_card_life WHERE iccid_mark IN (...) AND status=1 ORDER BY expire_date DESC
|
||||
→ 每张卡取最新生效的套餐:meal_id, meal_name, start_date, expire_date
|
||||
- tbl_card WHERE iccid_mark IN (...) → total_bytes_cnt (GB)
|
||||
- tbl_agent_commission_account → 按 agent_id 聚合 can_draw_amount, total_commision_amount
|
||||
- tbl_agent,根据 cards.csv 中 agent 信息映射出 shop_id
|
||||
3. 对每张卡:
|
||||
a. 套餐映射:legacy_meal_name → package_id(从 yaml 或业务方 csv)
|
||||
b. 生成伪订单 SQL(order_no = "MIG-CARD-" + iccid 后8位 + 时间戳)
|
||||
c. 生成 tb_package_usage SQL(关联伪订单 + 计算 snapshot 字段)
|
||||
d. 生成 UPDATE tb_iot_card SET data_usage_mb = ... (把 GB 转 MB)
|
||||
- `tbl_card_life` 当前生效正式套餐和 `tbl_next_month_card_life` 次月待生效套餐,保留 legacy 套餐 ID、套餐名、类型、状态、开始时间、到期时间和稳定排序键
|
||||
- `tbl_card.total_bytes_cnt` → 反算新系统真用量 MB
|
||||
- `tbl_agent_commission_account` / `tbl_agent` → 仅用于代理钱包初始化
|
||||
3. 对每个独立卡或设备套餐来源槽位:
|
||||
a. 套餐映射:`legacy_meal_id` → `target_package_id`(来自 `mapping.yaml.packages`)
|
||||
b. active 套餐写 `tb_package_usage.status=1`,pending 套餐写 `status=0` 并按稳定顺序写 `priority`
|
||||
c. 生成伪订单 SQL(order_no = `MIG-<ICCID>-<priority>`)
|
||||
d. 生成 `tb_package_usage` SQL(关联伪订单 + 计算 snapshot 字段)
|
||||
e. 生成 `UPDATE tb_iot_card SET data_usage_mb = ...`(真用量 MB)
|
||||
4. 对每个 shop(按代理映射出来的):
|
||||
a. 生成 INSERT/UPDATE tb_agent_wallet(总值)
|
||||
b. 生成一条 tb_agent_wallet_transaction(type=initial_migration, amount=总值, remark=源奇成代理ID)
|
||||
@@ -343,7 +371,10 @@ output/
|
||||
step2_03_card_data_usage_update.sql # 卡累计流量 UPDATE
|
||||
step2_04_agent_wallet_init.sql # 代理钱包总值
|
||||
step2_05_agent_wallet_transactions.sql # 迁移流水
|
||||
warnings.csv # 警告:找不到当前生效套餐、无法映射的代理等
|
||||
step2_06_asset_series_update.sql # 资产套餐系列回填
|
||||
package_resolution.csv # 套餐迁移审核文件
|
||||
errors.csv # 套餐映射缺失、多 active、槽位缺失等阻断错误
|
||||
warnings.csv # 用量反算和代理钱包警告
|
||||
summary.txt
|
||||
```
|
||||
|
||||
@@ -357,7 +388,7 @@ WITH new_order AS (
|
||||
total_amount, payment_method, payment_status, paid_at,
|
||||
source, generation, creator, updater, created_at, updated_at
|
||||
)
|
||||
SELECT 'MIG-CARD-' || RIGHT('89852000263338772439', 8) || '-' || TO_CHAR(NOW(), 'YYYYMMDDHH24MISS'),
|
||||
SELECT 'MIG-89852000263338772439-1',
|
||||
'single_card', 'personal', 0, c.id,
|
||||
0, 'offline', 2, NOW(),
|
||||
'migration', 1, 0, 0, NOW(), NOW()
|
||||
@@ -489,9 +520,9 @@ scripts/migration/
|
||||
| 1 | 业务方填的 carrier_name / package_name 与奇成不一致 → 映射失败 | 脚本预扫描,把所有未匹配项写到 unmatched.csv 让业务方修正 |
|
||||
| 2 | 奇成的 iccid 有 18/21/22 位脏数据 | 脚本第三节规则剔除,进 errors.csv |
|
||||
| 3 | 同一 iccid 被业务方填多次 | 脚本去重 + 警告 |
|
||||
| 4 | 设备上的卡在 cards.csv 里又写了 target_shop_code | 一致性校验,进 errors.csv |
|
||||
| 5 | 奇成 tbl_card_life 一张卡有多条 status=1 记录 | 取 expire_date 最大的那条,warnings.csv 记录 |
|
||||
| 6 | 套餐 ID 映射不全 | 脚本预扫描,缺的列出来,业务方/我们补全 yaml |
|
||||
| 4 | 设备 `current_slot` 或 `package_source_slot` 指向空槽位 | 写入 errors.csv,阻断该设备资产或套餐 SQL |
|
||||
| 5 | 同一资产存在多个 active 正式套餐 | 写入 errors.csv,阻断该资产套餐 SQL,人工裁决后修正 |
|
||||
| 6 | 套餐 ID 映射不全 | `package_resolution.csv` 记录,errors.csv 阻断对应套餐 SQL |
|
||||
| 7 | tb_iot_card.is_standalone 由触发器维护 | 脚本写入时设 true,触发器会因为后续 binding 自动转 false |
|
||||
| 8 | 代理→shop 映射不明确 | 需要业务方提供 agent_id → shop_code 映射表,作为 config/agent_shop_mapping.yaml |
|
||||
| 9 | 线上库 sequence 与测试库不同 | 用 CTE + RETURNING 模式,不硬编码 id |
|
||||
@@ -526,7 +557,7 @@ mappings:
|
||||
|
||||
| 字段 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| `order_no` | `MIG-CARD-<iccid后8位>-<时间戳>` / `MIG-DEV-<vno>-<时间戳>` | 前缀就是迁移数据的唯一识别标记 |
|
||||
| `order_no` | `MIG-<ICCID>-<priority>` | 前缀就是迁移数据的唯一识别标记 |
|
||||
| `source` | `'admin'`(复用现有) | 不新增 `migration` 取值 |
|
||||
| `buyer_type` | `'personal'` | 与 buyer_id=0 配对 |
|
||||
| `buyer_id` | `0` | 与赠送/平台自营一致 |
|
||||
|
||||
Reference in New Issue
Block a user