Phase 0:research.md(10项技术决策,无需澄清项) Phase 1:data-model.md(11张表+Redis结构),contracts/(8个模块API契约),quickstart.md(Docker Compose启动+流水线验证) plan.md:宪章11条全部通过,项目结构确认
6.5 KiB
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,防止线程池复用时数据串漏
备选方案放弃理由:
- 行级安全(RLS):PostgreSQL 原生支持,但与 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:对象存储路径规范
决策: RustFS(S3 兼容),文件字节流禁止入库,路径按资源类型分桶分目录
路径规范:
| 资源 | 桶 | 路径格式 |
|---|---|---|
| 文本文件 | 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 数据库
必须覆盖的测试场景:
- 并发任务领取:10 线程同时争抢同一任务,验证恰好 1 人成功(Redis + DB 双重锁)
- 视频回调幂等:同一 jobId 两次成功回调,验证只创建 1 个 annotation_task
- 状态机越界拒绝:非法状态转换(如 APPROVED → IN_PROGRESS)抛出 BusinessException
- 多租户隔离:公司 A 身份访问公司 B 资源,验证被拒绝
- Shiro 过滤器链:无 Token → 401;Token 有效但角色不足 → 403
无需澄清事项汇总
| 项目 | 状态 | 来源 |
|---|---|---|
| 认证方案 | ✅ 已确定(UUID Token) | 宪章原则三 |
| 数据库选型 | ✅ 已确定(PostgreSQL) | 宪章原则一 |
| ORM | ✅ 已确定(MyBatis Plus) | 宪章原则一 |
| 缓存/锁 | ✅ 已确定(Redis) | 宪章原则一 |
| 对象存储 | ✅ 已确定(RustFS S3) | 宪章原则一 |
| AI 集成方式 | ✅ 已确定(HTTP RestClient) | 宪章原则一 |
| 多租户隔离 | ✅ 已确定(ThreadLocal + Interceptor) | 宪章原则二 |
| 并发控制 | ✅ 已确定(双重锁) | 宪章原则七 |
| 审批事务边界 | ✅ 已确定(@TransactionalEventListener) | 宪章原则五 |
| 测试策略 | ✅ 已确定(Testcontainers) | 宪章开发工作流 |