# 付费用户数据迁移报告 > 生成时间: 2026-03-13 > 脚本: `scripts/export_paid_users.sh` > 输出: `scripts/output/paid_users_migration.sql` (3189 行) --- ## 1. 数据总览 ### 源库统计 | 指标 | 全量 | 付费用户筛选后 | 丢弃 | |------|-----:|---------------:|-----:| | 用户 (user) | 1,864 | **482** | 1,382 (74%) | | 登录方式 (user_auth_methods) | 2,041 | **598** | 1,443 | | 设备 (user_device) | 1,415 | **496** | 919 | | 订单 (order) | — | **1,669** | — | | 已完成订单 (status=3) | 1,806 | — | — | | 订阅 (user_subscribe) | 1,588 | **526** | 1,062 | | IAP 交易 (apple_iap_transactions) | 168 | **29** | 139 | | 系统日志 (system_logs) | 20,264 | **4,830** | 15,434 | | 套餐定义 (subscribe) | 1 | **1** (全量) | — | | 支付方式 (payment) | 4 | **4** (全量) | — | | 系统配置 (system) | 53 | **53** (全量) | — | ### 新增数据(迁移脚本自动生成) | 指标 | 数量 | 说明 | |------|-----:|------| | 家庭组 (user_family) | **482** | 每个付费用户 1 个 | | 家庭成员 (user_family_members) | **482 + 24** | 482 家主 + 24 拆分设备 | | 新用户 (拆分设备) | **24** | 多设备用户第 2 个设备→独立用户 | ### 幽灵用户 | 类型 | 数量 | 处理 | |------|-----:|------| | 有订单/IAP 但 user 表不存在 | **82** | **已排除**(INNER JOIN user) | | user_id=0 的脏数据 | 若干 | **已排除**(WHERE user_id > 0) | --- ## 2. 付费用户定义 ```sql SELECT DISTINCT t.uid FROM ( SELECT user_id AS uid FROM `order` WHERE status=3 AND user_id > 0 UNION SELECT user_id AS uid FROM apple_iap_transactions WHERE user_id > 0 ) t INNER JOIN user u ON u.id = t.uid ORDER BY t.uid; ``` **逻辑分析:** 1. **子查询 1** — `order WHERE status=3`:查找所有已完成支付的订单(status=3 = 支付完成),提取 `user_id` 2. **子查询 2** — `apple_iap_transactions`:查找所有 Apple IAP 交易记录的 `user_id` 3. **UNION**:合并去重,满足**任一条件**即为付费用户 4. **WHERE user_id > 0**:排除 `user_id=0` 的脏数据 5. **INNER JOIN user**:只保留在 `user` 表中**实际存在**的用户(排除 82 个幽灵用户) **结果**:564 个候选 → 排除 82 幽灵 → **482 个有效付费用户** --- ## 3. 脚本分步 SQL 逻辑分析 ### Step 1: 查询付费用户 ID 见上方"付费用户定义"。输出为换行分隔的 ID 列表,转为逗号分隔用于后续 WHERE IN。 ### Step 2: SQL 文件头 ```sql SET NAMES utf8mb4; -- 确保中文字符正确 SET FOREIGN_KEY_CHECKS = 0; -- 禁用外键检查,允许无序插入 SET UNIQUE_CHECKS = 0; -- 禁用唯一键检查,加速批量插入 SET AUTOCOMMIT = 0; -- 开启事务模式 CREATE DATABASE IF NOT EXISTS `ppanel` ...; USE `ppanel`; ``` **目的**:创建安全的导入环境,避免外键/唯一键冲突导致中断。 ### Step 3: 导出表结构(DDL) ```bash mysqldump --no-data --skip-add-drop-table \ user user_auth_methods user_device \ order user_subscribe apple_iap_transactions \ subscribe payment system system_logs ``` **逻辑**: - `--no-data`:只导出 CREATE TABLE 语句,不含数据 - `--skip-add-drop-table`:不生成 `DROP TABLE IF EXISTS`,避免误删新库已有表 - 后处理 `sed` 将 `CREATE TABLE` 改为 `CREATE TABLE IF NOT EXISTS` - **手动追加** `user_family` 和 `user_family_members` DDL(新系统表,源库可能没有) **涉及 12 张表**: | 表 | 类型 | |----|------| | user | 用户主表 | | user_auth_methods | 登录方式(email/device/telephone) | | user_device | 设备记录 | | order | 订单 | | user_subscribe | 用户订阅 | | apple_iap_transactions | Apple IAP 交易 | | subscribe | 套餐定义(全量配置表) | | payment | 支付方式(全量配置表) | | system | 系统配置(全量配置表) | | system_logs | 系统日志 | | user_family | 家庭组(新表,手动 DDL) | | user_family_members | 家庭成员(新表,手动 DDL) | ### Step 4: 全量配置表数据 ```bash for TBL in subscribe payment system; do mysqldump --no-create-info --complete-insert --skip-extended-insert "${TBL}" done ``` **逻辑**: - `--no-create-info`:只导出 INSERT,不重复 DDL - `--complete-insert`:生成包含列名的完整 INSERT(兼容性更好) - `--skip-extended-insert`:每行一条 INSERT(便于阅读和调试) - 这三张表**不按用户过滤**,全量导出 **数据量**:subscribe 1 条 + payment 4 条 + system 53 条 = 58 条 ### Step 5: 付费用户关联数据 ```bash export_table_by_user_ids() { mysqldump --no-create-info --complete-insert \ --where="${COL} IN (${PAID_ID_LIST})" "${TBL}" } ``` 逐表使用 `--where` 子句过滤: | 表 | 过滤列 | 导出数量 | SQL 逻辑 | |----|--------|---------|---------| | `user` | `id` | 482 | `WHERE id IN (1,5,7,...)` — 只导出付费用户的用户记录 | | `user_auth_methods` | `user_id` | 598 | `WHERE user_id IN (...)` — 付费用户的所有登录方式 | | `user_device` | `user_id` | 496 | `WHERE user_id IN (...)` — 付费用户的所有设备 | | `order` | `user_id` | 1,669 | `WHERE user_id IN (...)` — 付费用户的**所有**订单(含未完成) | | `user_subscribe` | `user_id` | 526 | `WHERE user_id IN (...)` — 付费用户的订阅记录 | | `apple_iap_transactions` | `user_id` | 29 | `WHERE user_id IN (...)` — 付费用户的 IAP 交易 | **注意**:`order` 表导出的是付费用户的**全部订单**(1,669 条),不仅仅是 status=3 的。这是合理的——保留用户完整的订单历史。 ### Step 6: 系统日志 ```sql mysqldump --where="object_id IN (${PAID_ID_LIST})" system_logs ``` **逻辑**:`system_logs.object_id` 记录的是操作对象 ID,通常是 user_id。按付费用户 ID 过滤。 **注意**:`object_id` 不一定都是 user_id(不同 type 含义不同),可能多导或少导少量记录,影响不大。 **数据量**:4,830 条 ### Step 7: 家庭组初始化 ```sql -- 对每个付费用户执行: INSERT INTO user_family (owner_user_id, max_members, status, created_at, updated_at) VALUES ({user_id}, 2, 1, NOW(), NOW()); INSERT INTO user_family_members (family_id, user_id, role, status, join_source, joined_at, ...) VALUES (LAST_INSERT_ID(), {user_id}, 1, 1, 'migration', NOW(), NOW(), NOW()); ``` **逻辑分析**: 1. 遍历 482 个付费用户 ID 2. 为每个用户创建 **1 个家庭组**(`user_family`) - `owner_user_id` = 该用户 ID - `max_members = 2`(默认最多 2 人) - `status = 1`(活跃) 3. 将该用户添加为**家主**(`user_family_members`) - `family_id = LAST_INSERT_ID()` — 引用刚插入的家庭组 ID - `role = 1`(家主) - `status = 1`(活跃) - `join_source = 'migration'`(标记来源为迁移) **LAST_INSERT_ID() 链式调用**:MySQL 保证 `LAST_INSERT_ID()` 返回同一连接中最后一次 AUTO_INCREMENT 的值,在顺序执行的 SQL 中是安全的。 ### Step 8: 多设备用户拆分 **背景**:旧系统中同一 user_id 可以有多个设备。新系统要求每个设备 = 独立用户,通过家庭组关联。 **查询多设备用户的第二个设备**: ```sql SELECT ud.user_id, ud.id, ud.Identifier, ud.user_agent, ud.created_at, ua.id FROM user_device ud INNER JOIN user_auth_methods ua ON ua.user_id = ud.user_id AND ua.auth_type = 'device' AND ua.auth_identifier = ud.Identifier WHERE ud.user_id IN ( -- 找到有 >1 个设备的付费用户 SELECT user_id FROM user_device WHERE user_id IN ({paid_ids}) GROUP BY user_id HAVING COUNT(*) > 1 ) AND ud.id NOT IN ( -- 排除每个用户的第一个设备(MIN(id) = 最早注册的设备) SELECT MIN(id) FROM user_device WHERE user_id IN (...多设备用户...) GROUP BY user_id ) ``` **逻辑分析**: 1. **识别多设备用户**:`GROUP BY user_id HAVING COUNT(*) > 1` → 找到 24 个用户 2. **保留第一个设备**:`MIN(id)` = 最早注册的设备,保留在原 user 上 3. **INNER JOIN user_auth_methods**:通过 `auth_type='device'` + `auth_identifier=Identifier` 关联设备的登录方式记录 4. **输出**:每个需要拆分的设备的完整信息 **对每个需要拆分的设备生成 SQL**: ```sql -- 1. 创建新用户(无密码无邮箱的纯设备用户) INSERT INTO user (password, algo, salt, enable, is_admin, created_at, updated_at) VALUES ('', 'default', 'default', 1, 0, '{device_created}', NOW()); SET @new_user_id = LAST_INSERT_ID(); -- 2. 将设备记录转移到新用户 UPDATE user_device SET user_id = @new_user_id WHERE id = {device_id}; -- 3. 将设备的 auth_method 转移到新用户 UPDATE user_auth_methods SET user_id = @new_user_id WHERE id = {auth_method_id}; -- 4. 将新用户加入原用户的家庭组 INSERT INTO user_family_members (family_id, user_id, role, status, join_source, ...) VALUES ( (SELECT id FROM user_family WHERE owner_user_id = {owner_uid}), @new_user_id, 2, 1, 'migration_split', ... ); ``` **处理流程**: ``` 原 user(id=100, 2 个设备) ├─ device_1 (id=50, MIN) → 保留在 user 100 上(已是家主) └─ device_2 (id=51) → 创建新 user(id=NEW) → UPDATE user_device SET user_id=NEW WHERE id=51 → UPDATE user_auth_methods SET user_id=NEW WHERE id=... → INSERT user_family_members(family_id=..., user_id=NEW, role=2) ``` **结果**:24 个设备被拆分为独立用户,并加入原用户的家庭组作为 member。 ### 文件尾 ```sql SET FOREIGN_KEY_CHECKS = 1; -- 恢复外键检查 SET UNIQUE_CHECKS = 1; -- 恢复唯一键检查 COMMIT; -- 提交事务 ``` --- ## 4. 数据完整性校验点 | 校验项 | 预期值 | 说明 | |--------|--------|------| | 导入后 user 数 | 482 + 24 = **506** | 482 原始 + 24 拆分 | | user_family 数 | **482** | 每个付费用户 1 个家庭组 | | user_family_members 数 | **506** | 482 家主 + 24 成员 | | 每个 family 的成员数 | 1 或 2 | 无拆分=1,有拆分=2 | | role=1 的成员数 | **482** | 每个家庭只有 1 个家主 | | role=2 的成员数 | **24** | 拆分设备的新用户 | | user_device.user_id 无孤儿 | 全部指向存在的 user | 拆分后 device 指向新 user | | user_auth_methods.user_id 无孤儿 | 全部指向存在的 user | 拆分后 auth 指向新 user | --- ## 5. 风险与缓解 | 风险 | 级别 | 缓解措施 | |------|------|----------| | 新库已有数据,ID 冲突 | 高 | 新库应为空库;或改用 `INSERT IGNORE` | | `refer_code` 唯一键冲突 | 中 | 迁移用户保留原值,新库确保无重复 | | `LAST_INSERT_ID()` 链断裂 | 低 | SQL 文件必须**顺序执行**,不可并行 | | 设备拆分后原用户订阅归属 | 低 | 订阅保留在原 user 上,新 user 通过家庭组共享 | | `system_logs.object_id` 语义不一致 | 低 | 不同 type 的 object_id 含义不同,可能多导 | --- ## 6. 导入命令 ```bash docker exec -i <新容器> mysql -uroot -p<密码> < scripts/output/paid_users_migration.sql ``` --- ## 7. 不导出的表(已丢弃) | 表 | 原因 | |----|------| | traffic_log | 体积大,非必要 | | ads / announcement / coupon / document | 0 条或非用户数据 | | nodes / servers / server / server_group | 节点配置,不随用户迁移 | | ticket / ticket_follow | 工单数据 | | task | 0 条 | | schema_migrations | 迁移记录 | | log_message / application_versions | 0 条 | | subscribe_application | 应用配置 | | user_device_online_record | 0 条 |