- Sprint 执行计划 v1.1:§1.1 补第一批交付实据;T9 SPIKE 结论回写 Sprint 1 表 (T-P0-16 通过 2.8ms;T-P0-12 有条件;T-P0-10 原型 6/6 + 真实照片重验前置); Sprint 2 表标注 T-P2-01~06 交付;新增 §9 两批交付记录 - 开发任务拆解 v1.5:交付回写 + 新风险登记(RTMPose 域差异、BiRefNet 异步化) - 新增 开发任务包/:三批多 agent 并行的协作规程与 14 份任务契约 - 新增 前置开通清单-云资源与资质.md - 清理已用毕的评审报告(文档评审 2026-08-13、项目整体评审 2026-08-14), README 中相应引用改为留痕表述
5.4 KiB
5.4 KiB
T3 · 客户端埋点批量上报
对应任务:
T-P1-10(埋点事件字典/上报通道的服务端部分)| 需求:FR-SYS-06、架构 §9.3 | 分支:task/t3-events| worktree:/home/xsl/meida_backend-t3先读《README.md(协作规程)》,遵守全部纪律。
1. 目标
实现 POST /events/batch:客户端批量上报埋点事件,落 ob_event 表。这是 V1 全链路埋点(FR-SYS-06)与后续三张看板(TD-24)的数据入口。
2. 已就绪的契约(协调提交已提供)
表(迁移 0004 已建,ORM 已加,你不需要碰 models.py / migrations)
ob_event(架构 §6.2):
event_idbigint PK(雪花)、user_idbigint NOT NULL、event_namevarchar(64)、propsJSONB、client_tstimestamptz NULL、server_tstimestamptz NOT NULL default now()、- 索引:
(event_name, server_ts)、(user_id, server_ts)。 - 架构原文"按月分区"——V1 先普通表(与 ob_ai_call 同样处理),代码注释写明"分区在数据量上来后再做,先不预支复杂度"。
配置(config.py 已加字段)
events_max_batch: int = 50 # env: EVENTS_MAX_BATCH
events_props_max_bytes: int = 8192 # env: EVENTS_PROPS_MAX_BYTES
错误码(errors.py 已加)
EVENTS_BATCH_REJECTED = 40020:400「埋点数据格式有误」,hint=null(对客户端静默降级即可,不打扰用户)。
骨架(协调提交已建,直接填充)
packages/analytics/__init__.py、packages/analytics/README.md、packages/analytics/service.py(空骨架)apps/api/routers/events.py(空骨架 router,已注册进 main.py,prefix/events)apps/api/deps.py已有AnalyticsServiceDep(构造AnalyticsService(session))——deps.py 禁改,签名如不满足需要,用路由函数内自建服务对象的方式,不改 deps。
分层位置
packages.analytics 被协调提交放进了 import-linter 的"共享能力层"(与 identity/billing/task 同层)——它只能 import packages.shared/packages.domain,不得 import 其它业务包或 llm。
3. 实现要点
允许改动:packages/analytics/**、apps/api/routers/events.py、测试文件。
packages/analytics/schemas.py(新建):EventIn:event_name: str(pattern=r"^[a-z][a-z0-9_.]{1,63}$")、props: dict[str, Any] = {}、client_ts: datetime | None = None;EventBatchRequest:events: list[EventIn](min_length=1)、EventBatchResponse:accepted: int。- 事件字典(event dictionary):V1 不做服务端枚举校验(客户端事件名集合由 TD-23 埋点字典维护,尚未定稿)——用正则约束形状即可,代码注释写明"字典校验等 TD-23 定稿后再收紧"。
packages/analytics/service.py:AnalyticsService(session):async def record(user_id: int, batch: list[EventIn]) -> int;- 校验(fail-close,任一不过 → 整批拒绝
AppError(EVENTS_BATCH_REJECTED),不部分落库):批量 ≤events_max_batch;每条 props 序列化后 ≤events_props_max_bytes(len(json.dumps(props, ensure_ascii=False).encode()));client_ts若给出:不晚于 server_now+5min、不早于 7 天前; - 组装
ObEvent(event_id=next_id(), user_id=..., server_ts=datetime.now(UTC))逐条 add,flush一次,返回条数;(from packages.domain.ids import next_id,参考 identity/service.py 用法。) - 不含 PII 清洗:props 里若有
phone/token/authorization键 → 直接删除该键并logger.warning("events_pii_dropped", ...)(NFR-SEC-03 日志脱敏的同族要求)。
apps/api/routers/events.py:POST /batch,user_id: CurrentUserId,analytics: AnalyticsServiceDep(若 Dep 不可用则函数内AnalyticsService(session)withSessionDep);ok(EventBatchResponse(...));服务层抛的AppError自然走统一异常处理,不需要额外 catch。
4. 测试要求
- 单测
tests/unit/test_events_validation.py(纯校验逻辑,不连库——把校验拆成模块级纯函数validate_batch(events, *, max_batch, max_bytes, now) -> list[ObEvent-like]便于无 DB 测试):- 合法批量通过;超批量拒;props 超字节拒;event_name 正则(大写/数字开头/超长拒);client_ts 未来>5min 拒、7 天前拒;PII 键被剔除。
- 集成测试
tests/integration/test_events.py(新文件,允许跑):dev-token → POST /v1/events/batch 两条事件 → 200 accepted=2;查库两行ob_event;再 POST 非法批(event_name 大写)→ 40020 且库里没有半条。
5. 禁改清单(通用禁改之外)
packages/llm/**、packages/billing/**、packages/identity/**、packages/storage/**、packages/agent/**、apps/api/routers/{agent,auth,quota,tasks,uploads}.py、tests/integration/test_vertical_slice.py。
6. 验收标准
POST /api/v1/events/batch(及/v1别名)可用,认证必需(无 token → 40100);- 整批原子:要么全落、要么全拒;
- 校验规则全部有测试覆盖;
- 全部检查绿(mypy 范围同 T2 列表)。
7. 提交信息
feat(analytics): POST /events/batch with fail-close validation
ob_event 落库通道:形状校验 + props 字节上限 + client_ts 合理性 +
PII 键剔除,整批原子(FR-SYS-06 服务端部分)。
偏差说明:<如有写在这里,无则写"无">