All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m49s
12 KiB
12 KiB
付费用户数据迁移报告
生成时间: 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 —
order WHERE status=3:查找所有已完成支付的订单(status=3 = 支付完成),提取user_id - 子查询 2 —
apple_iap_transactions:查找所有 Apple IAP 交易记录的user_id - UNION:合并去重,满足任一条件即为付费用户
- WHERE user_id > 0:排除
user_id=0的脏数据 - 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,避免误删新库已有表- 后处理
sed将CREATE TABLE改为CREATE TABLE IF NOT EXISTS - 手动追加
user_family和user_family_membersDDL(新系统表,源库可能没有)
涉及 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());
逻辑分析:
- 遍历 482 个付费用户 ID
- 为每个用户创建 1 个家庭组(
user_family)owner_user_id= 该用户 IDmax_members = 2(默认最多 2 人)status = 1(活跃)
- 将该用户添加为家主(
user_family_members)family_id = LAST_INSERT_ID()— 引用刚插入的家庭组 IDrole = 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
)
逻辑分析:
- 识别多设备用户:
GROUP BY user_id HAVING COUNT(*) > 1→ 找到 24 个用户 - 保留第一个设备:
MIN(id)= 最早注册的设备,保留在原 user 上 - INNER JOIN user_auth_methods:通过
auth_type='device'+auth_identifier=Identifier关联设备的登录方式记录 - 输出:每个需要拆分的设备的完整信息
对每个需要拆分的设备生成 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 条 |