feat(plan): 生成 label_backend 完整实施规划文档

Phase 0:research.md(10项技术决策,无需澄清项)
Phase 1:data-model.md(11张表+Redis结构),contracts/(8个模块API契约),quickstart.md(Docker Compose启动+流水线验证)
plan.md:宪章11条全部通过,项目结构确认
This commit is contained in:
wh
2026-04-09 12:27:16 +08:00
parent 0891ae188d
commit 4054a1133b
15 changed files with 1741 additions and 22 deletions

View File

@@ -0,0 +1,150 @@
# Phase 0 研究报告label_backend
**日期**: 2026-04-09
**分支**: `001-label-backend-spec`
---
## 技术决策汇总
所有技术选型均由宪章强制约束,无需评估备选方案。本报告记录关键设计决策的理由,供后续实施参考。
---
## 决策 1认证机制
**决策**: UUID v4 Token 存储于 Redis滑动过期禁止 JWT
**理由**:
- JWT 自包含令牌无法按需吊销,无法满足"管理员禁用账号立即生效"的安全要求
- UUID Token 在 Redis 中可精确控制生命周期:退出登录或禁用账号时同步删除 Key下一次请求立即失效
- 滑动过期(每次有效请求重置 TTL确保活跃用户不被意外踢出
**备选方案放弃理由**:
- JWT无法即时吊销存在安全窗口
- Session Cookie在无状态 REST API 架构中不适用
- OAuth2过度设计当前场景无第三方授权需求
---
## 决策 2多租户隔离机制
**决策**: MyBatis Plus `TenantLineInnerInterceptor` + `ThreadLocal CompanyContext`
**理由**:
- `TenantLineInnerInterceptor` 在 SQL 拦截器层自动在每条查询的 WHERE 子句中注入 `company_id`,覆盖范围广且无需逐方法手动添加条件
- ThreadLocal 存储当前请求的 `companyId`,由 Shiro TokenFilter 在解析 Token 时从 Redis 会话数据注入,确保 companyId 来自服务端权威来源而非客户端参数
- `finally` 块强制清理 ThreadLocal防止线程池复用时数据串漏
**备选方案放弃理由**:
- 行级安全RLSPostgreSQL 原生支持,但与 MyBatis Plus 集成复杂,且宪章已指定 ThreadLocal 方案
- 逐方法手动添加 WHERE容易遗漏维护成本高
---
## 决策 3任务并发领取控制
**决策**: Redis `SET NX`(分布式锁)+ 数据库乐观约束(`WHERE status = 'UNCLAIMED'`)双重保障
**理由**:
- 单纯使用数据库乐观锁在高并发下存在写放大问题(大量 UPDATE 竞争)
- 单纯使用 Redis 锁若锁过期后 DB 写入失败可能导致数据不一致
- 双重保障Redis 锁TTL 30s快速拦截大部分并发请求减少数据库压力DB 乐观约束作为最终一致性兜底
**Key 命名**: `task:claim:{taskId}`TTL 30s与宪章 Redis Key 规范一致)
---
## 决策 4审批触发 QA 任务的异步解耦
**决策**: Spring `@TransactionalEventListener(phase = AFTER_COMMIT)` + `@Transactional(REQUIRES_NEW)`
**理由**:
- 提取阶段审批通过后需调用 AI HTTP 生成候选问答对,该 HTTP 调用延迟不确定(秒级到分钟级)
- 若在 `@Transactional` 内同步调用,数据库连接被长时间占用,且 AI 失败会错误地回滚已完成的审批操作
- `AFTER_COMMIT` 保证业务审批先提交再触发事件,避免事务回滚导致的幽灵任务
- `REQUIRES_NEW` 为 QA 生成开启独立事务AI 失败仅影响 QA 任务创建,不影响审批结果
**事件流**: `approve()` → publish `ExtractionApprovedEvent` → 事务提交 → `onExtractionApproved()` 异步执行AI 调用 + 创建 QA 任务)
---
## 决策 5标注结果存储语义
**决策**: JSONB 整体覆盖PUT 语义),禁止局部 PATCH
**理由**:
- 三元组/四元组条目具有强关联性(主语-谓语-宾语作为整体,或主体-关系-客体-修饰词作为整体),局部更新易导致不一致
- 整体替换简化服务端逻辑,前端每次提交完整 items 数组,服务端直接执行 UPDATE `result_json = ?`
- 避免局部追加导致的索引层数据不一致(如删除某条目后残留旧数据)
---
## 决策 6审计日志事务边界
**决策**: 审计日志写入不要求与业务操作在同一事务AOP `finally` 块中独立写入
**理由**:
- 审计写入失败不应回滚业务操作(用户的标注/审批结果比审计日志更重要)
- `@Around` 通知在业务方法执行完成commit 或 rollback后捕获最终 `result`,可记录准确的成功/失败状态
- 审计失败仅 error 级别日志 + 告警,不影响用户体验
---
## 决策 7视频预处理幂等回调
**决策**: 回调处理时检查 `video_process_job.status`,已为 `SUCCESS` 则静默忽略
**理由**:
- AI 服务可能因网络抖动对同一 jobId 发起多次成功回调
- 幂等检查确保第一次成功回调创建标注任务,后续重复回调无任何副作用
- 检查粒度:`status == SUCCESS` 即返回,不进行任何 DB 写入
---
## 决策 8对象存储路径规范
**决策**: RustFSS3 兼容),文件字节流禁止入库,路径按资源类型分桶分目录
**路径规范**:
| 资源 | 桶 | 路径格式 |
|------|-----|---------|
| 文本文件 | `source-data` | `text/{yyyyMM}/{source_id}.txt` |
| 图片 | `source-data` | `image/{yyyyMM}/{source_id}.jpg` |
| 视频 | `source-data` | `video/{yyyyMM}/{source_id}.mp4` |
| 视频帧 | `source-data` | `frames/{source_id}/{frame_index}.jpg` |
| 视频转文本 | `source-data` | `video-text/{parent_source_id}/{timestamp}.txt` |
| bbox 裁剪图 | `source-data` | `crops/{task_id}/{item_index}.jpg` |
| 导出 JSONL | `finetune-export` | `export/{batchUuid}.jsonl` |
---
## 决策 9测试策略
**决策**: 集成测试使用 Testcontainers真实 PG + Redis不允许 Mock 数据库
**必须覆盖的测试场景**:
1. **并发任务领取**10 线程同时争抢同一任务,验证恰好 1 人成功Redis + DB 双重锁)
2. **视频回调幂等**:同一 jobId 两次成功回调,验证只创建 1 个 annotation_task
3. **状态机越界拒绝**:非法状态转换(如 APPROVED → IN_PROGRESS抛出 BusinessException
4. **多租户隔离**:公司 A 身份访问公司 B 资源,验证被拒绝
5. **Shiro 过滤器链**:无 Token → 401Token 有效但角色不足 → 403
---
## 无需澄清事项汇总
| 项目 | 状态 | 来源 |
|------|------|------|
| 认证方案 | ✅ 已确定UUID Token | 宪章原则三 |
| 数据库选型 | ✅ 已确定PostgreSQL | 宪章原则一 |
| ORM | ✅ 已确定MyBatis Plus | 宪章原则一 |
| 缓存/锁 | ✅ 已确定Redis | 宪章原则一 |
| 对象存储 | ✅ 已确定RustFS S3 | 宪章原则一 |
| AI 集成方式 | ✅ 已确定HTTP RestClient | 宪章原则一 |
| 多租户隔离 | ✅ 已确定ThreadLocal + Interceptor | 宪章原则二 |
| 并发控制 | ✅ 已确定(双重锁) | 宪章原则七 |
| 审批事务边界 | ✅ 已确定(@TransactionalEventListener | 宪章原则五 |
| 测试策略 | ✅ 已确定Testcontainers | 宪章开发工作流 |