diff --git a/docs/生产服务器高负载排查与优化报告.md b/docs/生产服务器高负载排查与优化报告.md
index 62d0524..ea7b406 100644
--- a/docs/生产服务器高负载排查与优化报告.md
+++ b/docs/生产服务器高负载排查与优化报告.md
@@ -1,50 +1,132 @@
-# 生产服务器高负载排查与优化落地实证报告
+# 生产服务器高负载排查与优化报告
-- **记录时间**: 2026-10-03
-- **服务器环境**: 远程生产机 `47.114.114.225`(主机别名:`order+douyu_huya`),8 核 CPU / 16GB 内存
-- **涉及容器服务**:
- - `live-hub-py-douyu-login-1` / `live-hub-py-mysql-1` (当前 live-hub-py 服务)
- - `order_site-postgres-1` / `order_site-backend-1` (工单平台 Node.js / PG)
+- **事件时间**:2026-10-03(排查 12:33 ~ 12:54,索引优化当日落地)
+- **服务器**:`47.114.114.225`(主机别名 `order+douyu_huya`)
+- **规格**:**4 vCPU / 7.1 GiB RAM / Ubuntu 24.04**(`nproc` + `free -h` 实测,见 §1)
+- **涉及容器**:
+ - 本项目:`live-hub-py-douyu-login-1`、`live-hub-py-mysql-1`
+ - 工单平台:`order_site-postgres-1`、`order_site-backend-1`
+
+> 本文合并了并行记录的两份原始文档(《排查分析报告》与《优化落地实证报告》),
+> 去掉了重复段落,并修正了互相矛盾的两处事实(服务器规格、负载基线)。
+> 两份原始文档对同一台机器的规格记录不一致(一处写 8 核/16GB),
+> 本文以带原始命令输出的 **4 vCPU / 7.1 GiB** 为准。
---
-## 1. 核心状态登记簿 (Status Registry)
+## 0. 结论速览
-| 类别 | 项目 | 现状描述 / 数据实证 | 确信度 |
-| :--- | :--- | :--- | :--- |
-| **已知 (Known)** | **Postgres 偶发 CPU 197%** | JIT 编译器在复杂聚合统计查询(`COUNT/FILTER`)时产生巨大的即时编译开销,单次耗时达 8.8s。执行 `ALTER SYSTEM SET jit = off;` 后,查询耗时直接降到 369ms(提速 24 倍)。 | 100% (已验证) |
-| **已知 (Known)** | **MySQL 任务全表 filesort 尖刺** | 客服列表轮询接口 `GET /api/douyu/tasks?handbook_scope=elite`,跨表 JOIN `accounts` 过滤后按 `id DESC LIMIT 100` 排序。导致 24 万条历史任务中符合条件的 1.1 万条全部写入临时表并产生 `Using filesort`,MySQL 周期性冲到 111.45% CPU,日志频繁报 `Out of sort memory`。 | 100% (已验证) |
-| **已知 (Known)** | **工单后台 Node.js 周期波峰** | `order_site-backend-1` 内有拼多多同步、打手抢单等后台调度(约 15~20s 周期),瞬时聚合打到 PG,形成系统的“忽高忽低”。 | 95% (日志证实) |
-| **目标 (Target)** | **彻底消除 MySQL 全表排序** | 将 `account_assigned_to` 冗余到 `douyu_tasks` 表,建立 `(account_assigned_to, handbook_scope, id)` 复合索引,消除 JOIN 与 filesort,降为单表倒序索引扫描。 | 100% (已达成) |
-| **目标 (Target)** | **零事故安全热更上线** | 完成数据库平滑迁移(24 万行数据回填与索引建立),发布 `live-hub-py` 容器,恢复业务调度与监控。 | 100% (已达成) |
-| **未知/待观察 (Unknown)** | **工单平台 Node/PG 定时聚合** | `order_site-postgres-1` 的业务慢查询(如未带索引的统计)仍存在周期性 CPU 波动(需工单系统代码级加索引优化,与 live-hub-py 相互隔离)。 | 90% |
+两个**互相独立**的根因同时压在一台 4 核机器上,任何一个单独出现都不会打穿负载:
+
+| # | 归属 | 根因 | 处理 | 状态 |
+| :--- | :--- | :--- | :--- | :--- |
+| ① | `order_site` | PostgreSQL LLVM **JIT** 对复杂聚合反复编译,单次 7.2s 全花在代码生成 | `ALTER SYSTEM SET jit = off;` 运行时止血 | **已止血,未根治**(§5) |
+| ② | `live-hub-py` | `douyu_tasks` 按归属客服筛选时跨表 JOIN + `Using temporary; Using filesort` | 迁移 `20261003_0040` 冗余 `account_assigned_to` + 复合索引 | **已根治**(单次 400~500ms → 5.7ms,**87 倍**) |
+
+**核心数字**:Postgres CPU 197.75% → 1.58%;MySQL CPU 111.45%(尖刺)→ 1.86%~11%;
+宿主机 load 17.09 → 9.52(JIT 止血后) → 7.6~8.8(索引优化后)。
---
-## 2. 优化方案设计与实施
+## 1. 现场取证:宿主机与容器负载
-### 2.1 数据库结构升级与迁移
+```bash
+# uptime
+12:33:44 up 43 days, 14:12, 3 users, load average: 17.09, 13.75, 11.38
-迁移文件:`web/backend/migrations/versions/20261003_0040_douyu_tasks_account_assigned_to.py`
-1. 为 `douyu_tasks` 添加 `account_assigned_to` 列(外键关联 `users.id`);
+# nproc & free -h
+4 vCPU
+Mem: total 7.1Gi, used 4.1Gi, free 351Mi, buff/cache 4.1Gi, available 3.0Gi
+Swap: total 4.0Gi, used 577Mi, free 3.4Gi
+
+# docker stats --no-stream
+CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
+301d6a15a032 live-hub-py-douyu-login-1 90.53% 593.4MiB / 2GiB 28.97% 2.95GB / 234MB 116MB / 22.2MB
+04111b7b40ae live-hub-py-mysql-1 49.07% 895.6MiB / 2GiB 43.73% 532MB / 11.8GB 384GB / 21.8GB
+d1831428e110 order_site-postgres-1 197.75% 1.708GiB / 2.5GiB 68.34% 2.88GB / 24.9GB 6.79GB / 9.84GB
+0fc436d47135 order_site-backend-1 39.75% 289.2MiB / 1GiB 28.24% 28GB / 14.8GB 278MB / 130MB
+```
+
+**结论**:CPU 合计超过 **376%**,4 核已饱和。`order_site-postgres-1` 占大头(~200%),
+本项目 `douyu-login` 第二(~90%),`mysql` 第三(~50%)。
+
+---
+
+## 2. 根因一:PostgreSQL JIT 编译放大(`order_site` 侧)
+
+**慢查询位置**:`/home/order_site/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts:463`
+函数 `getWorkerOrderOverview(workerId)`,结构为多段 `UNION` 的 CTE 后按状态 `GROUP BY` 聚合。
+
+**EXPLAIN ANALYZE 关键指标(Worker 7,名下 6895 单)**:
+
+```text
+JIT:
+ Functions: 63
+ Options: Inlining true, Optimization true, Expressions true, Deforming true
+ Timing: Generation 82.769 ms, Inlining 2197.205 ms, Optimization 2832.841 ms, Emission 2098.693 ms, Total 7211.508 ms
+Execution Time: 8803.367 ms
+```
+
+- **默认(JIT 开)**:8803 ms,其中 **7211 ms(82%)全耗在 LLVM 代码生成与优化**上;
+- **`SET jit = off`**:8803 ms → **1174 ms**(该接口实测 369 ms),省掉 7.2s CPU 运算。
+
+次要因素:`worker_base` 把 `visible_orders`(几千条)与 `work_orders`(10.2 万条)做 Hash Join 时
+引发 `work_orders` 的大范围 Index Scan。
+
+---
+
+## 3. 根因二:`douyu_tasks` 跨表文件排序(`live-hub-py` 侧)
+
+**慢查询**:客服列表轮询 `GET /api/douyu/tasks?handbook_scope=elite`
+
+```sql
+SELECT douyu_tasks.id AS douyu_tasks_id, ...
+FROM douyu_tasks
+INNER JOIN accounts ON accounts.id = douyu_tasks.account_id
+LEFT OUTER JOIN accounts AS accounts_1 ON accounts_1.id = douyu_tasks.account_id
+WHERE accounts.assigned_to = 13
+ AND (douyu_tasks.handbook_scope = 'elite'
+ OR douyu_tasks.handbook_scope = 'legacy' AND douyu_tasks.task_type IN (...))
+ORDER BY douyu_tasks.id DESC
+LIMIT 100;
+```
+
+**EXPLAIN 诊断**:
+
+```text
+id: 1, table: accounts, type: ref, key: ix_accounts_assigned_to, rows: 734,
+ Extra: Using index; Using temporary; Using filesort
+id: 1, table: douyu_tasks, type: ref, key: ix_douyu_tasks_account_id, rows: 19,
+ Extra: Using where
+```
+
+`accounts` 作驱动表筛出 734 个账号、关联出几万条任务,而最终要按 `douyu_tasks.id DESC` 排序 ——
+MySQL 只能把全部关联结果写进临时表做 **filesort**。24 万条任务中符合条件的 1.1 万条全量排序,
+导致 MySQL 周期性冲到 **111.45%** CPU、累计 **384GB** 磁盘读,日志频繁报 `Out of sort memory`。
+
+### 3.1 优化方案(迁移 `20261003_0040`)
+
+`web/backend/migrations/versions/20261003_0040_douyu_tasks_account_assigned_to.py`
+
+1. `douyu_tasks` 新增 `account_assigned_to`(外键关联 `users.id`);
2. 历史数据回填:
+
```sql
UPDATE douyu_tasks dt
JOIN accounts a ON a.id = dt.account_id
SET dt.account_assigned_to = a.assigned_to;
```
-3. 建立覆盖排序的高性能复合索引:
+
+3. 建立覆盖排序的复合索引:
- `ix_douyu_tasks_assigned_scope_id (account_assigned_to, handbook_scope, id)`
- `ix_douyu_tasks_account_assigned_to_id (account_assigned_to, id)`
-### 2.2 业务查询与状态同步改造
+### 3.2 配套代码改造
+
+1. **创建任务时自动注入**:`douyu_service.py` 的 `create_douyu_tasks()` 带入账号当前的 `assigned_to`;
+2. **账号改分配时级联更新**:`accounts.py` 的 `assign_account()` 同步更新该账号下任务的 `account_assigned_to`;
+3. **彻底消除跨表排序**:`douyu.py` 的 `_visible_tasks_query()` 改为纯单表条件:
-1. **创建任务时自动注入**:
- 在 `douyu_service.py` 的 `create_douyu_tasks()` 中,创建 `DouyuTask` 时自动带入当前账号的 `assigned_to`。
-2. **账号分配变更时级联更新**:
- 在 `accounts.py` 的 `assign_account()` 中,当管理员将账号移交给新客服时,同步更新该账号下任务的 `account_assigned_to`。
-3. **彻底消除跨表排序**:
- 在 `douyu.py` 的 `_visible_tasks_query()` 中,非管理员查询直接使用单表条件:
```python
# 优化前:需要 JOIN Account 表
# query = query.join(Account).filter(Account.assigned_to == current.id)
@@ -52,26 +134,68 @@
query = query.filter(DouyuTask.account_assigned_to == current.id)
```
+### 3.3 上线与实测
+
+- 生产环境共回填 **239,118** 条历史任务;`alembic_version` 推进到 `20261003_0040`;
+- `/api/health` 返回 `{"status":"ok"}`;WebSocket 正常,斗鱼批量赠送精英令 / 改名 / 改密任务并发执行无异常。
+
+| 指标 | 优化前 | 优化后(生产实测) | 改善 |
+| :--- | :--- | :--- | :--- |
+| 执行计划 | `Using temporary; Using filesort` | `Backward index scan` | **filesort 彻底消除** |
+| 索引命中 | 仅 `account_id`,跨表无法索引排序 | `ix_douyu_tasks_assigned_scope_id` (len 71) | 直接命中最优复合索引 |
+| 单次查询耗时 | 400 ~ 500 ms | **5.7 ms** | **~87 倍** |
+| `Out of sort memory` | 频繁 | **0 次** | 根治 |
+| MySQL 容器 CPU | 尖刺 **111.45%** | 平稳 **1.86% ~ 11%** | 降低 90%+ |
+| 系统平均负载 | 峰值 12.8 ~ 14.5 | 回落至 7.6 ~ 8.8 | 显著稳定 |
+
---
-## 3. 部署上线与实测效果验证
+## 4. 为什么负载「忽高忽低」:四个周期脉冲重叠
-### 3.1 迁移与部署过程
-- **镜像构建**: 多阶段构建完成,前端 dist 与后端代码更新打包。
-- **历史数据回填**: 生产环境共回填 239,118 条历史任务的所属客服数据。
-- **alembic_version**: 成功推进到 `20261003_0040`。
+对宿主机连续采样(12:50 ~ 12:54,2 秒间隔):
-### 3.2 优化前后量化对比
+```text
+12:51:51 Postgres 158% MySQL 49% Node 35% Python 11%
+12:51:55 Postgres 171% MySQL 65% Node 22% Python 72%
+12:51:59 Postgres 138% MySQL 111% Node 102% Python 36%
+12:52:04 Postgres 155% MySQL 104% Node 104% Python 56%
+12:52:09 Postgres 211% MySQL 66% Node 76% Python 4%
+12:52:33 Postgres 活跃查询归零,CPU 骤降
+```
-| 指标维度 | 优化前状态 | 优化后状态 (生产实测) | 改善幅度 |
+| 脉冲源 | 周期 | 触发动作 | 瞬间峰值 |
| :--- | :--- | :--- | :--- |
-| **查询执行计划 (EXPLAIN)** | `Using temporary; Using filesort` | `Backward index scan` | **彻底消除 filesort** |
-| **索引命中** | 仅命中 `account_id`,跨表无法索引排序 | 命中 `ix_douyu_tasks_assigned_scope_id` (len 71) | **直接命中最优复合索引** |
-| **单次查询耗时** | 400 ~ 500 ms | **5.7 ms (0.0057s)** | **提速 ~87 倍** |
-| **MySQL 排序内存报错** | 频繁报 `Out of sort memory` | **0 次报错**,完全脱离 sort buffer | **彻底根治** |
-| **MySQL 容器瞬时 CPU** | 周期性拉升至 **111.45%** | 平稳维持在 **1.86% ~ 11%** | **降低 90%+** |
-| **系统平均负载 (load avg)** | 峰值 12.8 ~ 14.5 | 逐步平稳回落至 7.6 ~ 8.8 | **系统稳定性显著增强** |
+| ① MySQL 任务全表排序 | ~3~5 s | 客服工作台轮询 `/api/douyu/tasks`,24 万条里做 filesort | 4% → **111%** |
+| ② PostgreSQL 聚合风暴 | ~5~15 s | `getWorkOrderStatistics`(10 万工单 × 28 万订单 LEFT JOIN 全量 SUM)、`countWorkerAcceptedOrders`、`getWorkerOrderOverview` | 1.5% → **211%** |
+| ③ Node 定时同步与派单 | 20 s | `pdd_order_sync` 拉单派发;每 60 s 超时检测与押金解冻 | 22% → **104%** |
+| ④ Python 充值/绑定波峰 | 任务驱动 | 供应商直充下单、5 秒一轮的绑定状态轮询、代理池并发探测 | 4% → **73%** |
-### 3.3 生产线上业务运转状态
-- `/api/health` 检查:`{"status":"ok"}`
-- WebSocket 连接:客服工作台正常连接,斗鱼批量赠送精英令、改名、改密任务均在正常并发执行,状态流转顺畅。
+**结论**:关掉 JIT 后 Postgres 不再陷入 10~20 秒的持续假死(这是 load 从 17 降下来的原因);
+但四个周期任务缺少防抖 / 短缓存 / 关键索引,一旦某一秒同时触发,4 核仍会出现 1~2 秒的 100% 尖刺 ——
+这就是「忽高忽低」的机理。① 已在 §3 根治,②③ 见 §5。
+
+---
+
+## 5. 遗留与后续
+
+| # | 事项 | 归属 | 说明 |
+| :--- | :--- | :--- | :--- |
+| 1 | `getWorkerOrderOverview` 的 CTE 重构 | `order_site` | `UNION` → `UNION ALL`,减少大集合 Hash 去重;合并 `worker_order_cancel_requests` / `worker_order_feedbacks` 的 EXISTS 检查 |
+| 2 | 打手端概览加短缓存 | `order_site` | 5~10 秒的内存缓存(LRU / Redis / Node Map),避免切页与轮询直穿数据库 |
+| 3 | **`jit = off` 只是止血** | `order_site` | 后续复测显示该容器 CPU **又回到 100%~222%**(见 [迁移方案](迁移方案-live-hub-py独立部署.md) §9)。SQL 重构与缓存不做,问题会复发 |
+| 4 | 单机混部缺硬隔离 | 运维 | 4 核上跑 2 个数据库(PG 16 + MySQL 8.4)与 2 套高并发业务。应在 `docker-compose.yml` 为各服务配 `cpus` 限额,防止单个慢查询占满宿主机 |
+
+> 「严禁在生产上手工热修业务代码」的推进原则见 [迁移方案-live-hub-py独立部署.md](迁移方案-live-hub-py独立部署.md):
+> 运行时参数属于紧急止血,源码改动一律走分支 → 本地验证 → 正规部署。
+
+---
+
+## 6. 时间线
+
+| 时刻 (CST) | 事件 |
+| :--- | :--- |
+| 12:33 | 采样确认 4 核饱和、load 17.09;定位 PG JIT 与 MySQL filesort |
+| 12:42 | 执行 `ALTER SYSTEM SET jit = off; SELECT pg_reload_conf();` |
+| 12:43 | Postgres 197.75% → 1.58%,MySQL 49% → 4.46%,load 17.09 → 9.52 |
+| 12:50 ~ 12:54 | 连续采样,拆出四个脉冲周期(§4) |
+| 当日 | 落地迁移 `20261003_0040`(回填 23.9 万行 + 复合索引)并发布,单次查询 87 倍提速 |
diff --git a/docs/生产服务器高负载排查分析报告.md b/docs/生产服务器高负载排查分析报告.md
deleted file mode 100644
index 0d65229..0000000
--- a/docs/生产服务器高负载排查分析报告.md
+++ /dev/null
@@ -1,210 +0,0 @@
-# 生产服务器高负载排查与性能分析报告
-
-> 采集时间:2026-10-03 12:35 ~ 12:38 CST
-> 目标服务器:`order+douyu_huya` (47.114.114.225)
-> 规格:4 vCPU / 7.1 GiB RAM / Ubuntu 24.04 (Linux WIN10 7.0.0-22-generic)
-
----
-
-## 1. 调查状态卡片(已知 / 未知 / 目标 / 确信度)
-
-| 维度 | 内容说明 | 确信度 |
-| :--- | :--- | :---: |
-| **已知 (Known)** | 1. 宿主机平均负载高达 **17.09, 13.75, 11.38**,4 核 CPU 几乎全部跑满(376%)。
2. 宿主机承载了两个业务体系:**接单平台 (`order_site`)** 和 **直播自动任务平台 (`live-hub-py`)**。
3. `order_site-postgres-1` 占用 CPU 高达 **197.75%**(吃掉 2 个核);`live-hub-py-douyu-login-1` 占 **90.53%**(吃掉近 1 个核);`live-hub-py-mysql-1` 占 **49.07%**;`order_site-backend-1` 占 **39.75%**。
4. Postgres 的 CPU 飙升主要是打手端工单统计查询 `getWorkerOrderOverview` 与列表查询,触发了 **PostgreSQL LLVM JIT 动态编译**,单次查询中 JIT 耗时达 **7.21 秒**(总耗时 8.8 秒)。
5. MySQL 的高 CPU 和 384GB 读 IO 是因为 `douyu_tasks` (23.8万条) 与 `accounts` 联表带 `assigned_to` 过滤时,执行计划使用了 **`Using temporary; Using filesort`**,高频轮询产生大量临时表全排序。
6. `live-hub-py-douyu-login-1` 当前正在持续、高并发执行斗鱼任务(供应商直充、赠送精英令、代理池多并发探测)。 | **100%(已由真实命令、日志与 EXPLAIN 实证)** |
-| **未知 (Unknown)** | 1. 打手端前端当前轮询的频率(是否为 2~3 秒一次硬轮询)。
2. 是否可以在 Postgres 配置中全局关闭 JIT (`jit = off`),或者仅在业务连接池上设置。
3. `douyu_tasks` 的管理后台当前是哪个页面在轮询 `assigned_to = 13` 的任务列表。
4. live-hub-py 的当前斗鱼任务并发限制是否允许动态限速。 | **待确认** |
-| **目标 (Objective)** | 1. **根治 Postgres 200% CPU 占用**:解决打手端统计与列表慢查询(禁用 JIT、优化 CTE/索引、增加缓存或降低轮询频率)。
2. **降低 MySQL 50% CPU 与 384GB IO 占用**:优化 `douyu_tasks` 的 `assigned_to` 联表排序查询,消除 `Using temporary; Using filesort`。
3. **平抑宿主机 Load Average 至 3.0 以下**,确保接单平台与直播充值任务稳定运行。 | **明确** |
-
----
-
-## 2. 证据分类记录 (Evidence & Forensics)
-
-### 证据 A:宿主机系统资源与 Docker 容器负载
-
-**执行证据:**
-```bash
-# uptime
-12:33:44 up 43 days, 14:12, 3 users, load average: 17.09, 13.75, 11.38
-
-# nproc & free -h
-4 vCPU
-Mem: total 7.1Gi, used 4.1Gi, free 351Mi, buff/cache 4.1Gi, available 3.0Gi
-Swap: total 4.0Gi, used 577Mi, free 3.4Gi
-
-# docker stats --no-stream
-CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O
-301d6a15a032 live-hub-py-douyu-login-1 90.53% 593.4MiB / 2GiB 28.97% 2.95GB / 234MB 116MB / 22.2MB
-04111b7b40ae live-hub-py-mysql-1 49.07% 895.6MiB / 2GiB 43.73% 532MB / 11.8GB 384GB / 21.8GB
-d1831428e110 order_site-postgres-1 197.75% 1.708GiB / 2.5GiB 68.34% 2.88GB / 24.9GB 6.79GB / 9.84GB
-0fc436d47135 order_site-backend-1 39.75% 289.2MiB / 1GiB 28.24% 28GB / 14.8GB 278MB / 130MB
-```
-
-**分析结论:**
-总 CPU 占用合计超过 **376%**(4 核饱和运行)。其中 `order_site-postgres-1` 占大头(~200%),`live-hub-py` 容器占第二(~90%),`mysql` 占第三(~50%)。
-
----
-
-### 证据 B:Postgres 慢查询与 JIT 编译瓶颈
-
-**慢查询代码位置:**
-`/home/order_site/apps/backend/src/repositories/worker-platform/work-order-query-repo.ts:463`
-函数:`getWorkerOrderOverview(workerId)`
-
-**核心 SQL:**
-```sql
-WITH visible_orders AS (
- SELECT wo.id AS work_order_id FROM work_orders wo WHERE wo.assigned_worker_id = $1
- UNION
- SELECT wo.id AS work_order_id FROM work_orders wo WHERE wo.last_assigned_worker_id = $1
- AND (wo.status <> 'open' OR EXISTS (
- SELECT 1 FROM work_order_events timeout_event
- WHERE timeout_event.work_order_id = wo.id AND timeout_event.event_type = 'timeout_reopen'
- ))
- UNION
- SELECT wos.work_order_id FROM work_order_shares wos WHERE wos.worker_id = $1 AND wos.status != 'cancelled'
-),
-worker_base AS ( ... ),
-worker_orders AS ( ... )
-SELECT status, COUNT(*)::int AS order_count, ... FROM worker_orders GROUP BY status;
-```
-
-**EXPLAIN ANALYZE 关键指标(Worker 7,拥有 6895 单):**
-```text
-JIT:
- Functions: 63
- Options: Inlining true, Optimization true, Expressions true, Deforming true
- Timing: Generation 82.769 ms, Inlining 2197.205 ms, Optimization 2832.841 ms, Emission 2098.693 ms, Total 7211.508 ms
-Execution Time: 8803.367 ms
-```
-- **开启 JIT(默认)**:耗时 **8803 ms**,其中 **7211 ms (82%)** 全耗在 LLVM JIT 动态代码生成与优化上,直接霸占 CPU 算力!
-- **关闭 JIT(SET jit = off)**:耗时从 **8803 ms 降为 1174 ms**,瞬间省掉 7.2 秒 CPU 运算!
-
----
-
-### 证据 C:MySQL 联表临时表与文件排序瓶颈
-
-**慢查询定位:**
-```sql
-SELECT douyu_tasks.id AS douyu_tasks_id, ...
-FROM douyu_tasks
-INNER JOIN accounts ON accounts.id = douyu_tasks.account_id
-LEFT OUTER JOIN accounts AS accounts_1 ON accounts_1.id = douyu_tasks.account_id
-WHERE accounts.assigned_to = 13
- AND (douyu_tasks.handbook_scope = 'elite' OR douyu_tasks.handbook_scope = 'legacy' AND douyu_tasks.task_type IN (...))
-ORDER BY douyu_tasks.id DESC
-LIMIT 100;
-```
-
-**EXPLAIN 诊断结果:**
-```text
-id: 1, table: accounts, type: ref, key: ix_accounts_assigned_to, rows: 734,
- Extra: Using index; Using temporary; Using filesort
-id: 1, table: douyu_tasks, type: ref, key: ix_douyu_tasks_account_id, rows: 19,
- Extra: Using where
-```
-- **诊断**:`accounts` 是驱动表,筛选 `assigned_to = 13` 的 734 个账号,关联出几万条 `douyu_tasks`。由于最终是按 `douyu_tasks.id DESC` 排序,MySQL 必须在临时表中对所有关联记录做 **`filesort` 文件全排序**。
-- 这导致了 MySQL 容器高达 **384GB** 的累计磁盘读取,以及 **49%** 的持续 CPU 消耗。
-
----
-
-### 证据 D:live-hub-py 运行中的真实业务负载
-
-**容器日志证据:**
-`live-hub-py-douyu-login-1` 正在同时运行多路斗鱼充值与精英令业务:
-- `[douyu] 供应商直充 | 下单 | 外部单号=...`
-- `[douyu] 赠送精英令成功: 770 ...`
-- `[douyu] 代理预检 1/3: 正在获取代理 -> 验证 -> 获取新代理`
-- 大量 WebSocket 客户端 `/api/douyu/ws/...` 连接,属于高并发高吞吐的正常业务处理流,但与 Postgres/MySQL 的慢查询争抢 CPU,加剧了系统卡顿。
-
----
-
-## 3. 核心根因归纳 (Root Causes)
-
-1. **PostgreSQL JIT 编译放大器(最主要的 CPU 杀手)**:
- PostgreSQL 对高 Cost 查询默认启用 JIT,而打手端概览/列表 SQL 结构极其复杂,每次命中都触发 63 个 LLVM 函数编译,耗时 7.2 秒,多打手并发轮询时直接把 4 核打穿。
-2. **PostgreSQL CTE 关联全索引扫描**:
- 在 `worker_base` 中将 `visible_orders`(几千条)与 `work_orders`(10.2万条)Hash Join 时,引发了 `work_orders` 10 万行的大范围 Index Scan。
-3. **MySQL 任务查询缺少最适索引与高效分页**:
- 按归属人 (`assigned_to`) 筛选并按任务 ID 倒排的接口缺少高效索引覆盖,每次调用都产生临时表和文件排序。
-4. **单机混部资源无硬隔离**:
- 4 核服务器同时跑了 2 个数据库(Postgres 16 + MySQL 8.4)和 2 套高并发业务(Node.js + Python Uvicorn),当任意一个查询发生慢查重试时,CPU 瞬间雪崩。
-
----
-
-## 4. 优化与降压建议方案 (Action Plan)
-
-### 第一阶段:立刻生效的立竿见影措施(1-2分钟内降低 50%~70% CPU)
-
-1. **关闭 PostgreSQL 的 JIT**:
- 在 PostgreSQL 配置或 Node 连接池初始化时执行 `ALTER SYSTEM SET jit = off; SELECT pg_reload_conf();`。
- - **预期效果**:该接口单次 CPU 开销直接下降 **80%**,Postgres CPU 占用将显著回落。
-2. **增加打手端概览查询缓存**:
- 打手端 `getWorkerOrderOverview` 增加 5~10 秒的内存缓存(LRU / Redis / Node 内存 Map),避免同一打手每次切换页面或轮询都直穿数据库。
-
-### 第二阶段:SQL 与索引专项优化
-
-1. **MySQL `douyu_tasks` 联合查询优化**:
- 针对 `douyu_tasks.account_id` 与 `accounts.assigned_to`,评估是否在 `douyu_tasks` 上冗余 `assigned_to` 字段或建立更优的联表分页索引,避免全量 filesort。
-2. **PostgreSQL `work-order-query-repo.ts` 结构重构**:
- 优化 CTE 逻辑,将 `UNION` 改为更精准的 `UNION ALL`,避免大集合 Hash 去重;对于 `worker_order_cancel_requests` 和 `worker_order_feedbacks` 的 EXISTS 检查进行合并或物化过滤。
-
-### 第三阶段:资源配额防护
-
-在 `docker-compose.yml` 中为各服务合理配置 `cpus` 限制,防止任一服务突发异常慢查询占满全部宿主机核心。
-
----
-
-## 5. 运维操作与代码分支推进路线 (Deployment & Git Flow)
-
-按照标准工程规范,严禁在生产服务器上随意手工热修业务代码,杜绝“代码漂移 (Drift)”和更新覆盖风险。
-
-### 推进原则:
-1. **数据库运行时参数紧急止血**:
- - 生产环境执行 `ALTER SYSTEM SET jit = off; SELECT pg_reload_conf();`(动态热生效,不涉及源码变更,解脱当前 200% JIT CPU 占用)。
-2. **工作区分流与保护**:
- - 将当前 `live-hub-py` 本地未提交的改动暂存或切换至 `dev` 分支,保持 `main` 分支干净。
-3. **本地开发与正规发布**:
- - **`order_site` 项目**(本地路径:`/Users/yml/codes/order_site`):在本地优化 `work-order-query-repo.ts` 的 CTE 与打手端概览缓存,本地验证通过后提交至对应 Git 分支,通过正规部署更新生产。
- - **`live-hub-py` 项目**(本地路径:`/Users/yml/codes/live-hub-py`):在 `dev` 分支优化 `douyu_tasks` 的 `assigned_to` 联表索引与查询,测试通过后合并部署。
-
----
-
-## 6. 紧急止血效果实测证据 (Post-Fix Verification)
-
-执行时间:2026-10-03 12:42:04 CST
-执行命令:`ALTER SYSTEM SET jit = off; SELECT pg_reload_conf();`
-
-### 止血前后实时指标对比表:
-
-| 监控项 | 优化前 (12:34) | 优化后 (12:43) | 变化幅度 | 状态 |
-| :--- | :---: | :---: | :---: | :---: |
-| **PostgreSQL 容器 CPU** | **197.75%** | **1.58%** | **暴降 99.2%** | 彻底恢复健康 |
-| **MySQL 容器 CPU** | **49.07%** | **4.46%** | **暴降 90.9%** | 大幅改善 |
-| **宿主机 1 分钟平均负载** | **17.09** | **9.52 (持续下探中)** | **下降 44.3%** | 迅速脱险 |
-| **打手概览单次查询耗时** | **8803 ms** | **369 ms** | **提速 23 倍** | 质的飞跃 |
-
----
-
-## 7. 忽高忽低脉冲式负载成因与周期对齐分析 (Pulse & Spike Analysis)
-
-通过对宿主机与各容器进行连续多点动态采样(12:50 ~ 12:54),证实了服务器负载**“忽高忽低(从 1.5% 瞬间飙至 200%+ 后又回落)”**的根本机理:**四大不同周期的脉冲任务重叠交汇**。
-
-### 连续采样实证(2秒间隔):
-- `12:51:51`:Postgres 158%, MySQL 49%, Node 35%, Python 11%
-- `12:51:55`:Postgres 171%, MySQL 65%, Node 22%, Python 72%
-- `12:51:59`:Postgres 138%, MySQL 111%, Node 102%, Python 36%
-- `12:52:04`:Postgres 155%, MySQL 104%, Node 104%, Python 56%
-- `12:52:09`:Postgres 211%, MySQL 66%, Node 76%, Python 4%
-- `12:52:33`:Postgres 瞬间恢复为 0 个活跃查询,CPU 骤降
-
-### 四大脉冲周期拆解:
-
-| 脉冲源 | 周期 | 核心动作 / 触发条件 | 瞬间 CPU 峰值 |
-| :--- | :---: | :--- | :---: |
-| **① MySQL 任务全表排序** | **~3~5 秒** | 客服人员打开任务工作台,前端轮询 `/api/douyu/tasks`;因按 `assigned_to` 过滤且 `ORDER BY id DESC`,MySQL 在 24 万条任务中做 `Using temporary; Using filesort`。 | **4% -> 111%** |
-| **② PostgreSQL 复合聚合风暴** | **~5~15 秒** | 虽已关掉 JIT(消除 7.2s 编译死锁),但三大重型聚合被频繁触发:
1. 管理端全局统计 `getWorkOrderStatistics`(10万工单与28万订单 LEFT JOIN 做全量 SUM);
2. 打手端完成单统计 `countWorkerAcceptedOrders`(COUNT DISTINCT);
3. 打手概览 `getWorkerOrderOverview`。
每次命中启动多 Parallel Workers 瞬间吃满 150%~210% CPU,执行完后立即归零。 | **1.5% -> 211%** |
-| **③ Node 定时同步与派单** | **20 秒整** | `scheduler-service` 每 20 秒执行一次 `pdd_order_sync`(调用拼多多防封算法拉单并派发消费者分流),每 60 秒执行超时检测与押金解冻。 | **22% -> 104%** |
-| **④ Python 充值与绑定波峰** | **任务驱动** | 斗鱼供应商充值下单、每隔 5 秒一次的 13 轮绑定状态轮询、代理池高并发探测。 | **4% -> 73%** |
-
-### 核心结论:
-优化 JIT 后,PostgreSQL **不再陷入 10~20 秒的持续假死**(这是平均 Load 从 17 降下来的原因);但由于上述 4 个周期任务缺少**防抖/短缓存**和**关键索引**,当它们在某一秒**同时触发**时,4 核 CPU 依然会瞬间出现 1~2 秒的 100% 尖刺(Spike),这就是用户感知到“忽高忽低”的根源。
-
diff --git a/docs/自动化与资料收集规划.md b/docs/自动化与资料收集规划.md
index 98d90a1..cbdd6dc 100644
--- a/docs/自动化与资料收集规划.md
+++ b/docs/自动化与资料收集规划.md
@@ -5,7 +5,7 @@
> ### 🔜 续作入口(下一个会话先看这里)
>
-> 1. **当前该做 P2(资料收集链接 + OSS 上传)**,施工顺序见 **§C.5**,下一个迁移编号 **`20261002_0040`**;
+> 1. **当前该做 P2(资料收集链接 + OSS 上传)**,施工顺序见 **§C.5**,下一个迁移编号 **`20261003_0042`**;
> 2. P0 的适配层已就绪(`services/external_order_source.py`,`lookup_order()` 直接可用),
> P2 做「用户填订单号后回显商品」时**直接调它**,不要重写跨库 SQL;
> 3. P0 遗留三件事(要真正启用时才做):生产跑迁移 0039 建表、`.env` 开 `ORDER_SITE_LOOKUP_ENABLED`、补查询接口/页面(见 §C.4-bis);
@@ -1307,14 +1307,16 @@ OSS_PREFIX=collect
### C.1 迁移编号(按落地顺序取,**不预留**)
-现状 head = `20261001_0036`。**谁先落地谁取下一个号**,不要按阶段预分配。
+现状 head = `20261003_0041`(账号事件,在 `dev`;`main` 上是 `20261003_0040`)。**谁先落地谁取下一个号**,不要按阶段预分配。
| 阶段 | revision | 说明 |
| --- | --- | --- |
| P1a(已落地) | `20261002_0037` | `automation_jobs` / `automation_runs` / `login_tasks` +3 / `accounts.auto_managed` |
| P1b(已落地) | `20261002_0038` | `login_tasks` + `relogin_failure_kind` |
| P0(已落地) | `20261002_0039` | `external_order_cache` |
-| P2 / P3 / P4 | 各自落地时依次取号(**下一个是 `20261002_0040`**) | 见附录 A 的表清单 |
+| 生产热修(已落地) | `20261003_0040` | `douyu_tasks.account_assigned_to` + 复合索引(消除任务列表 filesort) |
+| 账号事件(已落地,在 `dev`) | `20261003_0041` | `account_events` + `audit_logs.account_id`([设计](设计-账号详情页与事件历史.md)) |
+| P2 / P3 / P4 | 各自落地时依次取号(**下一个是 `20261003_0042`**) | 见附录 A 的表清单 |
> **为什么不能预留编号**:Alembic 的 `down_revision` 必须形成单链。若先给 P1 占 0038、
> 把 0037 留给 P0,等 P0 真落地时它的 `down_revision` 只能指向 0038(否则与 0038 平级,
diff --git a/docs/设计-登录任务与CK解耦.md b/docs/设计-登录任务与CK解耦.md
index 5e613db..c9f156c 100644
--- a/docs/设计-登录任务与CK解耦.md
+++ b/docs/设计-登录任务与CK解耦.md
@@ -68,7 +68,8 @@ relogin_failure_kind: Mapped[str] = mapped_column(String(16), default="", nullab
## 4. 迁移设计
-编号:下一个是 **`20261002_0040`**(按落地顺序取,见规划 §C.1)。
+编号:下一个是 **`20261003_0042`**(按落地顺序取,见规划 §C.1)。
+(`20261003_0040` 已被生产索引优化占用、`20261003_0041` 已被账号事件表占用 —— 这正是「不预留编号」的实例。)
### 4.1 DDL(幂等,照 `20261002_0039` 写法:先 `sa.inspect` 查存在)
@@ -264,7 +265,7 @@ db.query(Account)
### 6.2 一次切换的具体做法
-1. **迁移 `20261002_0040`**:加列 + 回填 + **自校验**(§4)+ 保留 `login_tasks` 旧列(不动);
+1. **迁移 `20261003_0042`**:加列 + 回填 + **自校验**(§4)+ 保留 `login_tasks` 旧列(不动);
2. **代码一次改到位**:读(§5.1)与写(§5.2)**全部**切到 `accounts`,
自动化改为按账号调度(§5.3),调整删除护栏(§5.5);
3. **旧列留在库里但不映射进模型**(或映射但不再读写)—— 零额外工作,纯属留后路;
@@ -406,7 +407,7 @@ if task is None:
| # | 提交信息 | 改动文件 | 干什么 | 怎么验证 |
| --- | --- | --- | --- | --- |
-| **C1** | `feat(db): accounts 增加 CK 与状态列,并从 login_tasks 回填` | `migrations/versions/20261002_0040_account_ck_state.py`(新)
`models.py`
`tests/test_migrations.py` | 加 8 列 + `(account_id, attempt_status)` 索引;分块回填;**自校验不过就抛错** | `pytest tests/test_migrations.py`;本地 dev 库实跑;回填后 `accounts.cookie<>''` == 记录级去重数 |
+| **C1** | `feat(db): accounts 增加 CK 与状态列,并从 login_tasks 回填` | `migrations/versions/20261003_0042_account_ck_state.py`(新)
`models.py`
`tests/test_migrations.py` | 加 8 列 + `(account_id, attempt_status)` 索引;分块回填;**自校验不过就抛错** | `pytest tests/test_migrations.py`;本地 dev 库实跑;回填后 `accounts.cookie<>''` == 记录级去重数 |
| **C2** | `refactor(cookie): 斗鱼 CK 本体改由账号承载` | `services/douyu_service.py`(`cookie_holding_filter` / `cookie_operable_filter` / `latest_cookie` / `latest_cookie_login_task`)
`services/login_service.py`(`_write_login_success`、清空、批次行复用)
`routers/cookies.py`(清除/删除 CK 的**写点**)
`services/douyu_runner.py` | **读+写一起切**到 `accounts.cookie`。批次行复用逻辑同步重写(§6.5 连带点,否则静默退化) | 新增 `tests/test_account_ck_storage.py`;改造 `test_login_attempt_isolation.py` |
| **C3** | `refactor(automation): CK 健康状态与重登状态迁到账号,作业按账号调度` | `services/automation/jobs/ck_health.py`
`services/automation/jobs/ck_relogin.py`
`routers/automation.py`
`routers/cookies.py`(检测写点)
`services/login_service.py`(重登后复检写点)
`services/douyu_runner.py` | `ck_check_*` / `relogin_*` 的**读+写+选行**一起切到 `Account`;`automation_runs.detail` 里的 task_id 改 account_id | 改造 `test_automation_ck_health.py` / `test_automation_ck_relogin.py`;新增「**没有历史行的账号也能被巡检/重登**」 |
| **C4** | `refactor(cookies): Cookie 页改为账号级` | `routers/cookies.py`
`routers/accounts.py`
`routers/douyu.py`
`routers/dashboard.py`
`web/frontend/src/pages/CookiePage.tsx`
`api/cookies.ts` / `api/types.ts` | 列表/汇总/重复/导出/检测/删除全部改为以**账号**为单位;`DELETE /cookies/{id}` 入参由 task_id 变 account_id | **风险最高**:前后端同一提交;专门用例覆盖「删除清的是入参那个账号」;上线后手工验证一次 |
diff --git a/docs/设计-账号详情页与事件历史.md b/docs/设计-账号详情页与事件历史.md
index fe0049c..93eb1a7 100644
--- a/docs/设计-账号详情页与事件历史.md
+++ b/docs/设计-账号详情页与事件历史.md
@@ -1,6 +1,8 @@
# 设计:账号详情页与账号事件历史
-> 状态:**后端已完成、前端已完成,本地验证通过**(2026-10-03,未提交)
+> 状态:**后端已完成、前端已完成,本地验证通过**(2026-10-03)
+> 已提交在 `dev`(`834bfdd`);迁移编号经合并后为 **`20261003_0041`**(原 `20261002_0040`,
+> 因生产索引优化先落地占了 `20261003_0040`)。**尚未合入 `main`、未上生产。**
> 关联:[登录任务与 CK 解耦](设计-登录任务与CK解耦.md) —— 两者的写入点高度重合,建议同一工作流推进(见 §8)。
>
> **范围(2026-10-03 已定)**:**不做历史回填**。既有数据(`douyu_tasks` 里的兑换/充值/积分记录)
@@ -297,11 +299,11 @@ class AccountEvent(Base):
## 11. 落地记录(2026-10-03)
-已按本文实现,**未提交、未碰生产**(用户要求先在本地验证)。
+已按本文实现,**已提交在 `dev`、未碰生产**(用户要求先本地验证)。迁移编号见 §11.4。
| 提交建议 | 内容 | 落点 |
| --- | --- | --- |
-| E1 | 迁移 `20261002_0040`:`account_events` + `audit_logs.account_id` | `migrations/versions/20261002_0040_account_events.py`、`models.py`(`AccountEvent`)、`tests/test_migrations.py` |
+| E1 | 迁移 `20261003_0041`:`account_events` + `audit_logs.account_id` | `migrations/versions/20261003_0041_account_events.py`、`models.py`(`AccountEvent`)、`tests/test_migrations.py` |
| E2 | 事件服务 `emit()` + 任务映射 | `services/account_events_service.py` |
| E3 | 发射点:CK 失效 / 登录·重登成功 / 重登转人工 / 精英宝典·电竞手册开通 | `jobs/ck_health.py`、`services/login_service.py`、`jobs/ck_relogin.py`、`services/douyu_runner_manual.py` |
| E4 | 收集器作业(水位线在 `automation_jobs.config`) | `jobs/account_events_collect.py`,注册为「账号事件收集」 |
@@ -344,3 +346,23 @@ class AccountEvent(Base):
> 本地验证时把收集器**临时启用**并手动灌了 3 条测试任务(`batch_id='local-evt'`,仅本地开发库)。
> 生产上该作业仍是 `enabled=false`,需在「自动巡检」页手动打开;首次运行只定水位线、不回溯历史。
+
+### 11.4 迁移编号改号:`20261002_0040` → `20261003_0041`
+
+本功能最初取的号是 `20261002_0040`,`down_revision` 指向 `20261002_0039`。开发期间生产上并发
+落地了索引优化,它同样从 `20261002_0039` 起链、占了 `20261003_0040`。两边一旦合并,
+**两个迁移平级 = 两个 alembic head**,`alembic upgrade head` 会直接报
+`Multiple head revisions are present`,`tests/test_migrations.py` 的单头断言也会失败。
+
+处理(2026-10-03):
+
+1. 文件名改为 `20261003_0041_account_events.py`,`revision` 改 `20261003_0041`,
+ `down_revision` 改指 `20261003_0040`,接在索引优化之后;
+2. `tests/test_migrations.py` 的 `HEAD_REVISION` 同步改 `20261003_0041`;
+3. **改号是安全的**,因为 `20261002_0040` 从未被生产库应用过(`dev` 未部署)。
+ 本机开发库 `douyu_login` 的 `alembic_version` 已是 `20261003_0040`,且 `account_events`
+ 表已存在 —— 迁移写成幂等的(先 `sa.inspect` 查存在再建),所以升级到 `0041` 只会空跑一次
+ 并把版本号推进,不会重复建表。
+
+> ⚠️ 教训与规划 §C.1 一致:**迁移编号一旦被任何库应用过就不能再改名**。
+> 这次能改,纯粹是因为它还没进生产。
diff --git a/docs/迁移方案-live-hub-py独立部署.md b/docs/迁移方案-live-hub-py独立部署.md
index cde1e90..445ba1f 100644
--- a/docs/迁移方案-live-hub-py独立部署.md
+++ b/docs/迁移方案-live-hub-py独立部署.md
@@ -77,7 +77,7 @@ live-hub-py-mysql-1 BLOCK I/O 396GB / 23.1GB # 累计读 396GB
### 1.5 历史结论的沿用与修正
-`docs/生产服务器高负载排查分析报告.md` 里「PostgreSQL JIT 导致 197% CPU」的判断成立,12:42 执行 `jit = off` 后 Postgres 从 197.75% 掉到 1.58%。**但本次复测显示它又回到了 100%~222%** —— 说明 `jit = off` 只是止血,`getWorkerOrderOverview` 的 CTE + 并行 worker 才是根,仍需按原报告做 SQL 重构(§9)。
+`docs/生产服务器高负载排查与优化报告.md` 里「PostgreSQL JIT 导致 197% CPU」的判断成立,12:42 执行 `jit = off` 后 Postgres 从 197.75% 掉到 1.58%。**但本次复测显示它又回到了 100%~222%** —— 说明 `jit = off` 只是止血,`getWorkerOrderOverview` 的 CTE + 并行 worker 才是根,仍需按原报告做 SQL 重构(§9)。
---
@@ -250,7 +250,7 @@ docker compose up -d && curl -fsS http://127.0.0.1:8000/api/health
- `order_site-postgres-1` **单独**峰值就能到 **222.8%**,加 backend 145.9% ≈ **3.2 核**。迁走 livehub 后,order_site 独占 4 核仍然会在高峰期打满。
- 所以迁 livehub 是**必要条件、不是充分条件**。后续二选一:
- - ① 按 `docs/生产服务器高负载排查分析报告.md` 做 `getWorkerOrderOverview` 的 CTE 重构(`UNION` → `UNION ALL`)+ 打手端概览缓存,消除并行 worker 突刺;
+ - ① 按 `docs/生产服务器高负载排查与优化报告.md` 做 `getWorkerOrderOverview` 的 CTE 重构(`UNION` → `UNION ALL`)+ 打手端概览缓存,消除并行 worker 突刺;
- ② 或把 order_site 留在 4 核机上但**升配到 8 核**。
- 建议顺序:**先迁 livehub(半天)→ 观察一周 → 再决定 order_site 是优化还是升配/搬迁**。这样每次只动一个变量,出问题好定位。
diff --git a/web/backend/migrations/versions/20261003_0041_account_events.py b/web/backend/migrations/versions/20261003_0041_account_events.py
index 22319b0..fc419fe 100644
--- a/web/backend/migrations/versions/20261003_0041_account_events.py
+++ b/web/backend/migrations/versions/20261003_0041_account_events.py
@@ -16,6 +16,16 @@
实测 `audit_logs.target` 是自由文本(`冰柳192` / `导入103个` / `douyu_batch:xxx` 混着),
无法可靠关联到账号,导致「谁什么时候分配/改标签/删除了这个账号」进不了账号时间线。
加一列可空的 account_id,账号相关操作写入时带上即可(旧记录保持 NULL)。
+
+## 编号为什么是 0041 而不是 0040
+
+本迁移最初取号 `20261002_0040`(`down_revision` = `20261002_0039`)。开发期间生产上并发落地了
+索引优化 `20261003_0040`,它同样从 `20261002_0039` 起链 —— 两边一合并就是**两个 alembic head**,
+`upgrade head` 会直接报 `Multiple head revisions are present`。
+
+因为本迁移**从未被生产库应用过**(`dev` 未部署),改号是安全的:接在 `20261003_0040` 之后成为
+`20261003_0041`。升级脚本写成幂等的(`sa.inspect` 先查存在),所以对已手工建过表的本地库
+重跑也只会空过并把版本号推进,不会重复建表。
"""
from collections.abc import Sequence
@@ -23,8 +33,8 @@ from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
-revision: str = "20261002_0040"
-down_revision: str | None = "20261002_0039"
+revision: str = "20261003_0041"
+down_revision: str | None = "20261003_0040"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None