151 lines
6.5 KiB
Markdown
151 lines
6.5 KiB
Markdown
|
|
# 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) | 宪章开发工作流 |
|