All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m49s
320 lines
12 KiB
Markdown
320 lines
12 KiB
Markdown
# 付费用户数据迁移报告
|
||
|
||
> 生成时间: 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 条 |
|