# 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 数据库 **必须覆盖的测试场景**: 1. **并发任务领取**:10 线程同时争抢同一任务,验证恰好 1 人成功(Redis + DB 双重锁) 2. **视频回调幂等**:同一 jobId 两次成功回调,验证只创建 1 个 annotation_task 3. **状态机越界拒绝**:非法状态转换(如 APPROVED → IN_PROGRESS)抛出 BusinessException 4. **多租户隔离**:公司 A 身份访问公司 B 资源,验证被拒绝 5. **Shiro 过滤器链**:无 Token → 401;Token 有效但角色不足 → 403 --- ## 无需澄清事项汇总 | 项目 | 状态 | 来源 | |------|------|------| | 认证方案 | ✅ 已确定(UUID Token) | 宪章原则三 | | 数据库选型 | ✅ 已确定(PostgreSQL) | 宪章原则一 | | ORM | ✅ 已确定(MyBatis Plus) | 宪章原则一 | | 缓存/锁 | ✅ 已确定(Redis) | 宪章原则一 | | 对象存储 | ✅ 已确定(RustFS S3) | 宪章原则一 | | AI 集成方式 | ✅ 已确定(HTTP RestClient) | 宪章原则一 | | 多租户隔离 | ✅ 已确定(ThreadLocal + Interceptor) | 宪章原则二 | | 并发控制 | ✅ 已确定(双重锁) | 宪章原则七 | | 审批事务边界 | ✅ 已确定(@TransactionalEventListener) | 宪章原则五 | | 测试策略 | ✅ 已确定(Testcontainers) | 宪章开发工作流 |