# 标注平台后端设计方案 ## 1. 文档目标 本文档只覆盖一期 Java Backend 需要落地的模块,不扩展到训练集导出、微调任务编排二期能力。 ## 2. 一期范围 ### 2.1 一期要落地的模块 一期后端只建设以下四个业务模块: 1. 资源管理模块 `source_resource` 2. 任务管理模块 `annotation_task` 3. 审批标注结果模块 `annotation_result` / `annotation_result_history` 4. 系统配置模块 `sys_config` 一期继续复用现有认证鉴权基线: - 用户登录、Token、会话版本控制沿用现有认证实现 - 岗位权限沿用 `position` - 数据权限沿用 `role` - 租户隔离沿用 `company_id` ### 2.2 一期不做的内容 以下内容不纳入本次一期后端设计范围: - 新建部门体系 - 新建复杂工作流引擎 - 训练集导出中心的完整业务闭环 - 面向前台的复杂配置编排 UI - AI Service 内部推理链路实现细节 ## 3. 总体架构 ### 3.1 系统组成 一期采用“四段式协作”: 1. `lb_backend`:对前端暴露 REST API,负责资源、任务、结果审核、系统配置。 2. `PostgreSQL`:持久化元数据、任务快照、审核结果、归档结果。 3. `RustFS`:保存原始资源、视频衍生文件、外置问答 JSON、导出文件。 4. `ai-service`:轮询待执行任务,拉取资源,完成抽取、校验、比对,并写回结果。 ### 3.2 核心设计原则 1. 资源生命周期、任务执行生命周期、审核归档生命周期分开建模。 2. 大字段优先支持表内存储,但必须预留外置存储能力。 3. 后端创建任务时只负责写入 `PENDING`;执行态状态全部由 `ai-service` 驱动。 4. `annotation_result` 只保存“当前待处理结果”;归档后的最终结果统一进入 `annotation_result_history`。 5. 运行态结果不再依赖 `review_status`,而是通过 `requires_manual_review`、`is_deleted` 和历史归档记录组合表达状态。 ### 3.3 一期包结构 一期包结构按 [微服务开发规范文档](D:/workspace/labeling/docs/微服务开发规范文档.md) 采用“按技术层分包”的标准结构,不再使用 `modules/resource`、`modules/task` 这类按大模块纵向拆包的目录形式: ```text lb_backend/src/main/java/com/labelsys/backend ├── annotation ├── common │ ├── exception │ ├── Result.java │ └── ResultCode.java ├── config ├── constant ├── context ├── controller │ ├── AnnotationResultController.java │ ├── AnnotationTaskController.java │ ├── SourceResourceController.java │ └── SysConfigController.java ├── dto │ ├── common │ │ ├── ApiResponse.java │ │ └── PageResult.java │ ├── request │ │ ├── SourceUploadRequest.java │ │ ├── SourceResourcePageQuery.java │ │ ├── ManualModelConfigRequest.java │ │ ├── TaskModelConfigRequest.java │ │ ├── PromptConfigOptionRequest.java │ │ ├── CreateAnnotationTaskRequest.java │ │ ├── UpdateAnnotationTaskRequest.java │ │ ├── AnnotationTaskPageQuery.java │ │ ├── AnnotationResultPageQuery.java │ │ ├── MergeReviewResultRequest.java │ │ ├── SaveSysConfigRequest.java │ │ └── SysConfigPageQuery.java │ └── response │ ├── SourceUploadResponse.java │ ├── SourceResourceResponse.java │ ├── TaskModelConfigResponse.java │ ├── TaskPromptConfigResponse.java │ ├── AnnotationResultResponse.java │ ├── AnnotationResultCompareResponse.java │ ├── AnnotationTaskResponse.java │ ├── MergeReviewResultResponse.java │ └── SysConfigResponse.java ├── entity ├── mapper ├── scheduled ├── service ├── typehandler └── util ``` 补充约定: 1. 控制器、DTO、实体、Mapper、Service 统一放在规范文档要求的根层目录下,业务边界通过类名前缀区分,例如 `AnnotationTaskController`、`SourceResourceMapper`。 2. 定时任务统一进入 `scheduled` 包,不再在业务模块下单独定义 `job` 子目录。 3. Mapper XML 统一放在 `src/main/resources/mapper`,SQL 基准文件统一放在 `src/main/resources/sql`。 4. DTO 进一步细分为 `dto/request`、`dto/response` 和 `dto/common`,避免不同模块的交互对象混放。 ### 3.4 接口对象与数据库实体分层 1. 数据库实体仅用于 Repository / Mapper 层持久化,统一命名为 `*Entity`,不直接作为 Controller 入参或出参。 2. Controller 层只接收 `*Request`、`*Query`,只返回 `*Response`、`*View`、`PageResult`。 3. Service 层负责实体转换与聚合装配,例如任务详情需要把 `annotation_task`、`annotation_task_resource`、`sys_config` 组合成前端可直接消费的交互对象。 4. API 字段命名遵循 Java 驼峰风格,例如数据库列 `is_deleted` 在接口层统一映射为 `isDeleted`。 5. 敏感字段不直接透出到界面层,模型 `apiKey` 只在任务快照和配置表中保存,接口返回时必须脱敏或不返回。 ## 4. 权限模型 ### 4.1 岗位权限 一期沿用现有岗位枚举: - `ANNOTATOR` - `DATA_TRAINER` - `REVIEWER` - `ADMIN` 权限建议如下: | 模块 | 查询 | 新增/修改 | 删除 | 特殊说明 | |---|---|---|---|---| | 资源管理 | 全岗位可查 | 全岗位可上传 | 全岗位可删自身可见资源 | 删除时仍受数据权限约束 | | 任务管理 | 全岗位可查 | 全岗位可创建 | 全岗位可删未执行任务 | backend 只创建 `PENDING` | | 结果查询 | 全岗位可查可见数据 | 无 | 无 | 仅查询本人/本层级可见数据 | | 人工审核 | `REVIEWER` 及以上 | `REVIEWER` 及以上 | 无 | 对所有未归档运行态结果开放;其中 `requires_manual_review = true` 的结果必须审核后才能归档 | | 系统配置 | 全岗位可查 | `ADMIN` 建议可写 | 无 | 若沿需求“所有岗位可操作”,需由产品再次确认风险 | 说明:需求原文提到“系统配置模块对所有岗位可以操作”,这在安全上风险较高。本文档保留查询全开放,但建议写权限收敛到 `ADMIN`。如果必须全岗位可写,则需额外增加配置白名单和审计。 ### 4.2 数据权限 一期沿用现有数据权限角色: - `EMPLOYEE`:只看自己创建的数据 - `MANAGER`:看本公司 `EMPLOYEE + MANAGER` - `ENGINEER`:看本公司全部数据 业务表统一保留: - `company_id` - `creator_id` - `creator_role` ### 4.3 权限落地规则 1. 系统表以 `company_id` 作为租户边界。 2. 业务列表接口默认按 `company_id` 过滤。 3. 再按 `creator_role + creator_id` 追加数据权限过滤。 4. 审核类写操作额外校验岗位是否为 `REVIEWER` 或 `ADMIN`。 ## 5. 领域对象与状态归属 ### 5.1 领域对象 一期核心领域对象如下: 1. `SourceResource`:原始资源元数据。 2. `AnnotationTask`:某一次抽取/校验任务快照。 3. `AnnotationTaskResource`:任务与资源的多对多绑定关系。 4. `AnnotationResult`:当前待审核或待自动归档的运行态结果。 5. `AnnotationResultHistory`:归档后的最终结果。 6. `SysConfig`:模型、Prompt、系统参数配置。 ### 5.2 状态归属评审结论 旧方案里 `source_data` 参考设计把资源、抽取、审核状态揉在一张表里,这不适合一期真实落地。重写后按以下原则拆分: | 责任对象 | 负责状态 | 不负责状态 | |---|---|---| | `source_resource` | 文件是否上传完成、是否已归档 | 任务执行进度、审核结果 | | `annotation_task` | AI 执行进度、失败原因、开始结束时间 | 资源存储状态、人工审核结论 | | `annotation_task_resource` | 任务与资源的绑定关系 | 任务执行进度、审核结论 | | `annotation_result` | 是否强制人工审核、是否软删除、审核人和审核时间 | 任务执行进度 | | `annotation_result_history` | 最终归档结果、归档原因、归档人 | 运行态处理中的瞬时状态 | ### 5.3 一期状态定义 #### 5.3.1 资源状态 `source_status` 当前 SQL 中资源状态仍按以下值设计: - `UPLOADED`:原始文件已上传,元数据已入库 - `PROCESSING`:预留给后续异步资源处理流程,一期暂不引入专门视频预处理作业 - `READY`:资源可用于创建任务 - `ARCHIVED`:资源已归档,不再允许创建新任务 不建议把 `EXTRACTING`、`QA_REVIEW`、`APPROVED`、`REJECTED` 放进资源表,因为这些属于任务或结果生命周期。 #### 5.3.2 任务状态 `task_status` 当前 SQL 中任务状态使用: - `PENDING` - `RUNNING` - `COMPLETED` - `FAILED` #### 5.3.3 结果运行态 `annotation_result` 不再保存 `review_status`。运行态按下列规则推导: | 运行态名称 | 计算规则 | |---|---| | `MANUAL_REVIEW_PENDING` | `requires_manual_review = true and is_deleted = false`,表示必须人工审核,禁止自动归档 | | `AUTO_ARCHIVE_PENDING` | `requires_manual_review = false and is_deleted = false`,表示允许人工审核,也允许超时自动归档 | | `ARCHIVED` | 对应记录已软删除,且已有 `annotation_result_history` 归档记录 | ## 6. 对象存储路径规范 ### 6.1 评审结论 旧参考路径存在三个问题: 1. 直接把业务含义写死在一级目录,后续跨租户清理困难。 2. 部分路径使用原始文件名,不利于重名控制和脱敏。 3. 原始资源、衍生资源、审核外置文件、导出文件没有统一前缀策略。 ### 6.2 一期统一规范 统一规则: 1. 路径全部使用小写。 2. 不直接使用用户原始文件名做对象 Key。 3. 优先使用业务主键和年月做路径分层。 4. 按“租户 / 资源域 / 类型 / 年月 / 业务 ID”组织。 ### 6.3 Bucket 规划 | Bucket | 用途 | |---|---| | `source-data` | 原始文本、图片、视频及视频衍生物 | | `annotation-artifacts` | 外置问答 JSON、diff JSON、人工审核快照 | | `finetune-export` | 导出 JSONL 文件 | ### 6.4 路径格式 | 场景 | Bucket | 路径格式 | |---|---|---| | 文本原始文件 | `source-data` | `source/{companyId}/text/{yyyyMM}/{resourceId}/original.{ext}` | | 图片原始文件 | `source-data` | `source/{companyId}/image/{yyyyMM}/{resourceId}/original.{ext}` | | 视频原始文件 | `source-data` | `source/{companyId}/video/{yyyyMM}/{resourceId}/original.{ext}` | | 外置问答 JSON | `annotation-artifacts` | `result/{companyId}/{yyyyMM}/{taskId}/{resultId}/qa.json` | | 外置 diff JSON | `annotation-artifacts` | `result/{companyId}/{yyyyMM}/{taskId}/{resultId}/diff.json` | | 导出 JSONL | `finetune-export` | `export/{companyId}/{yyyyMM}/{batchNo}/dataset.jsonl` | ### 6.5 命名规则 1. `resourceId`、`taskId`、`resultId` 使用数据库主键或雪花 ID。 2. `yyyyMM` 例如 `202604`。 3. 文件扩展名按后端识别后的标准扩展名写入,不沿用用户大小写。 4. 资源删除时优先删对象,再更新数据库状态。 ## 7. source 场景流程与状态流转/回退 ### 7.1 文本/图片上传场景 流程: 1. 前端上传文件到后端。 2. 后端完成文件校验和对象存储上传。 3. 后端创建 `source_resource`,状态写入 `UPLOADED`。 4. 当前一期不引入视频预处理作业,资源校验通过后直接置为 `READY`。 状态流转: `UPLOADED -> READY -> ARCHIVED` 回退规则: - 如果对象存储上传失败:不写库。 - 如果写库失败:回滚并删除已上传对象。 - 如果资源已被任务引用,不允许直接物理删除,只允许逻辑归档到 `ARCHIVED`。 ### 7.2 视频资源当前处理策略 一期暂不建设独立的视频资源预处理作业,视频资源和文本/图片资源保持同样的主流程: 1. 视频上传成功后创建 `source_resource`,状态为 `UPLOADED`。 2. 完成基础校验后直接改为 `READY`。 3. 当前不做拆帧、转写、衍生文件生产。 状态流转: `UPLOADED -> READY -> ARCHIVED` ### 7.3 任务驱动场景 资源与任务解耦,资源不会因为某个任务开始执行就改成任务态。即: - 创建任务不会把资源状态从 `READY` 改成 `EXTRACTING` - 任务重跑不会污染资源主状态 这是本次重写的核心结论之一。 ## 8. 模块详细设计 ## 8.1 资源管理模块 ### 8.1.1 模块职责 1. 接收文本、图片、视频文件上传。 2. 保存资源元数据与对象存储定位信息。 3. 提供分页查询、详情查询、删除能力。 4. 为任务创建与结果审核提供统一的资源视图,不引入独立视频预处理作业。 ### 8.1.2 接口列表 | 方法 | 路径 | 说明 | |---|---|---| | `POST` | `/api/source-resources/upload` | 上传资源 | | `GET` | `/api/source-resources` | 分页查询资源 | | `GET` | `/api/source-resources/{id}` | 查询资源详情 | | `DELETE` | `/api/source-resources/{id}` | 删除资源 | ### 8.1.3 接口交互对象 以下对象均为接口层 DTO,不直接复用数据库实体;列表和详情统一返回 `SourceResourceResponse`,保持前端交互对象一致: ```java public class SourceUploadRequest { private String resourceName; private String resourceType; // TEXT / IMAGE / VIDEO private String remark; } public class SourceUploadResponse { private Long id; private String resourceName; private String resourceType; private String bucketName; private String filePath; private Long fileSize; private String sourceStatus; private LocalDateTime createdAt; } public class SourceResourcePageQuery { private String keyword; private String resourceType; private String sourceStatus; private Integer pageNo; private Integer pageSize; } public class SourceResourceResponse { private Long id; private String resourceName; private String resourceType; private String bucketName; private String filePath; private Long fileSize; private String sourceStatus; private String storageProvider; private String remark; private String creatorName; private LocalDateTime createdAt; private LocalDateTime updatedAt; } ``` ### 8.1.4 关键规则 1. 只允许上传 `TEXT / IMAGE / VIDEO`。 2. `source_status = READY` 的资源才能创建任务。 3. 已被任务引用的资源删除时,优先做逻辑归档,不直接删除数据库记录。 4. 删除资源时必须同步清理 RustFS 对象;如清理失败则整体删除失败。 ## 8.2 任务管理模块 ### 8.2.1 模块职责 1. 基于一个或多个资源创建抽取任务。 2. 保存模型配置 ID、Prompt 配置 ID,并在创建时固化模型与 Prompt 快照。 3. 提供任务分页、详情、修改、软删除。 4. 后端只负责创建 `PENDING`;状态推进由 `ai-service` 执行。 ### 8.2.2 接口列表 | 方法 | 路径 | 说明 | |---|---|---| | `POST` | `/api/annotation-tasks` | 创建任务 | | `PUT` | `/api/annotation-tasks/{id}` | 修改任务 | | `GET` | `/api/annotation-tasks` | 分页查询任务 | | `GET` | `/api/annotation-tasks/{id}` | 查询任务详情 | | `DELETE` | `/api/annotation-tasks/{id}` | 删除任务 | ### 8.2.3 接口交互对象 任务模块查询与界面交互不直接使用 `annotation_task` 数据库实体,而是通过聚合视图返回。创建和修改任务时,模型与 Prompt 均支持“选择已有配置”与“手动输入”两种方式: ```java public class ManualModelConfigRequest { private String modelName; private String modelUrl; private String apiKey; } public class TaskModelConfigRequest { private String mode; // SELECT / MANUAL private String selectedConfigName; // mode = SELECT 时必填 private ManualModelConfigRequest manualConfig; // mode = MANUAL 时必填 } public class PromptConfigOptionRequest { private String configName; // 下拉选择 sys_config 中已有 PROMPT 配置时传 private String promptContent; // 手动输入 Prompt 时传 } public class CreateAnnotationTaskRequest { private String taskName; private String industryType; // 行业类型简写,默认 transport,示例 electricity private String taskType; // 默认 EXTRACT_QA private List resourceIds; private TaskModelConfigRequest extractModel; private TaskModelConfigRequest verifyModel; private PromptConfigOptionRequest extractPrompt; private PromptConfigOptionRequest verifyPrompt; } public class UpdateAnnotationTaskRequest { private String industryType; // 行业类型简写,默认 transport,示例 electricity private String taskType; private List resourceIds; private TaskModelConfigRequest extractModel; private TaskModelConfigRequest verifyModel; private PromptConfigOptionRequest extractPrompt; private PromptConfigOptionRequest verifyPrompt; } public class AnnotationTaskPageQuery { private String keyword; private String taskType; private String taskStatus; private Long resourceId; private Boolean isDeleted; private Integer pageNo; private Integer pageSize; } public class TaskModelConfigResponse { private Long configId; private String configName; private String modelName; private String modelUrl; private String apiKeyMasked; } public class TaskPromptConfigResponse { private Long configId; private String configName; private String promptContent; private String promptPreview; } public class AnnotationTaskResponse { private Long id; private String taskName; private String industryType; private String taskType; private List resourceIds; private Integer resourceCount; private String creatorName; private TaskModelConfigResponse extractModel; private TaskModelConfigResponse verifyModel; private TaskPromptConfigResponse extractPrompt; private TaskPromptConfigResponse verifyPrompt; private String taskStatus; private Boolean isDeleted; private LocalDateTime startedAt; private LocalDateTime finishedAt; private String errorMessage; private LocalDateTime createdAt; private LocalDateTime updatedAt; } ``` 交互与落库规则: 前端界面与 `CreateAnnotationTaskRequest` 的模型交互流程明确如下: 1. `extractModel` 和 `verifyModel` 均采用统一对象结构 `TaskModelConfigRequest`,通过 `mode` 显式表达本次是“下拉选择已有配置”还是“手动输入模型配置”。 2. 当 `mode = SELECT` 时,前端展示 `MODEL` 类型配置下拉框,并把用户选中的 `selectedConfigName` 传给后端;后端按 `company_id + config_name` 查询 `sys_config`,校验通过后写入 `extract_model_config_id` 或 `verify_model_config_id`,并同步固化模型快照字段。 3. 当 `mode = MANUAL` 时,前端展示三个手输字段:`modelName`、`modelUrl`、`apiKey`,并通过 `manualConfig` 传给后端。 4. 对于手动输入的 `extractModel`,后端先按 `company_id + config_name = modelName` 查询 `sys_config` 中 `MODEL` 类型配置: - 若存在:直接复用现有配置记录的 `id` 作为 `extract_model_config_id` - 若不存在:自动在 `sys_config` 新增一条 `MODEL` 配置记录,`config_name = modelName`,`config_value` 保存 `modelName + modelUrl + apiKey` 组成的 JSON,再把新配置 `id` 写入 `extract_model_config_id` - 无论配置是复用还是新建,任务表都要固化本次请求的模型快照字段 `extract_model_name`、`extract_model_url`、`extract_model_api_key` 5. 对于手动输入的 `verifyModel`,后端执行与 `extractModel` 完全一致的处理逻辑,写入 `verify_model_config_id` 及对应快照字段;若 `modelName` 不存在,同样自动创建 `sys_config` 记录。 6. `extractPrompt`、`verifyPrompt` 继续支持二选一:要么按 `configName` 选择已有 `PROMPT` 配置,要么直接传 `promptContent` 作为任务快照。 7. 手动输入 Prompt 时,仅固化到任务快照字段 `extract_prompt` / `verify_prompt`,对应 `*_prompt_config_id` 允许为空。 8. 创建接口和修改接口遵循完全相同的配置解析规则。 9. 若 `mode = SELECT` 时仍传入 `manualConfig`,或 `mode = MANUAL` 时仍传入 `selectedConfigName`,后端直接按参数非法处理,避免“选中配置”和“手工输入”语义冲突。 10. 若 `mode = MANUAL` 但 `modelName`、`modelUrl`、`apiKey` 任一为空,后端直接按参数非法处理。 ### 8.2.4 状态流转 标准流转: `PENDING -> RUNNING -> COMPLETED` 失败流转: `PENDING -> RUNNING -> FAILED` 重试流转: `FAILED -> PENDING` 删除规则: - `PENDING / FAILED / COMPLETED` 允许软删除 - `RUNNING` 不允许删除 - 删除接口统一写入 `is_deleted = true`,列表查询默认过滤软删除任务 ### 8.2.5 任务修改规则 修改任务接口覆盖以下属性: - `industryType` - `taskType` - `resourceIds` - `extractModel / verifyModel` - `extractPrompt / verifyPrompt` 字段级状态约束如下: | 任务状态 | `taskName` | `industryType` / `taskType` / `extractModel` / `verifyModel` / `extractPrompt` / `verifyPrompt` | `resourceIds` | |---|---|---|---| | `PENDING` | 不可修改 | 可修改 | 可修改 | | `FAILED` | 不可修改 | 可修改 | 可修改 | | `RUNNING` | 不可修改 | 可修改 | 不可修改 | | `COMPLETED` | 不可修改 | 可修改 | 可修改 | 设计结论: 1. 修改接口不只是绑定资源,还覆盖任务类型、模型配置和 Prompt 配置。 2. `taskName` 作为任务标识一经创建不可修改,修改接口不接收该字段。 3. 资源绑定采用 `1A` 规则:仅当当前任务状态为 `RUNNING` 时禁止修改 `resourceIds`,其他状态允许修改。 4. `RUNNING` 状态下,后端必须校验本次更新前后的 `resourceIds` 是否变化,变化则直接拒绝。 5. 修改 `resourceIds` 时,后端采用“删旧关联 + 重建新关联”的方式维护 `annotation_task_resource`。 6. 更新配置类字段时,后端先按 `TaskModelConfigRequest.mode` 解析“选配”或“手输”路径;对于手动输入模型,按 `modelName` 自动创建 / 复用 `sys_config`,再同步刷新任务快照字段,保证任务表中的执行快照与配置引用一致。 ## 8.3 审批标注结果模块 ### 8.3.1 模块职责 1. 承接 AI 抽取与校验差异结果。 2. 查询运行态结果列表与详情。 3. 对所有未归档运行态结果提供人工审核比对和合并能力。 4. 对 `requires_manual_review = true` 的结果强制走人工审核闭环。 5. 对 `requires_manual_review = false` 的结果提供超时自动归档能力。 ### 8.3.2 接口列表 | 方法 | 路径 | 说明 | |---|---|---| | `GET` | `/api/annotation-results` | 分页查询结果 | | `GET` | `/api/annotation-results/{id}` | 查询结果详情 | | `GET` | `/api/annotation-results/{id}/compare` | 查询人工审核比对视图,适用于全部未归档运行态结果 | | `POST` | `/api/annotation-results/{id}/merge-review` | 提交人工审核结果并归档 | ### 8.3.3 接口交互对象 以下结果对象均为接口层 Response / View,数据库字段 `is_deleted` 在接口层统一转换为 `isDeleted`。列表和详情统一返回 `AnnotationResultResponse`: ```java public class AnnotationResultPageQuery { private Long taskId; private Long resourceId; private Boolean requiresManualReview; private String runtimeStatus; // MANUAL_REVIEW_PENDING / AUTO_ARCHIVE_PENDING / ARCHIVED private Integer pageNo; private Integer pageSize; } public class AnnotationResultResponse { private Long id; private Long taskId; private Long resourceId; private String qaContentJson; private String qaContentStorageMode; private String qaContentFilePath; private String diffSummary; private Boolean requiresManualReview; private Boolean isDeleted; private Long reviewerId; private String reviewComment; private LocalDateTime reviewedAt; private String runtimeStatus; private LocalDateTime createdAt; private LocalDateTime updatedAt; } public class AnnotationResultCompareResponse { private Long id; private Long taskId; private Long resourceId; private String currentQaContentJson; private String qaContentStorageMode; private String qaContentFilePath; private String diffSummary; private String originalResourcePreview; private Boolean requiresManualReview; } public class MergeReviewResultRequest { private String diffSummary; private String qaContentJson; private String reviewComment; } public class MergeReviewResultResponse { private Long resultId; private Long historyId; private String archiveReason; private LocalDateTime archivedAt; } ``` ### 8.3.4 合并审核规则 1. 只要 `annotation_result.is_deleted = false`,审核员都可以进入人工审核流程,不要求 `requires_manual_review` 必须为 `true`。 2. `requires_manual_review = true` 仅表示“强制人工审核”,这类结果不允许走自动归档。 3. `compare` 接口统一返回当前运行态记录中的 `qaContentJson` 与 `diffSummary` 供界面编辑;当 `qaContentStorageMode = EXTERNAL` 时,额外返回 `qaContentFilePath`,用于界面展示当前外置文件来源。 4. `merge-review` 接口接收前端修订后的 `qaContentJson`、`diffSummary` 和 `reviewComment`,后端据此生成最终归档内容。 5. 当 `annotation_result.qa_content_storage_mode = EXTERNAL` 时: - 后端以当前外置文件名为基准,在本地临时目录生成同名临时文件 - 将用户修改后的最终内容写入该临时文件 - 再把该临时文件上传到 RustFS 的归档路径 - 新建 `annotation_result_history` 记录,写入 `qa_content_storage_mode = EXTERNAL` - `annotation_result_history.qa_content_file_path` 保存重新上传后的 RustFS 路径 - `annotation_result_history.qa_content_json` 仅保留精简摘要或默认值 `{}` 6. 当 `annotation_result.qa_content_storage_mode = INLINE` 时: - 后端将用户最终确认的 `qaContentJson` 与 `diffSummary` 合并为一个归档 JSON - 建议结构为 `{"qaContent": , "diffSummary": }` - 将该合并结果写入 `annotation_result_history.qa_content_json` - `annotation_result_history.qa_content_storage_mode` 继续写入 `INLINE` 7. 人工审核归档成功后: - 写入 `annotation_result_history` - 回写运行态结果 `reviewer_id`、`review_comment`、`reviewed_at` - 将运行态结果标记为 `is_deleted = true`,等价于从运行态结果集中删除 ### 8.3.5 自动归档规则 仅对 `requires_manual_review = false and is_deleted = false` 的运行态结果开放自动归档: 1. 创建后进入 `AUTO_ARCHIVE_PENDING` 2. 即使处于 `AUTO_ARCHIVE_PENDING`,审核员仍可在超时前主动进入人工审核并抢占归档路径 3. 超过默认 2 小时且未被人工审核接管时,定时任务把记录迁移到 `annotation_result_history` 4. `requires_manual_review = true` 的记录一律跳过,不允许自动归档 5. 自动归档完成后将运行态结果 `is_deleted = true` ### 8.3.6 审核状态设计结论 一期不再单独保留 `review_status` 字段,原因如下: 1. 当前业务并不存在“审核通过后仍停留在运行表”的必要。 2. 一旦人工确认完成或满足自动归档条件,结果就应该进入历史归档表。 3. 运行态结果只关心“是否强制人工审核”和“是否已经归档”;是否允许人工审核由 `is_deleted = false` 统一决定。 ## 8.4 系统配置模块 ### 8.4.1 模块职责 1. 维护模型配置、Prompt 配置、系统参数。 2. 支持按类型分页查询和详情查看。 3. 支持新增和修改。 ### 8.4.2 接口列表 | 方法 | 路径 | 说明 | |---|---|---| | `POST` | `/api/sys-configs` | 新增配置 | | `PUT` | `/api/sys-configs/{id}` | 修改配置 | | `GET` | `/api/sys-configs` | 分页查询配置 | | `GET` | `/api/sys-configs/{id}` | 查询配置详情 | ### 8.4.3 接口交互对象 配置模块同样使用独立 DTO,不直接向外暴露数据库实体;列表和详情统一返回 `SysConfigResponse`: ```java public class SaveSysConfigRequest { private String configType; // MODEL / PROMPT / SYSTEM private String configName; // 同一 company 下全局唯一,不能与任意类型配置重名 private String configValue; private String status; } public class SysConfigPageQuery { private String configType; private String configName; private String status; private Integer pageNo; private Integer pageSize; } public class SysConfigResponse { private Long id; private String configType; private String configName; private String configValue; private String status; private String creatorName; private LocalDateTime createdAt; private LocalDateTime updatedAt; } ``` ### 8.4.4 配置值约束 | 类型 | `config_name` | `config_value` | |---|---|---| | `MODEL` | 模型配置名,直接使用模型名称,如 `qwen-max` | JSON,至少包含 `modelName`、`modelUrl`、`apiKey` | | `PROMPT` | Prompt 名称,如 `extractPrompt` | 纯文本 | | `SYSTEM` | 系统参数名 | 文本或 JSON | 补充约束: 1. `sys_config` 在同一 `company_id` 下,`config_name` 不允许重复,不再区分 `config_type` 后再重复命名。 2. 任务创建/修改界面的模型和 Prompt 下拉框均以 `configName` 作为选择值。 3. 对于 `MODEL` 类型配置,`config_name` 同时承担任务手动录入模型时的自动建档主键语义。 4. 新增配置时,后端必须先按 `company_id + config_name` 做重复校验;若已存在同名配置,不论其 `config_type` 是 `MODEL`、`PROMPT` 还是 `SYSTEM`,都直接返回 `CONFIG_DUPLICATE`。 5. 修改配置时,如 `configName` 发生变化,后端同样必须校验目标名称是否已被同公司下其他配置占用;若占用则拒绝修改。 6. 该约束适用于人工维护配置和任务创建时的模型自动建档两条路径,二者共用同一唯一性规则。 ## 9. 通用返回结构 一期统一建议: ```java public class ApiResponse { private String code; private String message; private T data; } public class PageResult { private List records; private Long total; private Integer pageNo; private Integer pageSize; } ``` ### 9.1 分页机制评审结论 一期分页机制明确采用 MyBatis-Plus 分页插件方案,不采用 `PageHelper`。 结论如下: 1. 当前项目已经使用 MyBatis-Plus,继续使用其分页能力更符合现有技术栈。 2. 不建议再引入 `PageHelper`,否则会形成两套分页机制并存,增加维护成本和排障复杂度。 3. 也不建议在每个列表查询里手写一套 `LIMIT/OFFSET + COUNT(*)` 作为默认方案,重复代码过多。 推荐实现方式: 1. 在 Spring 配置中启用 MyBatis-Plus `PaginationInnerInterceptor`。 2. 列表查询统一接收 `pageNo`、`pageSize`。 3. Mapper 自定义 SQL 场景使用 `Page` + 查询对象参数的方式,由分页插件自动追加分页 SQL。 4. 对需要聚合的列表,例如任务列表里的 `resourceCount`,允许在 Mapper XML 中写聚合查询,但仍通过 MyBatis-Plus 分页插件处理分页。 分页设计约束: - 默认分页方案:MyBatis-Plus `PaginationInnerInterceptor` - 明确不使用:`PageHelper` - 明确不推荐:每个列表接口手写裸 SQL 分页 ### 9.2 分页实现设计 #### 9.2.1 Spring 配置 一期建议新增统一分页配置类,示例: ```java @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.POSTGRE_SQL); paginationInnerInterceptor.setOverflow(false); paginationInnerInterceptor.setMaxLimit(200L); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } } ``` 约束说明: 1. `DbType` 使用 `POSTGRE_SQL`。 2. `maxLimit` 建议限制为 `200`,防止大页拖垮查询。 3. `overflow(false)`,页码越界时返回空列表,不自动回退到第一页。 #### 9.2.2 通用分页请求约束 所有分页查询 DTO 统一约束如下: ```java public class PageQuery { @Min(1) private Integer pageNo = 1; @Min(1) @Max(200) private Integer pageSize = 20; } ``` 业务分页 DTO 在此基础上扩展过滤字段,例如: ```java public class AnnotationTaskPageQuery extends PageQuery { private String keyword; private String taskType; private String taskStatus; private Long resourceId; } ``` #### 9.2.3 Service 层调用约定 Service 层统一构造 MyBatis-Plus `Page`: ```java public PageResult pageTasks(AnnotationTaskPageQuery query) { Page page = new Page<>(query.getPageNo(), query.getPageSize()); IPage result = annotationTaskMapper.selectTaskPage(page, query); return PageResult.of(result); } ``` 建议补一个统一转换方法: ```java public class PageResult { private List records; private Long total; private Integer pageNo; private Integer pageSize; public static PageResult of(IPage page) { PageResult result = new PageResult<>(); result.setRecords(page.getRecords()); result.setTotal(page.getTotal()); result.setPageNo((int) page.getCurrent()); result.setPageSize((int) page.getSize()); return result; } } ``` #### 9.2.4 Mapper 设计 分页查询建议采用“自定义 XML + `Page` 首参”的方式。 Mapper 接口示例: ```java public interface AnnotationTaskMapper extends BaseMapper { IPage selectTaskPage( Page page, @Param("query") AnnotationTaskPageQuery query ); } ``` Mapper XML 设计原则: 1. `Page` 放在第一个参数位置。 2. 过滤条件统一从 `query.xxx` 取值。 3. 列表查询只查当前页需要字段,不做 `select *`。 4. 排序规则必须显式写出,避免分页结果不稳定。 #### 9.2.5 聚合分页场景 任务列表需要返回 `resourceCount`,属于典型聚合分页场景。推荐写法: ```xml ``` 设计结论: 1. 聚合字段允许在 XML 中显式计算。 2. 资源过滤推荐用 `EXISTS`,避免直接 join 过滤导致重复行干扰分页。 3. 排序建议固定为 `created_at DESC, id DESC`,保证翻页稳定。 #### 9.2.6 Count SQL 评审 对于简单列表,MyBatis-Plus 可自动生成 count 查询。 对于复杂聚合分页,有两条约束: 1. 先使用分页插件默认 count 机制。 2. 如果后续发现 count SQL 在复杂 `GROUP BY` 或多层子查询下性能差,再针对具体接口单独拆 `count` 查询,不要在一期一开始就全量手写。 这也是主流团队的常见做法,先统一插件方案,遇到瓶颈再对个别热点查询做定向优化。 #### 9.2.7 落地范围 一期以下分页接口统一采用同一机制: 1. `/api/source-resources` 2. `/api/annotation-tasks` 3. `/api/annotation-results` 4. `/api/sys-configs` ## 10. 数据库设计 ## 10.1 设计说明 1. 数据库以 `D:\workspace\labeling\lb_backend\src\main\resources\sql` 目录下的 SQL 文件为唯一基准来源。 2. `annotation_task` 当前真实建表同时保存配置引用字段和执行快照字段:前者用于界面回显与配置追溯,后者用于执行时解耦配置变更影响。 3. `annotation_task`、`annotation_result` 均采用软删除字段 `is_deleted` 表达逻辑删除或归档后的不可见状态。 4. 一期 ID 统一使用 `BIGINT`,由应用层生成,不使用自增。 ## 10.2 表关系 ```text sys_company 1 --- n sys_user sys_company 1 --- n sys_menu sys_company 1 --- n sys_config sys_company 1 --- n source_resource sys_company 1 --- n annotation_task sys_company 1 --- n annotation_result sys_company 1 --- n annotation_result_history sys_company 1 --- n training_dataset sys_company 1 --- n export_batch sys_user 1 --- n sys_config(creator_id) sys_user 1 --- n source_resource(creator_id) sys_user 1 --- n annotation_task(creator_id) sys_user 1 --- n annotation_result(creator_id / reviewer_id) sys_user 1 --- n annotation_result_history(creator_id / archived_by) sys_user 1 --- n training_dataset(creator_id) sys_user 1 --- n export_batch(creator_id) sys_config 1 --- n annotation_task(extract_model_config_id) sys_config 1 --- n annotation_task(verify_model_config_id) sys_config 1 --- n annotation_task(extract_prompt_config_id) sys_config 1 --- n annotation_task(verify_prompt_config_id) annotation_task 1 --- n annotation_task_resource source_resource 1 --- n annotation_task_resource annotation_task 1 --- n annotation_result source_resource 1 --- n annotation_result annotation_result 1 --- n annotation_result_history annotation_result_history 1 --- n training_dataset export_batch 1 --- n export_batch_item training_dataset 1 --- n export_batch_item ``` ## 10.3 `sys_company` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 公司主键 | | `company_code` | `VARCHAR(64)` | 否 | 无 | UNIQUE | 公司编码 | | `company_name` | `VARCHAR(128)` | 否 | 无 | 无 | 公司名称 | | `status` | `VARCHAR(32)` | 否 | `ENABLED` | 无 | 公司状态 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | ## 10.4 `sys_user` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 用户主键 | | `company_id` | `BIGINT` | 否 | 无 | FK,索引列 | 公司 ID | | `phone` | `VARCHAR(32)` | 否 | 无 | 同公司唯一约束 | 手机号 | | `username` | `VARCHAR(64)` | 是 | 无 | 无 | 用户名或展示别名 | | `role` | `VARCHAR(32)` | 否 | `EMPLOYEE` | 组合索引列 | 数据权限角色 | | `position` | `VARCHAR(32)` | 否 | `ANNOTATOR` | 组合索引列 | 岗位 | | `real_name` | `VARCHAR(64)` | 否 | 无 | 无 | 真实姓名 | | `password_hash` | `VARCHAR(255)` | 否 | 无 | 无 | 密码哈希 | | `must_change_password` | `BOOLEAN` | 否 | `TRUE` | 无 | 是否首次登录强制改密 | | `status` | `VARCHAR(32)` | 否 | `ENABLED` | 无 | 用户状态 | | `session_version` | `INTEGER` | 否 | `1` | 无 | 会话版本号 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | 实际索引/约束: - `uq_sys_user_company_phone(company_id, phone)` - `idx_sys_user_company(company_id)` - `idx_sys_user_role(company_id, role)` - `idx_sys_user_position(company_id, position)` ## 10.5 `sys_menu` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 菜单主键 | | `company_id` | `BIGINT` | 否 | 无 | FK,组合索引前缀列 | 公司 ID | | `menu_code` | `VARCHAR(64)` | 否 | 无 | 无 | 菜单编码 | | `menu_name` | `VARCHAR(128)` | 否 | 无 | 无 | 菜单名称 | | `path` | `VARCHAR(255)` | 否 | 无 | 无 | 路由路径 | | `visible_positions` | `VARCHAR(255)` | 否 | `ADMIN` | 无 | 可见岗位列表 | | `sort_order` | `INTEGER` | 否 | `0` | 组合索引列 | 排序号 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | 实际索引/约束: - `idx_sys_menu_company_sort(company_id, sort_order)` ## 10.6 `sys_config` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 主键,非自增 | | `company_id` | `BIGINT` | 否 | 无 | FK,索引前缀列 | 公司 ID | | `config_type` | `VARCHAR(32)` | 否 | `SYSTEM` | 组合索引列 | 配置类型 | | `config_name` | `VARCHAR(128)` | 否 | 无 | 同公司唯一索引,不允许同名 | 配置名称,也是前端下拉选择值 | | `config_value` | `TEXT` | 否 | 无 | 无 | 配置值;`MODEL` 类型保存 `modelName/modelUrl/apiKey` JSON | | `status` | `VARCHAR(32)` | 否 | `ENABLED` | 无 | 状态 | | `creator_id` | `BIGINT` | 否 | 无 | FK | 创建人 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | 实际索引/约束: - `uq_sys_config_company_name(company_id, config_name)` - `idx_sys_config_company_type(company_id, config_type)` 约束说明: 1. `uq_sys_config_company_name(company_id, config_name)` 是一期强约束,表示同一公司下 `config_name` 全局唯一。 2. 该唯一性不区分 `config_type`,例如同一公司下不能同时存在名为 `qwen-max` 的 `MODEL` 配置和名为 `qwen-max` 的 `SYSTEM` 配置。 3. 任务创建时若用户手动输入模型名称,自动写入 `sys_config` 前也必须先经过这条唯一性校验。 ## 10.7 `source_resource` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 资源主键 | | `company_id` | `BIGINT` | 否 | 无 | FK,索引前缀列 | 公司 ID | | `creator_id` | `BIGINT` | 否 | 无 | FK,组合索引列 | 上传人 | | `creator_role` | `VARCHAR(32)` | 否 | `EMPLOYEE` | 无 | 创建时角色快照 | | `resource_name` | `VARCHAR(255)` | 否 | 无 | 无 | 展示名称 | | `resource_type` | `VARCHAR(32)` | 否 | `TEXT` | 组合索引列 | `TEXT / IMAGE / VIDEO` | | `bucket_name` | `VARCHAR(128)` | 否 | 无 | 无 | 对象存储桶 | | `file_path` | `VARCHAR(512)` | 否 | 无 | 无 | 对象路径 | | `file_size` | `BIGINT` | 否 | `0` | 无 | 文件大小 | | `source_status` | `VARCHAR(32)` | 否 | `UPLOADED` | 组合索引列 | 资源状态 | | `storage_provider` | `VARCHAR(64)` | 否 | `rustfs` | 无 | 存储提供方 | | `remark` | `VARCHAR(255)` | 是 | 无 | 无 | 备注 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | 实际索引/约束: - `idx_source_resource_company_type(company_id, resource_type)` - `idx_source_resource_company_status(company_id, source_status)` - `idx_source_resource_creator(company_id, creator_id)` ## 10.8 `annotation_task` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 任务主键 | | `company_id` | `BIGINT` | 否 | 无 | FK,索引前缀列 | 公司 ID | | `creator_id` | `BIGINT` | 否 | 无 | FK,组合索引列 | 创建人 | | `creator_role` | `VARCHAR(32)` | 否 | `EMPLOYEE` | 无 | 创建时角色快照 | | `task_name` | `VARCHAR(255)` | 否 | 无 | 无 | 任务名称 | | `industry_type` | `VARCHAR(32)` | 否 | `transport` | 无 | 行业类型简写,默认 transport,示例 electricity | | `task_type` | `VARCHAR(32)` | 否 | `EXTRACT_QA` | 无 | 任务类型 | | `extract_model_config_id` | `BIGINT` | 是 | 无 | FK | 抽取模型配置 ID,关联 `sys_config.id` | | `extract_model_name` | `VARCHAR(128)` | 是 | 无 | 无 | 抽取模型名 | | `extract_model_url` | `VARCHAR(255)` | 是 | 无 | 无 | 抽取模型地址 | | `extract_model_api_key` | `VARCHAR(255)` | 是 | 无 | 无 | 抽取模型 Key | | `verify_model_config_id` | `BIGINT` | 是 | 无 | FK | 校验模型配置 ID,关联 `sys_config.id` | | `verify_model_name` | `VARCHAR(128)` | 是 | 无 | 无 | 校验模型名 | | `verify_model_url` | `VARCHAR(255)` | 是 | 无 | 无 | 校验模型地址 | | `verify_model_api_key` | `VARCHAR(255)` | 是 | 无 | 无 | 校验模型 Key | | `extract_prompt_config_id` | `BIGINT` | 是 | 无 | FK | 抽取 Prompt 配置 ID,关联 `sys_config.id` | | `extract_prompt` | `TEXT` | 是 | 无 | 无 | 抽取 Prompt 快照 | | `verify_prompt_config_id` | `BIGINT` | 是 | 无 | FK | 校验 Prompt 配置 ID,关联 `sys_config.id` | | `verify_prompt` | `TEXT` | 是 | 无 | 无 | 校验 Prompt 快照 | | `task_status` | `VARCHAR(32)` | 否 | `PENDING` | 组合索引列 | 任务状态 | | `is_deleted` | `BOOLEAN` | 否 | `FALSE` | 组合索引列 | 任务软删除标记 | | `started_at` | `TIMESTAMP` | 是 | 无 | 无 | 开始时间 | | `finished_at` | `TIMESTAMP` | 是 | 无 | 无 | 结束时间 | | `error_message` | `TEXT` | 是 | 无 | 无 | 错误信息 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | 实际索引/约束: - `idx_annotation_task_company_status(company_id, task_status)` - `idx_annotation_task_company_deleted(company_id, is_deleted)` - `idx_annotation_task_creator(company_id, creator_id)` 设计说明: 1. `annotation_task` 主表不再直接保存 `resource_id`。 2. 一个任务可绑定多个资源,具体绑定关系落在 `annotation_task_resource`。 3. `task_type` 默认值为 `EXTRACT_QA`。 4. 配置 ID 字段用于关联按 `configName` 选中的模型/Prompt 配置;名称、URL、Key、Prompt 文本字段用于固化执行快照。 5. `industry_type` 用于保存任务所属行业类型简写,默认值为 `transport`,可选值按业务枚举扩展,例如 `electricity`。 6. `extractModel`、`verifyModel` 在接口层不再复用单层平铺字段,而是统一使用 `TaskModelConfigRequest(mode + selectedConfigName + manualConfig)` 显式表达页面交互模式。 7. 手动输入模型配置时,后端先按模型名称匹配 `sys_config.config_name`;不存在则自动创建 `MODEL` 配置后再写入任务;存在则复用已有配置 ID。 8. 手动输入 Prompt 时,仅保存快照文本,不强制自动创建 `PROMPT` 配置。 9. `task_name` 只允许创建时写入,后续更新接口不可修改。 10. 删除接口不物理删除任务,统一更新 `is_deleted = true`。 ## 10.9 `annotation_task_resource` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 任务资源关联主键 | | `company_id` | `BIGINT` | 否 | 无 | FK,组合索引前缀列 | 公司 ID | | `task_id` | `BIGINT` | 否 | 无 | FK,唯一约束组成列,组合索引列 | 任务 ID | | `resource_id` | `BIGINT` | 否 | 无 | FK,唯一约束组成列,组合索引列 | 资源 ID | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | 实际索引/约束: - `uq_annotation_task_resource(task_id, resource_id)` - `idx_annotation_task_resource_company_task(company_id, task_id)` - `idx_annotation_task_resource_company_resource(company_id, resource_id)` ## 10.10 `annotation_result` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 运行态结果主键 | | `company_id` | `BIGINT` | 否 | 无 | FK,索引前缀列 | 公司 ID | | `creator_id` | `BIGINT` | 否 | 无 | FK | 结果创建人 | | `creator_role` | `VARCHAR(32)` | 否 | `EMPLOYEE` | 无 | 创建时角色快照 | | `task_id` | `BIGINT` | 否 | 无 | FK,组合索引列 | 任务 ID | | `resource_id` | `BIGINT` | 否 | 无 | FK | 资源 ID | | `qa_content_json` | `TEXT` | 否 | `'{}'` | 无 | 问答 JSON | | `qa_content_storage_mode` | `VARCHAR(32)` | 否 | `INLINE` | 无 | `INLINE / EXTERNAL` | | `qa_content_file_path` | `VARCHAR(512)` | 是 | 无 | 无 | 外置问答文件路径 | | `diff_summary` | `TEXT` | 否 | `'{}'` | 无 | 差异摘要 JSON | | `requires_manual_review` | `BOOLEAN` | 否 | `FALSE` | 组合索引列 | 是否强制人工审核 | | `is_deleted` | `BOOLEAN` | 否 | `FALSE` | 组合索引列 | 软删除标记 | | `reviewer_id` | `BIGINT` | 是 | 无 | FK | 审核人 | | `review_comment` | `TEXT` | 是 | 无 | 无 | 审核备注 | | `reviewed_at` | `TIMESTAMP` | 是 | 无 | 无 | 审核时间 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | 实际索引/约束: - `idx_annotation_result_company_deleted(company_id, is_deleted)` - `idx_annotation_result_company_manual(company_id, requires_manual_review)` - `idx_annotation_result_task(company_id, task_id)` 设计说明: 1. 去掉 `result_type`,结果类型由任务和资源类型推导。 2. 去掉 `review_status`,审核完成后直接归档。 3. 新增 `is_deleted` 作为软删除标记,配合历史表形成完整生命周期。 ## 10.11 `annotation_result_history` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 历史记录主键 | | `company_id` | `BIGINT` | 否 | 无 | FK,普通索引 | 公司 ID | | `creator_id` | `BIGINT` | 否 | 无 | FK | 原结果创建人 | | `creator_role` | `VARCHAR(32)` | 否 | `EMPLOYEE` | 无 | 原创建角色 | | `source_result_id` | `BIGINT` | 是 | 无 | FK | 来源运行态结果 ID | | `task_id` | `BIGINT` | 否 | 无 | FK,组合索引列 | 任务 ID | | `resource_id` | `BIGINT` | 否 | 无 | FK,组合索引列 | 资源 ID | | `qa_content_json` | `TEXT` | 否 | `'{}'` | 无 | 归档后的问答 JSON | | `qa_content_storage_mode` | `VARCHAR(32)` | 否 | `INLINE` | 无 | 归档后的问答存储模式 | | `qa_content_file_path` | `VARCHAR(512)` | 是 | 无 | 无 | 归档后的外置问答文件路径 | | `archive_reason` | `VARCHAR(255)` | 是 | 无 | 无 | 归档原因 | | `archived_by` | `BIGINT` | 是 | 无 | FK | 归档操作人 | | `archived_at` | `TIMESTAMP` | 是 | 无 | 无 | 归档时间 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | 实际索引/约束: - `idx_annotation_result_history_company(company_id)` - `idx_annotation_result_history_task(company_id, task_id)` - `idx_annotation_result_history_resource(company_id, resource_id)` ## 10.12 `training_dataset` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 训练样本主键 | | `company_id` | `BIGINT` | 否 | 无 | FK | 公司 ID | | `creator_id` | `BIGINT` | 否 | 无 | FK | 创建人 | | `creator_role` | `VARCHAR(32)` | 否 | `EMPLOYEE` | 无 | 创建人数据权限角色 | | `result_history_id` | `BIGINT` | 否 | 无 | FK | 来源历史结果 ID | | `sample_type` | `VARCHAR(32)` | 否 | `TEXT` | 无 | 样本类型 | | `glm_format_json` | `TEXT` | 否 | 无 | 无 | GLM 微调格式 JSON | | `dataset_status` | `VARCHAR(32)` | 否 | `DRAFT` | 组合索引列 | 样本状态 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | 实际索引/约束: - `idx_training_dataset_company_status(company_id, dataset_status)` ## 10.13 `export_batch` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 导出批次主键 | | `company_id` | `BIGINT` | 否 | 无 | FK | 公司 ID | | `creator_id` | `BIGINT` | 否 | 无 | FK | 创建人 | | `creator_role` | `VARCHAR(32)` | 否 | `EMPLOYEE` | 无 | 创建人数据权限角色 | | `batch_no` | `VARCHAR(64)` | 否 | 无 | UNIQUE | 批次编号 | | `dataset_file_path` | `VARCHAR(512)` | 是 | 无 | 无 | 导出文件路径 | | `sample_count` | `INTEGER` | 否 | `0` | 无 | 样本数 | | `finetune_job_id` | `VARCHAR(128)` | 是 | 无 | 无 | 微调任务 ID | | `finetune_status` | `VARCHAR(32)` | 否 | `NOT_STARTED` | 组合索引列 | 微调状态 | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | | `updated_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 更新时间 | 实际索引/约束: - `idx_export_batch_company_status(company_id, finetune_status)` ## 10.14 `export_batch_item` | 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | |---|---|---|---|---|---| | `id` | `BIGINT` | 否 | 无 | PK | 批次样本关系主键 | | `batch_id` | `BIGINT` | 否 | 无 | FK,唯一约束组成列 | 导出批次 ID | | `dataset_id` | `BIGINT` | 否 | 无 | FK,唯一约束组成列 | 训练样本 ID | | `created_at` | `TIMESTAMP` | 是 | `CURRENT_TIMESTAMP` | 无 | 创建时间 | 实际索引/约束: - `uq_export_batch_item(batch_id, dataset_id)` ## 11. 一期关键流程 ### 11.1 资源上传到任务创建 1. 用户上传资源。 2. 后端将文件写入 RustFS,并生成 `source_resource`。 3. 当前无论文本、图片还是视频,校验通过后均直接置为 `READY`。 4. 用户选择一个或多个资源、模型、Prompt 创建任务。 5. 后端写入 `annotation_task` 和 `annotation_task_resource`,任务状态固定为 `PENDING`。 ```mermaid flowchart TD A[用户上传资源] --> B[backend 写入 RustFS] B --> C[创建 source_resource] C --> D[资源校验通过] D --> E[source_status = READY] E --> F[用户选择资源/模型/Prompt] F --> G[创建 annotation_task] G --> H[创建 annotation_task_resource] H --> I[task_status = PENDING] ``` ### 11.2 AI 处理到结果入库 1. `ai-service` 轮询 `PENDING` 任务。 2. 拉取资源并处理,任务改为 `RUNNING`。 3. 生成抽取结果和校验结果。 4. 对比差异后写入 `annotation_result`,同时落库 `qa_content_json`、`diff_summary`、`qa_content_storage_mode` 等运行态字段。 5. 若结果必须由人工确认后才能进入最终归档,则标记 `requires_manual_review = true`。 6. 若结果允许系统在超时后直接归档,则标记 `requires_manual_review = false`。 7. 两种结果在 `is_deleted = false` 期间都允许审核员主动发起人工审核;差异仅在于是否允许后续自动归档。 ```mermaid flowchart TD A[ai-service 轮询 PENDING 任务] --> B[任务置为 RUNNING] B --> C[抽取与校验] C --> D[生成 qa_content_json 和 diff_summary] D --> E{是否必须人工审核?} E -- 是 --> F[写入 annotation_result
requires_manual_review = true] E -- 否 --> G[写入 annotation_result
requires_manual_review = false] F --> H[等待人工审核归档] G --> I[等待人工审核或自动归档] ``` ### 11.3 人工审核归档 1. 审核员打开结果比对页。 2. 查询 `compare` 接口获取资源预览、当前 `qaContentJson`、`diffSummary`、`qaContentStorageMode` 和 `qaContentFilePath`。 3. 无论 `requires_manual_review = true` 还是 `false`,只要记录尚未归档,审核员都可以修改 `qaContentJson` 和必要的 `diffSummary`。 4. 调用 `merge-review` 接口提交最终内容。 5. 若当前记录 `qa_content_storage_mode = EXTERNAL`: - 后端在本地生成与当前外置文件同名的临时文件 - 将用户最终内容写入该临时文件 - 上传到 RustFS 归档路径 - 新建 `annotation_result_history`,写入 `qa_content_storage_mode = EXTERNAL` 和新的 `qa_content_file_path` 6. 若当前记录 `qa_content_storage_mode = INLINE`: - 后端将 `qaContentJson` 与 `diffSummary` 合并 - 将合并结果写入 `annotation_result_history.qa_content_json` 7. 归档成功后,运行态 `annotation_result` 回写审核人信息并标记 `is_deleted = true`,从运行态结果集中移除。 ```mermaid flowchart TD A[审核员打开 compare 页面] --> B[读取 qaContentJson / diffSummary / storageMode] B --> C[审核员修改最终内容] C --> D[调用 merge-review] D --> E{qa_content_storage_mode} E -- EXTERNAL --> F[本地生成同名临时文件] F --> G[写入最终内容] G --> H[上传 RustFS 归档路径] H --> I[写 annotation_result_history
storage_mode = EXTERNAL] E -- INLINE --> J[合并 qaContentJson + diffSummary] J --> K[写 annotation_result_history
storage_mode = INLINE] I --> L[回写 reviewer 信息] K --> L L --> M[annotation_result.is_deleted = true] ``` ### 11.4 自动归档 1. 定时任务仅扫描 `requires_manual_review = false and is_deleted = false` 的运行态结果。 2. 若结果已被审核员人工处理接管,则本次不再进入自动归档。 3. 若 `created_at + 2h < now()`,则执行自动归档。 4. 自动归档完成后写入 `annotation_result_history`。 5. 回写运行态结果 `is_deleted = true`。 6. `requires_manual_review = true` 的记录不参与自动归档扫描。 ```mermaid flowchart TD A[AutoArchiveAnnotationResultJob 扫描运行态结果] --> B{requires_manual_review = false?} B -- 否 --> C[跳过] B -- 是 --> D{is_deleted = false?} D -- 否 --> C D -- 是 --> E{是否已被人工审核接管?} E -- 是 --> C E -- 否 --> F{是否超过2小时?} F -- 否 --> C F -- 是 --> G[写 annotation_result_history] G --> H[annotation_result.is_deleted = true] ``` ## 12. 典型任务与定时任务 一期当前仅保留一个后台任务: 1. `AutoArchiveAnnotationResultJob` - 仅扫描 `requires_manual_review = false` 且超时的运行态结果 - 跳过已被人工审核接管或已归档的记录 - 迁移到历史表 - 回写 `is_deleted = true` ## 13. 错误处理与审计 ### 13.1 错误码建议 | 错误码 | 场景 | |---|---| | `RESOURCE_NOT_READY` | 资源未就绪不可创建任务 | | `TASK_STATUS_INVALID` | 当前任务状态不允许删除或重试 | | `RESULT_NOT_REVIEWABLE` | 结果已归档、已从运行态移除,或当前记录不允许继续进入审核提交 | | `CONFIG_DUPLICATE` | 同公司下 `configName` 重复 | | `OBJECT_STORAGE_ERROR` | RustFS 上传/删除失败 | ### 13.2 审计建议 以下操作必须记录操作日志: 1. 资源删除 2. 任务创建与删除 3. 人工审核合并 4. 系统配置新增与修改 ## 14. 一期验收标准 1. 资源、任务、结果、配置四个模块均有清晰 REST 接口。 2. 请求 DTO、响应 DTO、分页结构与数据库实体解耦,可直接映射 Controller 层代码。 3. 数据表字段类型、默认值、索引、约束与业务语义一致。 4. 对象存储路径能覆盖原始资源、外置问答结果和导出文件。 5. `annotation_result` 生命周期遵循“运行态在当前表、最终态进历史表”的原则。 6. source 相关状态只描述资源本身,不再混入任务和审核状态。 ## 15. 与当前代码基线的差异说明 为满足最新需求,本文档相对旧版设计的关键调整如下: 1. `annotation_result` 去掉 `result_type`。 2. `annotation_result` 去掉 `review_status`。 3. `annotation_result` 增加 `is_deleted`。 4. `annotation_task` 改为任务主表 + `annotation_task_resource` 关联表,一个任务可绑定多个资源。 5. `annotation_task.task_type` 默认值调整为 `EXTRACT_QA`。 6. `annotation_task` 新增 `extract_model_config_id`、`verify_model_config_id`、`extract_prompt_config_id`、`verify_prompt_config_id`。 7. `annotation_task` 新增软删除字段 `is_deleted`,删除接口采用逻辑删除。 8. 任务修改接口明确不允许修改 `taskName`,且 `RUNNING` 状态不可修改绑定资源。 9. 任务、资源、结果、配置均通过独立接口 DTO/VO 对外提供,不直接暴露数据库实体。 10. 任务状态使用 `PENDING / RUNNING / COMPLETED / FAILED`。 11. `source_resource` 只保留资源状态,不承接抽取和审核流转。 12. 当前不引入独立的视频预处理作业设计。 13. 对象存储规范重新分桶、重新分层。 14. 创建和修改任务的请求对象改为支持按 `configName` 选择已有配置,或手动输入模型 / Prompt;模型名不存在时自动创建 `sys_config`。 15. `AnnotationTaskResponse`、`SourceResourceResponse`、`SysConfigResponse`、`AnnotationResultResponse` 分别统一列表和详情返回结构。 16. `sys_config` 改为同公司下 `config_name` 唯一,不再允许不同类型复用同名配置。 17. `annotation_task` 新增 `industry_type` 字段,默认值 `transport`,用于保存行业类型简写;`source_resource` 去掉 `content_type` 字段。 ## 15. 开发规范 ### 15.1 包命名规范 - 基础包名: `com.{service-name}` - 服务名使用小写字母和连字符,如: `user-service`, `order-service` ### 15.2 类命名规范 - 实体类: 使用名词,如 `User`, `Order` - 控制器: 以 `Controller` 结尾,如 `UserController` - 服务类: 以 `Service` 结尾,如 `UserService` - DTO类: 以 `Request`/`Response` 结尾,如 `UserRequest`, `UserResponse` - 常量类: 以 `Constants` 结尾,如 `UserConstants` ### 15.3 方法命名规范 - 查询方法: `get`, `find`, `list`, `query` - 创建方法: `create`, `add`, `save` - 更新方法: `update`, `modify` - 删除方法: `delete`, `remove` ### 15.4 异常处理规范 - 使用统一的异常处理机制 - 自定义业务异常继承 `RuntimeException` - 使用 `@ControllerAdvice` 进行全局异常处理 ### 15.5 日志规范 - 使用 `@Slf4j` 注解 - 日志级别: DEBUG(开发), INFO(生产) - 关键操作必须记录日志 ## 16 swagger 接口规范 ### 16.1 接口设计原则: - 所有接口和固定结构的注释请使用中文描述性注释,如 `@ApiModelProperty("用户名")` - 如果接口返回的是稳定字段集合,应使用明确 DTO 或明确对象字段注解。 - 所有公开接口都能展示清晰的路径参数、查询参数、请求头参数、表单参数说明 ### 16.2 swagger规范 - 所有接口、固定机构注释和说明统一使用中文 - 所有固定结构的请求体都使用 DTO 建模,并为每个字段提供名称、类型、必填性和含义说明 - 如果业务内部结果本身仍是动态 JSON,则至少提供一个固定外层 DTO,把最外层字段含义说明清楚,避免 Swagger 展示匿名 `Map`。 - 所有固定结构的主要响应对象都能展示字段说明 - 所有分页与统一返回包装的字段含义清晰可见 这份设计文档应作为后续一期实现、SQL 调整和接口落地的基准版本。