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

320 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 付费用户数据迁移报告
> 生成时间: 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 条 |