feat(account-events): 账号详情页与账号事件历史全链路支持
1. 新增账号事件数据表 AccountEvent 与迁移版本 20261002_0040_account_events,支持审计 Cookie 变更、自动重登、健康检查及任务执行事件; 2. 后端新增 account_events_service 并扩展 /api/accounts 路由,支持事件查询、过滤与上报; 3. 前端新增 AccountDetailPage 账号详情页组件,支持账号元信息展示、事件历史瀑布流与快捷重登操作; 4. 修复 CK 重登与自动化定时任务中事件关联逻辑,补充 test_account_events 单元测试; 5. 新增生产服务器高负载排查分析报告及相关设计文档。
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
# 生产服务器高负载排查与性能分析报告
|
||||
|
||||
> 采集时间: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%)。<br>2. 宿主机承载了两个业务体系:**接单平台 (`order_site`)** 和 **直播自动任务平台 (`live-hub-py`)**。<br>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%**。<br>4. Postgres 的 CPU 飙升主要是打手端工单统计查询 `getWorkerOrderOverview` 与列表查询,触发了 **PostgreSQL LLVM JIT 动态编译**,单次查询中 JIT 耗时达 **7.21 秒**(总耗时 8.8 秒)。<br>5. MySQL 的高 CPU 和 384GB 读 IO 是因为 `douyu_tasks` (23.8万条) 与 `accounts` 联表带 `assigned_to` 过滤时,执行计划使用了 **`Using temporary; Using filesort`**,高频轮询产生大量临时表全排序。<br>6. `live-hub-py-douyu-login-1` 当前正在持续、高并发执行斗鱼任务(供应商直充、赠送精英令、代理池多并发探测)。 | **100%(已由真实命令、日志与 EXPLAIN 实证)** |
|
||||
| **未知 (Unknown)** | 1. 打手端前端当前轮询的频率(是否为 2~3 秒一次硬轮询)。<br>2. 是否可以在 Postgres 配置中全局关闭 JIT (`jit = off`),或者仅在业务连接池上设置。<br>3. `douyu_tasks` 的管理后台当前是哪个页面在轮询 `assigned_to = 13` 的任务列表。<br>4. live-hub-py 的当前斗鱼任务并发限制是否允许动态限速。 | **待确认** |
|
||||
| **目标 (Objective)** | 1. **根治 Postgres 200% CPU 占用**:解决打手端统计与列表慢查询(禁用 JIT、优化 CTE/索引、增加缓存或降低轮询频率)。<br>2. **降低 MySQL 50% CPU 与 384GB IO 占用**:优化 `douyu_tasks` 的 `assigned_to` 联表排序查询,消除 `Using temporary; Using filesort`。<br>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 编译死锁),但三大重型聚合被频繁触发:<br>1. 管理端全局统计 `getWorkOrderStatistics`(10万工单与28万订单 LEFT JOIN 做全量 SUM);<br>2. 打手端完成单统计 `countWorkerAcceptedOrders`(COUNT DISTINCT);<br>3. 打手概览 `getWorkerOrderOverview`。<br>每次命中启动多 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),这就是用户感知到“忽高忽低”的根源。
|
||||
|
||||
@@ -1381,6 +1381,15 @@ OSS_PREFIX=collect
|
||||
`docker exec -w /app <容器> env VIRTUAL_ENV=/app/.venv uv pip install <包>`;
|
||||
另:`docker exec -e` 覆盖不了 compose 已注入的同名变量,验证时改用容器自带的 env
|
||||
或在脚本里 `os.environ[...] = ...`。
|
||||
- **⚠️ `create_all` 不会修改已存在的表**:测试库靠 `Base.metadata.create_all()` 建表,
|
||||
它**只建缺失的表**。给**已有表**加列/改列后,测试库里的旧结构不会更新,症状是一堆
|
||||
**不相关**的用例同时挂(例如加 `audit_logs.account_id` 后 22 个用例报
|
||||
`Unknown column 'account_id'`)。处理:改已有表的模型后,对测试库手动 `ALTER TABLE`,
|
||||
或 `DROP TABLE` 让它按新模型重建。**新增表**不受影响(会直接建出来);
|
||||
- **灌测试数据要带 `--default-character-set=utf8mb4`**:`docker exec -i mysql ... < file`
|
||||
不带这个参数时中文会被双重编码(`套装` 存成 `套装`)。排查这类问题的快捷判断:
|
||||
对可疑列取 `HEX()`,若是 `C3A5C2A5...` 这种「UTF-8 再编码一次」的形态,
|
||||
说明是**写坏了**而不是读坏了。
|
||||
|
||||
### C.3 已核实的环境事实
|
||||
|
||||
|
||||
+37
-2
@@ -121,8 +121,43 @@ WHERE a.cookie IS NULL OR a.cookie = '';
|
||||
| 持有 CK 的**行**数 | 13265 |
|
||||
| 其中多行持 CK 的账号 | 1(回填取 id 最大的那行) |
|
||||
|
||||
迁移后 `SELECT COUNT(*) FROM accounts WHERE cookie <> ''` **必须等于 13264**
|
||||
(之后随登录增长,只增不减)。
|
||||
迁移后 `SELECT COUNT(*) FROM accounts WHERE cookie <> ''` **必须等于**记录级去重数
|
||||
(之后随登录增长,只增不减;基线是动态的,迁移里用运行时算出的值比对)。
|
||||
|
||||
### 4.2.2 ⚠️ 为什么这条迁移**绝不能丢 CK**(实测代价)
|
||||
|
||||
用户明确要求:**不能靠重新登录来补**。实测依据(2026-10-03):
|
||||
|
||||
| 项 | 实测值 |
|
||||
| --- | --- |
|
||||
| 需要保住的账号数 | **13,534**(持 CK 账号,且在增长) |
|
||||
| 重登速率 | 约 7 轮/小时 × 30 条 = **210 账号/小时**(98% 成功) |
|
||||
| 全量重登耗时 | 13,534 ÷ 210 ≈ **64 小时 ≈ 2.7 天**连续跑 |
|
||||
| **当前重登可用性** | ⚠️ **最近 3 小时成功率 0**(`ck_relogin`:00h 发起 90 / 成功 30;01h、02h 均为 0)——斗鱼对新账号静默不发验证码,即前面查到的邮件验证码问题 |
|
||||
|
||||
**最后一行才是关键**:重登**现在本身就是坏的**。此刻若丢了 CK,不是「花 2.7 天补回来」,
|
||||
而是**很可能根本补不回来**,这些账号的登录态就永久没了。
|
||||
|
||||
由此三条不可让步的要求:
|
||||
|
||||
1. **迁移必须非破坏性** —— 回填只读 `login_tasks`、**不删不改源数据**。
|
||||
CK 在源表里始终留一份,任何环节出问题都能重来;
|
||||
2. **回填不完整必须中止启动**(§6.3)——不能带着半份 CK 切到新读路径;
|
||||
3. **校验要到值级别,不能只比条数** —— 条数相等仍可能张冠李戴(A 账号拿到 B 账号的 CK):
|
||||
|
||||
```sql
|
||||
-- 必须为 0:账号的 CK 与其「最近一条持 CK 记录」的值不一致
|
||||
SELECT COUNT(*) FROM accounts a
|
||||
JOIN (
|
||||
SELECT account_id, cookie,
|
||||
ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY id DESC) AS rn
|
||||
FROM login_tasks WHERE cookie IS NOT NULL AND cookie <> ''
|
||||
) src ON src.account_id = a.id AND src.rn = 1
|
||||
WHERE a.cookie <> src.cookie;
|
||||
```
|
||||
|
||||
> 安全网:因为迁移不删源数据,「最坏情况」永远是**回退代码**(旧列数据完好),
|
||||
> 而不是「账号没救了」。这是整个方案敢一次切换的前提。
|
||||
|
||||
### 4.3 回填校验(迁移后立即执行,写进验收)
|
||||
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
# 设计:账号详情页与账号事件历史
|
||||
|
||||
> 状态:**后端已完成、前端已完成,本地验证通过**(2026-10-03,未提交)
|
||||
> 关联:[登录任务与 CK 解耦](设计-登录任务与CK解耦.md) —— 两者的写入点高度重合,建议同一工作流推进(见 §8)。
|
||||
>
|
||||
> **范围(2026-10-03 已定)**:**不做历史回填**。既有数据(`douyu_tasks` 里的兑换/充值/积分记录)
|
||||
> **不导入**事件表,事件**从上线那一刻起往后累积**。账号的历史明细仍可去既有的任务页查。
|
||||
> 这样本需求从「一次数据搬运工程」缩小为「建表 + 几处发事件 + 一个页面」。
|
||||
|
||||
## 1. 需求
|
||||
|
||||
要一个「账号相关」的界面,围绕**单个账号**把下面这些都记下来、看得见:
|
||||
|
||||
| # | 需求项 | 用户原话 |
|
||||
| --- | --- | --- |
|
||||
| 1 | 导入时间 | 「这个账号 导入时间」 |
|
||||
| 2 | CK 登录记录 | 「ck登录记录」 |
|
||||
| 3 | **CK 失效时间** | 「失效时间」 |
|
||||
| 4 | 兑换记录 | 「兑换记录」 |
|
||||
| 5 | 积分多少 | 「积分多少」 |
|
||||
| 6 | 消耗记录 | 「消耗记录」 |
|
||||
| 7 | 充值记录 | 「充值记录」 |
|
||||
| 8 | **精英宝典开通记录** | 「精英宝典开通记录 时间等等」 |
|
||||
| 9 | **电竞手册开通时间** | 「还有电竞手册开通时间 啥的」 |
|
||||
| 10 | 兜底原则 | 「只要跟这个账号有关系的 都记录下来」 |
|
||||
|
||||
## 2. 现状盘点:每条需求今天有没有数据
|
||||
|
||||
上机实测(2026-10-03)—— 结论是**大部分业务数据已经存在**,但两点不足:
|
||||
**散在 687MB 的噪声表里**、以及**状态变更没有历史**(覆盖式)。本设计**不回填**这些历史,
|
||||
只保证「从现在起不再丢」。
|
||||
|
||||
| # | 需求 | 今天的数据在哪 | 本设计怎么处理 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 导入时间 | `accounts.created_at` | ✅ 直接读,无需事件 |
|
||||
| 2 | CK 登录记录 | `login_tasks`;解耦后状态在 `accounts` | ✅ 读现有 + 新事件补「失效/重登」 |
|
||||
| 3 | **CK 失效时间** | 只有 `ck_check_status`+`ck_checked_at`,**每次检测覆盖** | 🆕 **新增事件**(覆盖前记一笔) |
|
||||
| 4 | 兑换记录 | `douyu_tasks` `exchange_goods`(10,009 行) | 🔸 **不回填**;上线后用收集器增量提取 |
|
||||
| 5 | 积分多少 | `accounts.points`(当前值) | ✅ 状态卡直接读 |
|
||||
| 6 | 消耗记录 | 兑换任务的 `result.score` | 🔸 同 #4(增量) |
|
||||
| 7 | 充值记录 | `douyu_tasks` `create_gold_qr`(13,867 行) | 🔸 同 #4(增量) |
|
||||
| 8 | **精英宝典开通** | 只有 `elite_opened`(bool),**覆盖式** | 🆕 **新增事件** |
|
||||
| 9 | **电竞手册开通** | 只有 `esports_bind_status` 字符串,**覆盖式** | 🆕 **新增事件** |
|
||||
| 10 | 兜底(人工操作) | `audit_logs` | ⚠️ **关联不到账号**,需补 `account_id`(§7) |
|
||||
|
||||
> 🔸 = 既有数据不回填,从上线起增量收录。账号的历史明细仍可在既有的
|
||||
> `/douyu/elite`、`/douyu/esports`、`/login-tasks` 等页面查到,**本需求不复制它们的历史**。
|
||||
|
||||
### 2.1 为什么用事件表、而不是详情页实时聚合
|
||||
|
||||
`douyu_tasks` 现状 **222,679 行 / 687MB**,每天在写。若详情页实时聚合:
|
||||
|
||||
- 每次开页要扫大表 + 解析巨型 JSON(单条兑换的 `result` ≈ 8KB);
|
||||
- 噪声占绝大多数(`query_limited_goods` 6.7 万行、`query_change_bind_time` 3 万行是例行轮询);
|
||||
- `douyu_tasks` 将来若做保留策略(规划 §11.4),账号历史会跟着消失。
|
||||
|
||||
所以事件表**精简、独立、可长期保留**。反过来说:**不回填也就不必去核对历史 JSON 的字段路径**,
|
||||
风险与工作量都大幅下降。
|
||||
|
||||
## 3. 目标模型:`account_events`(账号事件流)
|
||||
|
||||
一张 append-only 的事件表,**账号的时间线 = 这张表按时间倒序读**。
|
||||
|
||||
```python
|
||||
class AccountEvent(Base):
|
||||
__tablename__ = "account_events"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
account_id: Mapped[int] = mapped_column(ForeignKey("accounts.id"), index=True)
|
||||
event_type: Mapped[str] = mapped_column(String(32), index=True)
|
||||
occurred_at: Mapped[datetime] = mapped_column(UTCDateTime, index=True)
|
||||
title: Mapped[str] = mapped_column(String(256), default="") # 直接可展示
|
||||
amount: Mapped[int | None] = mapped_column(Integer, nullable=True) # 充值额/消耗积分(有符号)
|
||||
balance_after: Mapped[int | None] = mapped_column(Integer, nullable=True) # 变更后余额
|
||||
detail: Mapped[dict | None] = mapped_column(JSON, nullable=True) # 精简后的细节(非原始大 JSON)
|
||||
source: Mapped[str] = mapped_column(String(24), default="") # login/task/audit/manual
|
||||
source_ref: Mapped[str] = mapped_column(String(96), unique=True) # 幂等键
|
||||
created_at: Mapped[datetime] = mapped_column(UTCDateTime, default=_utcnow)
|
||||
```
|
||||
|
||||
设计要点:
|
||||
|
||||
- **`source_ref` 唯一键**:如 `douyu_task:123456`、`login_task:14166`、`audit:789`。
|
||||
作用是**幂等**:收集器重跑、事件重复发射都不会造出重复时间线(§4 的水位线失守时靠它兜底);
|
||||
- **不存原始大 JSON**:只存提取出的业务字段(商品名、单号、金额…),把 687MB 的包袱甩掉;
|
||||
- **`amount` 有符号**:充值为正、兑换消耗为负,便于页面上做「累计充值 / 累计消耗」;
|
||||
- **`occurred_at` 用业务发生时间**(不是入库时间):收集器取原任务行的 `finished_at || created_at`;
|
||||
直接发射的取当下时刻。
|
||||
|
||||
### 3.1 事件类型(初版)
|
||||
|
||||
| event_type | 含义 | 来源 |
|
||||
| --- | --- | --- |
|
||||
| `imported` | 账号导入 | `accounts.created_at` |
|
||||
| `login_ok` | 登录成功(含新 CK) | `login_tasks` / 解耦后的账号写入点 |
|
||||
| `ck_invalid` | **CK 失效**(新增记录) | 巡检判定 |
|
||||
| `relogin_ok` / `relogin_failed` | 自动重登结果 | `ck_relogin` 作业 |
|
||||
| `bind_ok` / `rebind` | 绑定 / 换绑 | `douyu_tasks` |
|
||||
| `recharge` | 充值到账 | `create_gold_qr` |
|
||||
| `exchange` | 兑换(**含消耗积分**) | `exchange_goods` |
|
||||
| `points_credit` | 积分到账(含**到期时间**) | `donate_elite_gift` |
|
||||
| `points_expire` | 积分即将到期 | 由 `willExpireTime` 派生 |
|
||||
| `elite_opened` | **精英宝典开通**(新增记录) | `douyu_runner_manual` |
|
||||
| `esports_opened` | **电竞手册开通**(新增记录) | `douyu_runner_manual` |
|
||||
| `assigned` / `tag_changed` / `remark_changed` | 人工操作 | `audit_logs`(需补 account_id,见 §7) |
|
||||
|
||||
## 4. 业务事件怎么收录:增量收集器(不回填)
|
||||
|
||||
既有历史**不导入**。业务事件(充值/兑换/积分到账/绑定)用**一个增量收集器**从 `douyu_tasks`
|
||||
往后提取 —— 好处是**完全不侵入 runner 代码**(不必改 8 个业务文件),且新老任务类型统一处理。
|
||||
|
||||
### 4.1 收集器设计
|
||||
|
||||
```
|
||||
水位线(watermark):记录「已收集到 douyu_tasks.id = N」
|
||||
每轮(挂到现有 automation 调度器,5~15 分钟一次):
|
||||
1. 读 douyu_tasks WHERE id > N AND task_type IN (业务类型) ORDER BY id LIMIT 500
|
||||
2. 按 §4.2 的映射提取字段 → 写 account_events(source_ref = "douyu_task:<id>" 保证幂等)
|
||||
3. 更新水位线到本批最大 id
|
||||
```
|
||||
|
||||
- 水位线存在 `automation_jobs.config`(JSON)或单独一小张表,**必须持久化**(进程重启不重来);
|
||||
- `source_ref` 唯一键兜住「同一行被处理两次」(例如水位线写入失败后重跑);
|
||||
- **起点 = 上线那一刻的 `MAX(douyu_tasks.id)`**,不回溯。
|
||||
|
||||
### 4.2 字段映射(已抽样核实,仅用于**新**数据)
|
||||
|
||||
已实测的 `result` 结构(用于收集器解析):
|
||||
|
||||
**`exchange_goods` → `exchange`**
|
||||
|
||||
| 事件字段 | JSON 路径 | 实测样例 |
|
||||
| --- | --- | --- |
|
||||
| title | `commodity_name` | `套装-兔兔白日梦` |
|
||||
| detail.commodity_id | `commodity_id` | `20260120QYOOB_bag10` |
|
||||
| **amount(消耗)** | `score`,取负 | `-1280` |
|
||||
| **balance_after** | `points_query.points` | `0` |
|
||||
| detail.order_id / exchange_id | `order_id` / `exchange_id` | `147879` / `11241526` |
|
||||
| detail.game_name | `game_name` | `唉痛恨透` |
|
||||
| detail.game_channel | `game_channel` | `手Q / 安卓(android)` |
|
||||
|
||||
**`create_gold_qr` → `recharge`**
|
||||
|
||||
| 事件字段 | JSON 路径 | 实测样例 |
|
||||
| --- | --- | --- |
|
||||
| **amount(充值)** | `buy_num` / `pay_amount` | `98` |
|
||||
| detail.order_id | `order_id` | `DYGF5C24C5E4EA84T238035` |
|
||||
| detail.supplier_order_id | `supplier_order.order_id` | `200000229425420261003114601` |
|
||||
| detail.supplier_finished_at | `supplier_order.finish_time` | `2026-10-03 11:46:07` |
|
||||
| detail.channel | `recharge_channel` | `supplier_api` |
|
||||
| detail.douyu_uid | `douyu_uid` | `678503799` |
|
||||
|
||||
**`donate_elite_gift` → `points_credit`**
|
||||
|
||||
| 事件字段 | JSON 路径 | 实测样例 |
|
||||
| --- | --- | --- |
|
||||
| **amount(到账)** | `gift_points_expected_gain` | `980` |
|
||||
| **balance_after** | `points` / `points_query.points` | `1280` |
|
||||
| detail.baseline / target | `gift_points_baseline` / `gift_points_target` | `300` / `1280` |
|
||||
| **detail.points_expire_at** | `points_query.raw.data.willExpireTime` | `1798819199`(时间戳) |
|
||||
|
||||
> 绑定类(`confirm_bind` / `*_esports_bind`)的 `result` 结构**尚未抽样**,实现前先各 dump 一条
|
||||
> (教训见 [CK 误删复盘](事故复盘-CK误删与恢复.md):字段假设必须实测)。收集器对
|
||||
> **解析不出的行要记 warning 并跳过**,不能让一行坏数据卡住整条水位线。
|
||||
|
||||
### 4.3 覆盖的事件量(预估)
|
||||
|
||||
按实测的业务任务量,稳定后每天新增事件量级在**数百条**(充值/兑换/到账为主),
|
||||
一年约 10 万条以内 —— 事件表长期保留没有压力。
|
||||
|
||||
## 5. 事件从哪来:两条路
|
||||
|
||||
### 5.1 直接发(CK 相关 + 开通类)—— 4 处
|
||||
|
||||
这几类**不是 `douyu_tasks` 里的行**,只能在代码里「状态被覆盖之前先记一笔」:
|
||||
|
||||
| 场景 | 位置 | 事件 |
|
||||
| --- | --- | --- |
|
||||
| CK 判定失效 | `automation/jobs/ck_health.py`(判定 invalid 时) | `ck_invalid` |
|
||||
| CK 重登成功/失败 | `login_service._write_login_success` / `ck_relogin`(**CK 解耦的同一处**) | `login_ok` / `relogin_ok` / `relogin_failed` |
|
||||
| 精英宝典开通 | `douyu_runner_manual.py:377`(`elite_opened = True` 那一行) | `elite_opened` |
|
||||
| 电竞手册开通 | `douyu_runner_manual.py:426`(`esports_bind_status = "esports_opened"`) | `esports_opened` |
|
||||
|
||||
这 4 处正好是「当前状态被覆盖」的现场。实现上收敛成一个 helper:
|
||||
`account_events_service.emit(db, account_id, type, title, amount=..., balance_after=..., detail=...)`,
|
||||
并要求**发射失败不影响主流程**(事件是旁路,不能因为它让登录/兑换失败)。
|
||||
|
||||
### 5.2 收集器(业务类)—— 0 处侵入
|
||||
|
||||
充值 / 兑换 / 积分到账 / 绑定 这些**已经是 `douyu_tasks` 的行**,走 §4 的增量收集器,
|
||||
**不动 runner 代码**。
|
||||
|
||||
> 这个分工的取舍:CK 与开通事件「只在内存里出现过」,不记就永远没有;
|
||||
> 业务事件已经在表里有结构化落库,再改一遍 runner 属于重复工作。
|
||||
|
||||
## 6. 页面设计:`/accounts/:id`(账号详情)
|
||||
|
||||
现在是**列表页**(`AccountsPage.tsx`),没有详情页。新增:
|
||||
|
||||
```
|
||||
┌─ 顶部:账号身份 ────────────────────────────────────────┐
|
||||
│ 用户名 / 斗鱼昵称+UID / 标签 / 归属客服 / 导入时间(第 N 天) │
|
||||
│ 操作:分配 · 标签 · 备注 · 登录 · 检测 CK · 重登 · 导出 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
┌─ 状态卡(当前值:accounts 表)──────────────────────────┐
|
||||
│ 积分(含到期时间) · 鱼翅余额 · 兑换余额 │
|
||||
│ 精英宝典:已开通 / 到期时间 · 电竞手册:开通+绑定+可换绑 │
|
||||
│ CK:状态 · 最近检测 · 最近重登 ← CK 解耦后从 accounts 读 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
┌─ 统计条(由 account_events 聚合)──────────────────────┐
|
||||
│ 累计充值 ¥X · 累计兑换 N 次 / 消耗积分 M · CK 失效 K 次 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
┌─ 时间线(account_events 倒序 + 类型筛选 + 分页)────────┐
|
||||
│ 10-03 11:46 充值 ¥98 供应商直充成功 │
|
||||
│ 10-03 11:46 积分 +980 → 余额 1280(到期 12-31) │
|
||||
│ 10-03 11:46 兑换 -1280 套装-兔兔白日梦 │
|
||||
│ 10-02 17:17 登录 重新登录成功,Cookie 有效 │
|
||||
│ 10-01 09:12 CK失效 检测判定 CK 已失效 │
|
||||
│ 09-30 14:00 开通 精英宝典开通 │
|
||||
└──────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 接口:`GET /api/accounts/{id}/events?types=&page=&page_size=`、`GET /api/accounts/{id}/overview`;
|
||||
- 权限:沿用账号可见性(客服只能看自己名下账号)+ 敏感字段按现有 `account:view_full` 控制;
|
||||
- 入口:账号列表点用户名进详情;从 CK 页、登录任务页也可跳转。
|
||||
|
||||
## 7. 必须先修的缺口:`audit_logs` 关联不到账号
|
||||
|
||||
实测 `audit_logs.target` 是**自由文本**:
|
||||
|
||||
| action | 样例 target |
|
||||
| --- | --- |
|
||||
| `account:assign` | `冰柳192`(用户名) |
|
||||
| `account:import` | `导入103个`(一句话) |
|
||||
| `recharge:douyu:create` | `douyu_batch:000619c494fd`(批次号) |
|
||||
| `account:delete` | `hui60945`(用户名) |
|
||||
|
||||
**没有 account_id**,只能拿用户名去模糊匹配 —— 用户名可改、可重复,**不可靠**。
|
||||
|
||||
建议:`audit_logs` 增加 `account_id`(可空)列,账号相关操作写入时带上。
|
||||
这样「谁在什么时候分配/改标签/删除了这个账号」才能进账号时间线。
|
||||
这是本次需求的前置项,改动很小(加一列 + 若干调用点传参)。
|
||||
|
||||
## 8. 与 CK 解耦怎么「一块弄」
|
||||
|
||||
两者的**写入点是同一批**,所以设计上共用一套约定,实施上排好顺序即可:
|
||||
|
||||
| 事项 | CK 解耦 | 账号事件 |
|
||||
| --- | --- | --- |
|
||||
| 状态落点 | CK/健康/重登状态 → `accounts` | 新增 `account_events`(流水) |
|
||||
| 写入点 | `_write_login_success`、`ck_health`、`ck_relogin` | 同上 + `elite/esports 开通` 两处 |
|
||||
| 读取点 | Cookie 页、工作台、仪表盘 | 新增详情页 |
|
||||
|
||||
**共享的一条原则**:`accounts` 存**当前状态**,`account_events` 存**发生过什么** ——
|
||||
这与 CK 解耦「状态归账号、流水归历史表」是同一个模型。做完解耦,账号上就有了完整的
|
||||
「当前状态」,事件表补上「历史」,详情页就是把两者拼起来。
|
||||
|
||||
**建议顺序**(在 [CK 解耦的 7 个提交](设计-登录任务与CK解耦.md#10-提交计划7-个提交) 之后接):
|
||||
|
||||
| 提交 | 内容 | 依赖 |
|
||||
| --- | --- | --- |
|
||||
| E1 | `feat(db): account_events 表 + audit_logs.account_id` | 无 |
|
||||
| E2 | `feat(events): 事件发射 helper + CK 事件(失效/登录/重登)` | **CK 解耦已完成**(否则写入点还会变) |
|
||||
| E3 | `feat(events): 开通事件(精英宝典 / 电竞手册)` | 无 |
|
||||
| E4 | `feat(events): douyu_tasks 增量收集器 + 水位线` | 无 |
|
||||
| E5 | `feat(api): 账号事件与概览接口` | E1 |
|
||||
| E6 | `feat(ui): 账号详情页(状态卡 + 统计 + 时间线)` | E5 |
|
||||
| E7 | `docs: 落点与验收` | —— |
|
||||
|
||||
> 原计划的「历史回填脚本」已取消(§2 已定不回填),E4 从「搬运 8.5 万行」变成
|
||||
> 「装一个水位线收集器」。
|
||||
|
||||
## 9. 风险与注意
|
||||
|
||||
1. **不改 runner 代码**:业务事件走收集器(§4),只有 CK/开通 4 处直接发(§5.1)——
|
||||
把改动面压到最小;
|
||||
2. **`source_ref` 唯一键是幂等的生命线**:收集器重跑、事件重复发射都靠它兜住,
|
||||
漏了就会污染时间线;
|
||||
3. **收集器的水位线必须持久化**,且**解析失败要跳过并告警**,不能让一行坏数据卡住整条链路;
|
||||
4. **发射事件是旁路**:必须保证 `emit()` 抛错不影响登录/兑换主流程(用 try 包住 + 记日志);
|
||||
5. **积分口径要先定**(决策 C):`accounts.points` 与 `donate_elite_gift` 的 `points_query.points`
|
||||
有不一致的情况(如兑换后 `points_query.points=0`),时间线显示哪个要明确;
|
||||
6. 绑定类任务的 `result` 结构**尚未抽样**(§4.2),收集器实现前先 dump 一条确认。
|
||||
|
||||
## 10. 待确认的决策点
|
||||
|
||||
| # | 决策 | 选项 | 建议 |
|
||||
| --- | --- | --- | --- |
|
||||
| **A** | 时间线是否收录「例行查询」类事件 | ①只收业务事件(推荐)②全都收 | ①:6.7 万行轮询进去后时间线没法看 |
|
||||
| **B** | 详情页入口 | ①账号列表点用户名(推荐)②独立菜单 | ①更顺手;②可加,但主入口应是列表 |
|
||||
| **C** | 积分显示口径 | ①以 `accounts.points` 为准,事件里记变更量(推荐)②以事件累加为准 | ①简单可靠;②容易和历史噪声打架 |
|
||||
| **D** | 收集器的跑法 | ①挂到现有 automation 调度器(推荐)②独立定时任务 | ①复用现成的开关/运行记录/页面;②多一套机制 |
|
||||
| **E** | `audit_logs` 加 `account_id` 是否随本次做 | ①做(否则人工操作进不了时间线)②先不做 | ①(§7) |
|
||||
| ~~F~~ | ~~回填范围~~ | —— | ✅ **已定:不回填**,从上线起增量累积 |
|
||||
|
||||
---
|
||||
|
||||
## 11. 落地记录(2026-10-03)
|
||||
|
||||
已按本文实现,**未提交、未碰生产**(用户要求先在本地验证)。
|
||||
|
||||
| 提交建议 | 内容 | 落点 |
|
||||
| --- | --- | --- |
|
||||
| E1 | 迁移 `20261002_0040`:`account_events` + `audit_logs.account_id` | `migrations/versions/20261002_0040_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`,注册为「账号事件收集」 |
|
||||
| E5 | 接口 | `routers/accounts.py`:`GET /{id}/overview`、`GET /{id}/events` |
|
||||
| E6 | 页面 | `pages/AccountDetailPage.tsx`;路由 `/accounts/:id`;列表用户名可点入 |
|
||||
|
||||
### 11.1 与设计的差异(实施时改的)
|
||||
|
||||
1. **收集器的水位线放作业自己的 `config`**,不再寄生在 `ck_health` 的 config 里 ——
|
||||
顺带白拿「自动巡检」页的开关、运行记录与手动触发;
|
||||
2. **`source_ref` 可空唯一,不用空串默认值**。原设计写的是 `server_default=""`:MySQL 唯一索引下
|
||||
**空串会互相冲突**,第二条「无幂等键」的事件就插不进去。改为可空(MySQL 允许 NULL 重复),
|
||||
并补了用例专门盯这点;
|
||||
3. **电竞手册只在「真正开通」那一支发事件**。代码里有两处都会写 `esports_bind_status="esports_opened"`:
|
||||
支付流程成功后(真开通)与「基线查到已开通」(检测)。两处都发会每次检查刷一条,只取前者;
|
||||
4. **重登失败只在「转人工」时发事件**:还要退避重试的每轮都会再来,发事件会刷屏;
|
||||
5. **`emit` 用 SAVEPOINT 隔离**:失败只回滚这一条,不污染调用方事务(有用例覆盖)。
|
||||
|
||||
### 11.2 实施时踩到的坑
|
||||
|
||||
1. **`create_all` 不改已存在的表** —— 给 `audit_logs` 加列后,测试库里的旧表没有该列,
|
||||
**22 个不相关测试全挂**(`Unknown column 'account_id'`)。改已有表的模型后,测试库要么手动
|
||||
`ALTER`,要么 `DROP` 让 `create_all` 重建。已在本地测试库补列;**这个坑值得写进通用约定**;
|
||||
2. **REPEATABLE READ 快照**(又踩一次):用例里用另一个 session 写入事件后,原 session 查不到 ——
|
||||
与 P1b「成功被记成失败」同根因。用例里补 `commit()` 结束事务;
|
||||
3. **`delete_account` 必须连带清事件**:`account_events` 有指向 `accounts` 的外键,不清就删不掉账号。
|
||||
这是本次改动**引入的**问题,已修并有用例;
|
||||
4. **测试数据的字符集**:用 `docker exec mysql < file` 灌中文时没带 `--default-character-set=utf8mb4`,
|
||||
数据被双重编码(`套装` → `套装`)。排查时先看 `HEX()` 就能立刻分清「存坏了」还是「读坏了」。
|
||||
|
||||
### 11.3 本地验证结果
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| 后端测试 | **363 通过**(新增 20 条:幂等、旁路隔离、映射、水位线、API、账号隔离) |
|
||||
| 收集器 | 3 条业务任务 → 3 条事件;金额/余额/中文均正确;水位线 0 → 686 |
|
||||
| 概览接口 | 累计充值 98、兑换 1 次、消耗积分 1280、导入时间、登录次数、最近 CK 检测均正确 |
|
||||
| 时间线接口 | 倒序分页正确、按类型筛选正确、中文无乱码 |
|
||||
| 前端 | `tsc -b` 通过、`vite build` 通过、模块转译正常(dev 5174 → 8800) |
|
||||
|
||||
> 本地验证时把收集器**临时启用**并手动灌了 3 条测试任务(`batch_id='local-evt'`,仅本地开发库)。
|
||||
> 生产上该作业仍是 `enabled=false`,需在「自动巡检」页手动打开;首次运行只定水位线、不回溯历史。
|
||||
Reference in New Issue
Block a user