docs: 沉淀 Task3 质量审查结论(Task8 索引打包/active_now 走 last_seen_at/站点软删/分区预案)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-15 11:56:44 +08:00
parent 462852c3f3
commit 3242d12f12
@@ -1056,6 +1056,7 @@ return resp
- Create: `server/tracker/services/stats.py`, `server/tracker/management/__init__.py`, `server/tracker/management/commands/__init__.py`, `server/tracker/management/commands/refresh_stats.py`, `server/tracker/tests/test_stats.py`
- [ ] **Step 1: 失败测试**:造 2 天数据(访客 v1 两天访问、v2 一天)→ `refresh(site, date)` 后 DailyStat 的 pv/uv/sessions/new_visitors 正确;uv 按访客去重、new_visitors 按 first_seen_at 当日计数。
- [ ] **Step 1b: 索引迁移打包(Task 3 质量审查结论)**:本任务顺带一个迁移——`Event` 加 `Index(fields=["site", "occurred_at"])`(按日聚合查询否则只能前缀扫全站)、`VisitSession` 加 `Index(fields=["site", "started_at"])`、FK 冗余单列索引清理(`event.site`/`event.session`/`visit_session.site`/`visit_session.visitor`/`daily_stat.site` 的 FK 加 `db_index=False`,**`event.visitor` 保留**——级联删除需要);同时收紧 `test_models.py` 断言(`pytest.raises(IntegrityError)`)并补一条删站级联测试(删 site → 三张子表全清)。
- [ ] **Step 2: 实现** `stats.py`:
```python
from datetime import datetime, time, timedelta
@@ -1096,7 +1097,7 @@ def refresh_daily(site, date) -> DailyStat:
**Files:**
- Create: `server/tracker/api/sites.py`, `server/tracker/tests/test_sites.py`, Modify: `config/urls.py`
- [ ] **Step 1: 失败测试**:CRUD(列表/新建自动生成 site_id/更新域名白名单/停用);仅登录成员可访问,删除仅 admin;域名校验(去协议、转小写、非空列表)。
- [ ] **Step 1: 失败测试**:CRUD(列表/新建自动生成 site_id/更新域名白名单/停用);仅登录成员可访问,删除仅 admin;域名校验(去协议、转小写、非空列表)。**删除走软路径**:默认 `is_active=False` 停用;物理删除为管理命令分批删子表(大站单语句级联删除有锁/binlog 风暴风险,CASCADE 只作完整性兜底)。
- [ ] **Step 2: 实现**:DRF `APIView` 或轻量 `ModelViewSet` + 自行包 `{code,data,message}`。序列化输出含 `site_id/domains/is_active/allow_no_origin`。
- [ ] **Step 3: 测试通过,Commit** `feat: 站点管理接口`
@@ -1116,7 +1117,7 @@ def test_overview_shape(db, client, django_user_model):
assert r.json()["data"]["sources"][0]["source"] == "google.com"
assert r.json()["data"]["top_pages"][0]["path"] == "/pricing"
```
- [ ] **Step 2: 实现**:`today` 用事件/会话表实时聚合(当天 Asia/Shanghai 边界);`trend` 取 DailyStat 近 30 天(缺日补零;今天用实时值覆盖最后一位);`sources` = 当天 VisitSession group by `referrer_domain`(空→"直接访问")top8;`top_pages` = 当天 page_view group by path top8;`active_now` = Event `received_at > now-5min` distinct visitor。认证 + 成员权限。**所有聚合排除 `is_bot=True`(爬虫不进 Dashboard)。**
- [ ] **Step 2: 实现**:`today` 用事件/会话表实时聚合(当天 Asia/Shanghai 边界);`trend` 取 DailyStat 近 30 天(缺日补零;今天用实时值覆盖最后一位);`sources` = 当天 VisitSession group by `referrer_domain`(空→"直接访问")top8;`top_pages` = 当天 page_view group by path top8;`active_now` = `Visitor.last_seen_at > now-5min` 计数(**不要用** `Event.received_at`——无索引会全表扫;last_seen_at 每批刷新且已有索引,语义等价)。认证 + 成员权限。**所有聚合排除 `is_bot=True`(爬虫不进 Dashboard)。**
- [ ] **Step 3: 测试通过,Commit** `feat: 总览聚合接口`
### Task 12: 访客列表/详情/时间线/会话 API
@@ -1514,6 +1515,7 @@ services:
- Task 6 ingest 的 `duration_ms` 以"计划内正确版本"实现(F 表达式 + new_events 排除重放),不要照抄占位伪码。
- geo.py 是进程级单例(xdb 一次读入 buffer);dropped 统一按事件数计。
- MySQL 的 `INSERT IGNORE`(ignore_conflicts)会连字段截断类错误一起吞——`_FIELD_LEN` 已按模型长度截断,属双保险;导入/迁移后抽查字段长度。
- **event 表按月分区预案(触发阈值:行数 >5000 万或分区维护成为瓶颈时启动)**,迁移日必做四件事:① PK `(id)` → `(id, occurred_at)`(Django 5.2 `CompositePrimaryKey` 可表达);② `uq_site_event` 扩为 `(site, event_id, occurred_at)`——去重从全局弱化为时间作用域,SDK 重放携带同 occurred_at 仍撞键 + ingest 应用层预查双保险在;③ event 的 3 个 FK 改 `db_constraint=False`(InnoDB 分区表不支持外键),站点清除改管理命令分批 DELETE;④ `ALTER ... PARTITION BY` 是 COPY 重建(锁写、磁盘翻倍)——要么趁数据小做,要么 gh-ost/pt-osc 切换。
- Art Design Pro 模板内部组件 API 以克隆下来的实际代码为准(Task 17 后先读 `src/views` 现存示例再写页面,注意模板 request 封装签名可能是 `request.get({url, params})`);本计划页面代码是基线,允许按模板约定适配,**不允许**砍功能。
- ip2region pip 包若与计划 API 不符,参照其 README 调整 `geo.py`,测试断言不变。
- 所有中文文案直接用于界面;代码注释保持中文、少量、说明意图。