hi-server/scripts/MIGRATION_REPORT.md
shanshanzhong ad578883e4
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m49s
feat: 添加付费用户数据迁移脚本、报告及相关管理逻辑调整。
2026-03-14 22:37:03 -07:00

12 KiB
Raw Permalink Blame History

付费用户数据迁移报告

生成时间: 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. 付费用户定义

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. 子查询 1order WHERE status=3查找所有已完成支付的订单status=3 = 支付完成),提取 user_id
  2. 子查询 2apple_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 文件头

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

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,避免误删新库已有表
  • 后处理 sedCREATE TABLE 改为 CREATE TABLE IF NOT EXISTS
  • 手动追加 user_familyuser_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: 全量配置表数据

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: 付费用户关联数据

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: 系统日志

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: 家庭组初始化

-- 对每个付费用户执行:
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 可以有多个设备。新系统要求每个设备 = 独立用户,通过家庭组关联。

查询多设备用户的第二个设备

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

-- 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。

文件尾

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. 导入命令

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 条