# 任务清单:label_backend 知识图谱智能标注平台 **输入**: `/specs/001-label-backend-spec/` 全部设计文档 **前置条件**: plan.md ✅ | spec.md ✅ | research.md ✅ | data-model.md ✅ | contracts/ ✅ | quickstart.md ✅ ## 格式说明 - **[P]**: 可并行执行(不同文件,无未完成任务的依赖) - **[USn]**: 对应 spec.md 中的用户故事编号 - 每条任务包含精确的文件路径 --- ## Phase 1: 项目初始化 **目标**: 创建 Maven 项目骨架、基础配置和 Docker 环境 - [ ] T001 创建 Maven 项目骨架(`com.label` GroupId,`label-backend` ArtifactId,Java 17 编译目标) - [ ] T002 配置 `pom.xml`(Spring Boot 3、Apache Shiro 1.13.x、MyBatis Plus 3.5.x、Spring Data Redis、AWS S3 SDK v2、Testcontainers、Lombok) - [ ] T003 [P] 创建 `sql/init.sql`(按依赖顺序建全部 11 张表:sys_company → sys_user → source_data → annotation_task → annotation_result → training_dataset → export_batch → sys_config → sys_operation_log → annotation_task_history → video_process_job;含所有索引和初始配置数据) - [ ] T004 [P] 创建 `docker-compose.yml`(postgres、redis、rustfs、backend、ai-service、frontend 六个服务,含健康检查)和后端 `Dockerfile`(eclipse-temurin:17-jre-alpine) - [ ] T005 创建 `src/main/resources/application.yml`(数据源、Redis、RustFS、AI 服务 base-url、Shiro 相关配置项) **检查点**: Maven 编译通过(`mvn compile`),Docker Compose `up -d` 全部服务健康 --- ## Phase 2: 公共基础设施(阻塞性前置条件) **目标**: 所有业务模块依赖的公共组件。**必须全部完成后用户故事阶段才能开始** **⚠️ 重要**: 此阶段未完成前任何用户故事均不可开始实现 - [ ] T006 创建 `Result`、`ResultCode`、`PageResult` — `src/main/java/com/label/common/result/`(统一响应格式:`{"code":"SUCCESS","data":{...}}`) - [ ] T007 [P] 创建 `BusinessException`(含 `code`、`message`、`httpStatus`)和 `GlobalExceptionHandler`(`@RestControllerAdvice`)— `src/main/java/com/label/common/exception/` - [ ] T008 [P] 创建 `CompanyContext`(ThreadLocal,含 `set/get/clear` 三个方法,clear 必须在 finally 块调用)— `src/main/java/com/label/common/context/CompanyContext.java` - [ ] T009 创建 `RedisKeyManager`(三个静态方法:`tokenKey`、`userPermKey`、`taskClaimKey`)和 `RedisService` — `src/main/java/com/label/common/redis/` - [ ] T010 创建 MyBatis Plus 配置类 `MybatisPlusConfig`,注册 `TenantLineInnerInterceptor`(从 `CompanyContext` 获取 `companyId` 自动注入 WHERE 子句;`sys_company`、`sys_config` 加入忽略表列表)— `src/main/java/com/label/common/config/MybatisPlusConfig.java` - [ ] T011 创建 `StateValidator`(`assertTransition` 泛型方法,违规时抛出 `BusinessException("INVALID_STATE_TRANSITION",...)`)— `src/main/java/com/label/common/statemachine/StateValidator.java` - [ ] T012 [P] 创建 `SourceStatus` 枚举(PENDING/PREPROCESSING/EXTRACTING/QA_REVIEW/APPROVED,含 TRANSITIONS Map)— `src/main/java/com/label/common/statemachine/SourceStatus.java` - [ ] T013 [P] 创建 `TaskStatus` 枚举(UNCLAIMED/IN_PROGRESS/SUBMITTED/APPROVED/REJECTED,含 TRANSITIONS Map,含 IN_PROGRESS→IN_PROGRESS 用于 ADMIN 强制转移)— `src/main/java/com/label/common/statemachine/TaskStatus.java` - [ ] T014 [P] 创建 `DatasetStatus` 枚举(PENDING_REVIEW/APPROVED/REJECTED,含 TRANSITIONS Map)— `src/main/java/com/label/common/statemachine/DatasetStatus.java` - [ ] T015 [P] 创建 `VideoJobStatus` 枚举(PENDING/RUNNING/SUCCESS/FAILED/RETRYING,含 TRANSITIONS Map,注释说明 FAILED→PENDING 由 ADMIN 手动触发)— `src/main/java/com/label/common/statemachine/VideoJobStatus.java` - [ ] T016 创建 `@OperationLog` 注解(`type` 和 `targetType` 两个属性,`@Around` 级别)— `src/main/java/com/label/common/aop/OperationLog.java` - [ ] T017 创建 `AuditAspect`(`@Around("@annotation(operationLog)")`,在 finally 块以独立操作写入 `sys_operation_log`;审计写入失败只记录 error 日志,禁止抛出异常回滚业务)— `src/main/java/com/label/common/aop/AuditAspect.java` - [ ] T018 [P] 创建 `RustFsClient`(AWS S3 SDK v2 封装,endpoint 指向 RustFS;实现 `upload`、`download`、`delete`、`getPresignedUrl`)— `src/main/java/com/label/common/storage/RustFsClient.java` - [ ] T019 [P] 创建 `AiServiceClient`(`RestClient` 封装,8 个端点:`extractText`、`extractImage`、`extractFrames`、`videoToText`、`genTextQa`、`genImageQa`、`startFinetune`、`getFinetuneStatus`)— `src/main/java/com/label/common/ai/AiServiceClient.java` - [ ] T020 创建 Shiro 三件套:`TokenFilter`(解析 `Authorization: Bearer {uuid}`,查 Redis `token:{uuid}`,注入 `CompanyContext`,请求结束 finally 清理 ThreadLocal)、`UserRealm`(先查 Redis `user:perm:{userId}` TTL 5min,未命中查 PG;含 `addInheritedRoles`)、`ShiroConfig`(过滤器链:`/api/auth/login` → `anon`,`/api/**` → `tokenFilter`)— `src/main/java/com/label/common/shiro/` - [ ] T021 创建 `AbstractIntegrationTest`(Testcontainers,启动真实 PostgreSQL + Redis 容器,执行 sql/init.sql,注入测试用的公司和用户数据)— `src/test/java/com/label/AbstractIntegrationTest.java` - [ ] T022 集成测试:`ShiroFilterIntegrationTest`(无 Token → 401;有效 Token 但角色不足 → 403;有效 Token 且角色满足 → 200)— `src/test/java/com/label/integration/ShiroFilterIntegrationTest.java` - [ ] T023 单元测试:`StateMachineTest`(验证所有枚举的合法转换通过;非法转换抛出 `BusinessException("INVALID_STATE_TRANSITION")`)— `src/test/java/com/label/unit/StateMachineTest.java` **检查点**: 基础设施就绪,所有 Phase 3+ 的用户故事可并行开始 --- ## Phase 3: 用户故事 1 — 用户登录与身份认证(优先级: P1)🎯 MVP **目标**: 用户可以用用户名和密码登录,获得会话凭证,使用凭证访问受保护接口,退出后凭证立即失效 **独立测试**: 登录 → 获取 Token → 访问 `/api/auth/me` 返回用户信息 → 退出 → 再次访问返回 401 - [ ] T024 [P] [US1] 创建 `SysCompany` 实体(MyBatis Plus `@TableName`)和 `SysCompanyMapper` — `src/main/java/com/label/module/user/entity/SysCompany.java` + `mapper/SysCompanyMapper.java` - [ ] T025 [P] [US1] 创建 `SysUser` 实体(`passwordHash` 字段加 `@JsonIgnore`)和 `SysUserMapper`(含 `selectByCompanyAndUsername` 方法)— `src/main/java/com/label/module/user/entity/SysUser.java` + `mapper/SysUserMapper.java` - [ ] T026 [US1] 实现 `AuthService`:`login()`(BCrypt 校验密码 → UUID v4 Token → Redis Hash 存储 userId/role/companyId/username → 设置 TTL = `token_ttl_seconds` 配置值);`logout()`(删除 Redis Token Key)— `src/main/java/com/label/module/user/service/AuthService.java` - [ ] T027 [US1] 实现 `AuthController`:`POST /api/auth/login`(`anon`,调用 `AuthService.login()`)、`POST /api/auth/logout`(已登录)、`GET /api/auth/me`(返回当前用户信息);所有响应用 `Result` 包装 — `src/main/java/com/label/module/user/controller/AuthController.java` - [ ] T028 [US1] 集成测试:正确密码登录返回 Token;Token 有效时 `/api/auth/me` 返回 200;主动退出后再访问返回 401;错误密码登录返回 401 — `src/test/java/com/label/integration/AuthIntegrationTest.java` **检查点**: US1 独立可测试 — 登录/退出流程完整可用 --- ## Phase 4: 用户故事 2 — 原始资料上传(优先级: P1) **目标**: 上传员可以上传文本/图片/视频,查询自己的资料列表;管理员可查看全公司资料 **独立测试**: 上传文本文件 → 列表查到 → 详情含预签名 URL → 管理员可删除 - [ ] T029 [P] [US2] 创建 `SourceData` 实体(含 `parentSourceId` 自引用字段)和 `SourceDataMapper`(含 `updateStatus` 方法)— `src/main/java/com/label/module/source/entity/SourceData.java` + `mapper/SourceDataMapper.java` - [ ] T030 [US2] 实现 `SourceService`:`upload()`(先 insert 获取 ID → 构造路径 → 上传 RustFS → 更新 filePath);`list()`(UPLOADER 按 `uploaderId` 过滤,ADMIN 不过滤,强制分页);`findById()`(含 15 分钟预签名 URL);`delete()`(仅 PENDING 状态可删,同步删 RustFS 文件)— `src/main/java/com/label/module/source/service/SourceService.java` - [ ] T031 [US2] 实现 `SourceController`(`POST /api/source/upload`、`GET /api/source/list`、`GET /api/source/{id}`、`DELETE /api/source/{id}`;`@RequiresRoles` 注解声明权限;所有响应 `Result` 包装)— `src/main/java/com/label/module/source/controller/SourceController.java` - [ ] T032 [US2] 集成测试:UPLOADER 上传文本/图片 → 列表仅返回自己的资料;ADMIN 查看列表返回全部;上传视频 → source_data 状态为 PENDING(视频预处理 Phase 9 覆盖);已进入流水线的资料删除返回 409 — `src/test/java/com/label/integration/SourceIntegrationTest.java` **检查点**: US2 独立可测试 — 上传/查询/删除流程完整可用 --- ## Phase 5: 用户故事 3+4 — 提取阶段标注与审批(优先级: P1) **目标**: 标注员可以领取任务(并发安全)、AI 辅助预标注、编辑并提交;审批员可以通过(自动触发 QA 任务)或驳回(标注员可重领) **独立测试**: 创建任务 → 标注员领取 → AI 预标注 → 提交 → 审批通过 → QA 任务自动出现在任务池 ### 实体与数据层 - [ ] T033 [P] [US3] 创建 `AnnotationTask` 实体 + `AnnotationTaskMapper`(含 `claimTask(taskId, userId, companyId)` 方法,SQL:`UPDATE ... SET status='IN_PROGRESS', claimed_by=?, claimed_at=NOW() WHERE id=? AND status='UNCLAIMED' AND company_id=?`,返回影响行数)— `src/main/java/com/label/module/task/entity/AnnotationTask.java` + `mapper/AnnotationTaskMapper.java` - [ ] T034 [P] [US3] 创建 `AnnotationTaskHistory` 实体 + `TaskHistoryMapper` — `src/main/java/com/label/module/task/entity/AnnotationTaskHistory.java` + `mapper/TaskHistoryMapper.java` - [ ] T035 [P] [US3] 创建 `AnnotationResult` 实体 + `AnnotationResultMapper`(含 `updateResultJson` 整体覆盖方法和 `selectByTaskId` 方法)— `src/main/java/com/label/module/annotation/entity/AnnotationResult.java` + `mapper/AnnotationResultMapper.java` ### 任务管理服务与控制器 - [ ] T036 [US3] 实现 `TaskClaimService.claim()`(① Redis `SET NX task:claim:{taskId}` TTL 30s,失败抛 `TASK_CLAIMED`;② DB `claimTask()` 影响行数为 0 时抛 `TASK_CLAIMED`;③ `insertHistory(UNCLAIMED→IN_PROGRESS)`)和 `unclaim()`(StateValidator + 清 Redis 锁 + 历史)和 `reclaim()`(校验 REJECTED + claimedBy = 当前用户 + REJECTED→IN_PROGRESS + 历史)— `src/main/java/com/label/module/task/service/TaskClaimService.java` - [ ] T037 [US3] 实现 `TaskService`(`createTask`、`getPool`(按角色过滤:ANNOTATOR→UNCLAIMED/EXTRACTION;REVIEWER→SUBMITTED)、`getMine`(含 IN_PROGRESS/SUBMITTED/REJECTED)、`getPendingReview`(SUBMITTED,分页)、`getById`、`reassign`(ADMIN,仅更新 claimedBy + 历史))— `src/main/java/com/label/module/task/service/TaskService.java` - [ ] T038 [US3] 实现 `TaskController`(10 个端点:`POST /api/tasks`、`GET /api/tasks/pool`、`POST /api/tasks/{id}/claim`、`POST /api/tasks/{id}/unclaim`、`GET /api/tasks/mine`、`POST /api/tasks/{id}/reclaim`、`GET /api/tasks/pending-review`、`GET /api/tasks/{id}`、`GET /api/tasks`、`PUT /api/tasks/{id}/reassign`)— `src/main/java/com/label/module/task/controller/TaskController.java` ### 提取标注服务与控制器 - [ ] T039 [US3] 实现 `ExtractionService.aiPreAnnotate()`(调用 `AiServiceClient.extractText/extractImage`,写入 `annotation_result`)和 `updateResult()`(整体覆盖 `result_json`,校验 JSON 格式)— `src/main/java/com/label/module/annotation/service/ExtractionService.java` - [ ] T040 [US3] 实现 `ExtractionService.submit()`(`@Transactional`:IN_PROGRESS→SUBMITTED + `submitted_at` + insertHistory)— `src/main/java/com/label/module/annotation/service/ExtractionService.java` - [ ] T041 [US4] 创建 `ExtractionApprovedEvent`(携带 `taskId`、`sourceId`、`sourceType`、`companyId`)— `src/main/java/com/label/module/annotation/event/ExtractionApprovedEvent.java` - [ ] T042 [US4] 实现 `ExtractionService.approve()`(`@Transactional`:① 自审校验;② `is_final=true`;③ SUBMITTED→APPROVED + `completedAt` + 历史;④ `publishEvent(ExtractionApprovedEvent)`;AI 调用禁止在此事务内执行)— `src/main/java/com/label/module/annotation/service/ExtractionService.java` - [ ] T043 [US4] 实现 `ExtractionApprovedEventListener`(`@TransactionalEventListener(AFTER_COMMIT)` + `@Transactional(REQUIRES_NEW)`:调用 AI 生成候选问答对 → 写 `training_dataset`(PENDING_REVIEW)→ 创建 QA_GENERATION 任务(UNCLAIMED)→ `source_data` 状态→ QA_REVIEW)— `src/main/java/com/label/module/annotation/service/ExtractionApprovedEventListener.java` - [ ] T044 [US4] 实现 `ExtractionService.reject()`(`@Transactional`:① 自审校验;② StateValidator;③ SUBMITTED→REJECTED + 历史)— `src/main/java/com/label/module/annotation/service/ExtractionService.java` - [ ] T045 [US4] 实现 `ExtractionController`(5 个端点:`GET /api/extraction/{taskId}`、`PUT /api/extraction/{taskId}`、`POST /api/extraction/{taskId}/submit`、`POST /api/extraction/{taskId}/approve`、`POST /api/extraction/{taskId}/reject`)— `src/main/java/com/label/module/annotation/controller/ExtractionController.java` ### 集成测试 - [ ] T046 [US3] 并发集成测试:10 个线程同时争抢同一 UNCLAIMED 任务,验证恰好 1 人成功、其余均收到 `TASK_CLAIMED` 错误、DB 中 `claimed_by` 唯一 — `src/test/java/com/label/integration/TaskClaimConcurrencyTest.java` - [ ] T047 [US4] 集成测试:审批通过 → QA 任务自动出现在任务池;自审返回 `SELF_REVIEW_FORBIDDEN` 403;驳回后标注员可重领并再次提交 — `src/test/java/com/label/integration/ExtractionApprovalIntegrationTest.java` **检查点**: US3+US4 独立可测试 — 完整提取流水线(领取→标注→提交→审批→QA任务自动创建)可用 --- ## Phase 6: 用户故事 5 — 问答生成阶段标注与审批(优先级: P2) **目标**: 标注员领取 QA 任务、修改候选问答对并提交;审批员通过后训练样本入库,整条流水线完成 **独立测试**: 领取 QA 任务 → 修改问答对 → 提交 → 审批通过 → training_dataset 状态 APPROVED,source_data 状态 APPROVED - [ ] T048 [P] [US5] 创建 `TrainingDataset` 实体 + `TrainingDatasetMapper`(含 `approveByTaskId`、`deleteByTaskId` 方法)— `src/main/java/com/label/module/annotation/entity/TrainingDataset.java` + `mapper/TrainingDatasetMapper.java` - [ ] T049 [US5] 实现 `QaService.updateResult()`(整体覆盖问答对 JSONB)和 `submit()`(`@Transactional`:IN_PROGRESS→SUBMITTED + 历史)— `src/main/java/com/label/module/annotation/service/QaService.java` - [ ] T050 [US5] 实现 `QaService.approve()`(`@Transactional`:① `validateAndGetTask` 先于一切 DB 写入;② 自审校验;③ `training_dataset` → APPROVED;④ `annotation_task` → APPROVED + 历史;⑤ `source_data` → APPROVED)— `src/main/java/com/label/module/annotation/service/QaService.java` - [ ] T051 [US5] 实现 `QaService.reject()`(`@Transactional`:① 自审校验;② `deleteByTaskId` 清除候选问答对;③ SUBMITTED→REJECTED + 历史;④ `source_data` 保持 QA_REVIEW 不变)— `src/main/java/com/label/module/annotation/service/QaService.java` - [ ] T052 [US5] 实现 `QaController`(5 个端点:`GET /api/qa/{taskId}`、`PUT /api/qa/{taskId}`、`POST /api/qa/{taskId}/submit`、`POST /api/qa/{taskId}/approve`、`POST /api/qa/{taskId}/reject`)— `src/main/java/com/label/module/annotation/controller/QaController.java` - [ ] T053 [US5] 集成测试:QA 审批通过 → `training_dataset.status = APPROVED`,`source_data.status = APPROVED`;QA 驳回 → 候选记录被删除,标注员可重领 — `src/test/java/com/label/integration/QaApprovalIntegrationTest.java` **检查点**: US5 独立可测试 — 完整 QA 流水线可用,training_dataset 产出验证通过 --- ## Phase 7: 用户故事 6 — 训练数据导出与微调提交(优先级: P2) **目标**: 管理员将已审批样本批量导出为 JSONL,并可提交 GLM 微调任务 **独立测试**: 选取已审批样本 → 创建批次 → RustFS 中存在 JSONL 文件 → 提交微调 → 可查询状态 - [ ] T054 [P] [US6] 创建 `ExportBatch` 实体 + `ExportBatchMapper` — `src/main/java/com/label/module/export/entity/ExportBatch.java` + `mapper/ExportBatchMapper.java` - [ ] T055 [US6] 实现 `ExportService.createBatch()`(`@Transactional`:① 校验全部样本为 APPROVED;② 生成 JSONL(每行一个 `glm_format_json`);③ 上传 RustFS `finetune-export/export/{batchUuid}.jsonl`;④ 批量更新 `export_batch_id`/`exported_at`;⑤ 插入 `export_batch` 记录)— `src/main/java/com/label/module/export/service/ExportService.java` - [ ] T056 [US6] 实现 `FinetuneService`:`trigger()`(调用 `AiServiceClient.startFinetune()`,更新 `glm_job_id` 和 `finetune_status = RUNNING`)和 `getStatus()`(调用 `AiServiceClient.getFinetuneStatus()`)— `src/main/java/com/label/module/export/service/FinetuneService.java` - [ ] T057 [US6] 实现 `ExportController`(`GET /api/training/samples`、`POST /api/export/batch`、`POST /api/export/{batchId}/finetune`、`GET /api/export/{batchId}/status`、`GET /api/export/list`;全部 `@RequiresRoles("ADMIN")`)— `src/main/java/com/label/module/export/controller/ExportController.java` - [ ] T058 [US6] 集成测试:成功创建批次后 JSONL 文件存在于 RustFS;包含非 APPROVED 样本时返回 `INVALID_SAMPLES` 400 — `src/test/java/com/label/integration/ExportIntegrationTest.java` **检查点**: US6 独立可测试 — 导出批次创建和微调提交流程可用 --- ## Phase 8: 用户故事 7 — 用户与权限管理(优先级: P2) **目标**: 管理员可以创建用户、变更角色(立即生效)、禁用账号(立即失效) **独立测试**: 创建标注员用户 → 验证其能领取任务 → 升为审批员 → 验证立即可以审批 → 禁用账号 → 已有 Token 立即失效 - [ ] T059 [US7] 实现 `UserService`:`createUser()`(BCrypt 哈希密码,强度因子 ≥ 10);`updateUser()`;`updateRole()`(DB 写入后立即 `redisTemplate.delete(userPermKey(userId))`);`updateStatus()`(禁用时删 Redis Token + 权限缓存)— `src/main/java/com/label/module/user/service/UserService.java` - [ ] T060 [US7] 实现 `UserController`(`GET /api/users`、`POST /api/users`、`PUT /api/users/{id}`、`PUT /api/users/{id}/status`、`PUT /api/users/{id}/role`;全部 `@RequiresRoles("ADMIN")`)— `src/main/java/com/label/module/user/controller/UserController.java` - [ ] T061 [US7] 集成测试:变更角色后权限下一次请求立即生效(无需重新登录);禁用账号后现有 Token 下一次请求立即返回 401 — `src/test/java/com/label/integration/UserManagementIntegrationTest.java` **检查点**: US7 独立可测试 — 用户管理和即时权限变更可用 --- ## Phase 9: 用户故事 8 — 视频处理与系统配置(优先级: P3) **目标**: 上传视频后触发异步预处理(帧提取/转文字);AI 回调幂等处理;管理员可配置 Prompt 模板等系统参数 **独立测试(视频)**: 上传视频 → 创建处理任务 → 模拟成功回调 → annotation_task 出现在任务池;重复成功回调 → 任务数量不增加 **独立测试(配置)**: 为公司设置专属 Prompt → 验证该公司使用新值;其他公司使用全局默认 - [ ] T062 [P] [US8] 创建 `VideoProcessJob` 实体 + `VideoProcessJobMapper` — `src/main/java/com/label/module/video/entity/VideoProcessJob.java` + `mapper/VideoProcessJobMapper.java` - [ ] T063 [P] [US8] 创建 `SysConfig` 实体 + `SysConfigMapper`(含 `selectByCompanyAndKey(companyId, configKey)` 方法,支持 `companyId IS NULL` 查询)— `src/main/java/com/label/module/config/entity/SysConfig.java` + `mapper/SysConfigMapper.java` - [ ] T064 [US8] 实现 `VideoProcessService`:`createJob()`(`@Transactional`:`source_data.status → PREPROCESSING` + 插入 job + 触发 AI 异步调用);`handleCallback()`(`@Transactional`:幂等检查 status==SUCCESS 则 return;成功 → SUCCESS + `source_data.status → PENDING`;失败 → 按 retry_count 决定 RETRYING 或 FAILED);`reset()`(FAILED → PENDING)— `src/main/java/com/label/module/video/service/VideoProcessService.java` - [ ] T065 [US8] 实现 `VideoController`(`POST /api/video/process`、`GET /api/video/jobs/{jobId}`、`POST /api/video/jobs/{jobId}/reset`、`POST /api/video/callback`(内部接口,IP 白名单或服务密钥保护))— `src/main/java/com/label/module/video/controller/VideoController.java` - [ ] T066 [US8] 实现 `SysConfigService.get(configKey)`(先按 `(companyId, key)` 查;未命中按 `(NULL, key)` 查全局默认)和 `update(key, value)`(UPSERT:公司级配置不存在则创建,存在则覆盖)— `src/main/java/com/label/module/config/service/SysConfigService.java` - [ ] T067 [US8] 实现 `SysConfigController`(`GET /api/config`(合并公司级 + 全局,标注 scope)、`PUT /api/config/{key}`;均 `@RequiresRoles("ADMIN")`)— `src/main/java/com/label/module/config/controller/SysConfigController.java` - [ ] T068 [US8] 集成测试:同一 jobId 两次成功回调,`annotation_task` 记录数为 1(幂等);达最大重试次数后 status = FAILED — `src/test/java/com/label/integration/VideoCallbackIdempotencyTest.java` - [ ] T069 [US8] 集成测试:公司级配置覆盖同 Key 的全局默认;其他公司读取全局默认 — `src/test/java/com/label/integration/SysConfigIntegrationTest.java` **检查点**: US8 独立可测试 — 视频处理幂等和配置管理可用 --- ## Phase 10: 收尾与横切关注点 **目标**: 多租户隔离验证、整体合规检查、快速启动验证 - [ ] T070 集成测试:`MultiTenantIsolationTest`(公司 A 身份查询公司 B 的资料/任务 → 返回空列表或 404,不泄露数据)— `src/test/java/com/label/integration/MultiTenantIsolationTest.java` - [ ] T071 [P] 代码审查:检查所有 Controller 方法返回值均为 `Result` 或 `Result>`,无裸 POJO 或裸 List 返回 - [ ] T072 [P] 代码审查:检查所有列表查询方法均含分页参数(`page`/`pageSize`),无 `selectAll()` 或不分页的查询 - [ ] T073 [P] 代码审查:检查 `sys_operation_log` 相关代码,确认应用层零处 UPDATE 或 DELETE - [ ] T074 [P] 代码审查:检查所有 `@Transactional` 方法内无 `AiServiceClient` 的同步 HTTP 调用(审批触发 AI 必须通过 `@TransactionalEventListener`) - [ ] T075 运行 `quickstart.md` 端到端验证:`docker compose up -d` → 登录 → 上传文件 → 创建任务 → 领取 → 提交 → 审批通过 → 确认 QA 任务出现 --- ## 依赖关系与执行顺序 ### 阶段依赖 ``` Phase 1(初始化) ↓ Phase 2(基础设施)[全部完成后解锁所有用户故事] ↓ Phase 3(US1 认证) ← 可与 Phase 4/5/6/7/8/9 并行 Phase 4(US2 上传) ← 依赖 Phase 2,独立于其他用户故事 Phase 5(US3+4 提取) ← 依赖 Phase 2(上传已有资料的集成测试依赖 US2) Phase 6(US5 QA) ← 依赖 Phase 5 完成(QA 任务由提取审批自动创建) Phase 7(US6 导出) ← 依赖 Phase 6 完成(需要 APPROVED 的 training_dataset) Phase 8(US7 用户管理) ← 依赖 Phase 3(UserService 在 AuthService 基础上扩展) Phase 9(US8 视频+配置) ← 依赖 Phase 2,其余独立 ↓ Phase 10(收尾) ``` ### 用户故事间依赖 - **US1(认证)**: 仅依赖 Phase 2,完全独立 - **US2(上传)**: 仅依赖 Phase 2,完全独立 - **US3+4(提取)**: 依赖 Phase 2;集成测试中使用已上传资料需 US2 - **US5(QA)**: 依赖 US3+4(QA 任务来源于提取阶段审批通过的级联触发) - **US6(导出)**: 依赖 US5(需要 APPROVED 状态的 training_dataset) - **US7(用户管理)**: 依赖 US1(UserService 扩展 AuthService 的用户实体) - **US8(视频+配置)**: 仅依赖 Phase 2 ### 阶段内并行机会 - Phase 2:T007-T010、T012-T015、T018-T019 均可并行(独立文件) - Phase 3:T024、T025 可并行(独立文件) - Phase 5:T033、T034、T035 可并行(独立文件) - Phase 9:T062、T063 可并行(独立文件) - Phase 10:T071-T074 全部可并行(仅代码审查,无文件修改) --- ## 并行执行示例 ### Phase 2 基础设施并行 ``` 同时启动: 任务: "创建 BusinessException、GlobalExceptionHandler — common/exception/" [T007] 任务: "创建 CompanyContext(ThreadLocal)— common/context/" [T008] 任务: "创建 RustFsClient — common/storage/" [T018] 任务: "创建 AiServiceClient — common/ai/" [T019] 任务: "创建 SourceStatus 枚举" [T012] 任务: "创建 TaskStatus 枚举" [T013] ``` ### Phase 5 提取阶段并行 ``` 同时启动(实体/Mapper): 任务: "创建 AnnotationTask 实体 + Mapper" [T033] 任务: "创建 AnnotationTaskHistory 实体 + Mapper" [T034] 任务: "创建 AnnotationResult 实体 + Mapper" [T035] ``` --- ## 实施策略 ### MVP 优先(仅用户故事 1) 1. 完成 Phase 1(初始化) 2. 完成 Phase 2(基础设施)— **关键,阻塞所有故事** 3. 完成 Phase 3(US1 认证) 4. **停止并验证**: 登录/退出/权限校验全流程可用 5. 可以独立部署演示认证功能 ### 增量交付 1. Phase 1 + Phase 2 → 基础就绪 2. Phase 3(US1)→ 验证 → 演示(MVP) 3. Phase 4(US2)→ 验证 → 演示(上传功能) 4. Phase 5(US3+4)→ 验证 → 演示(标注流程) 5. Phase 6(US5)→ 验证 → 演示(完整双阶段流水线) 6. Phase 7(US6)→ 验证 → 演示(训练数据产出) 7. Phase 8+9 → 验证 → 演示(完整平台) 8. Phase 10 → 收尾 ### 多人协作策略 Phase 2 完成后: - 开发者 A:Phase 3(US1 认证)+ Phase 8(US7 用户管理) - 开发者 B:Phase 4(US2 上传)+ Phase 5(US3+4 提取) - 开发者 C:Phase 9(US8 视频+配置) Phase 5 完成后: - 开发者 A/B 合力:Phase 6(US5 QA)→ Phase 7(US6 导出) --- ## 说明 - `[P]` 任务 = 不同文件,无依赖,可并行 - `[USn]` 标签将任务映射到具体用户故事,便于追踪 - 每个用户故事应独立可完成和可测试 - 每完成一个阶段后提交 git commit - 在每个检查点停下来独立验证该用户故事 - 避免:模糊任务、同文件并发冲突、破坏独立性的跨故事依赖