Files
lablesys_backend/README.md
2026-04-27 23:26:25 +08:00

1497 lines
63 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 标注平台后端设计方案
## 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<T>`
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<Long> 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<Long> 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<Long> 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": <qaContentJson>, "diffSummary": <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<T> {
private String code;
private String message;
private T data;
}
public class PageResult<T> {
private List<T> 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<T>` + 查询对象参数的方式,由分页插件自动追加分页 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<T>`
```java
public PageResult<AnnotationTaskResponse> pageTasks(AnnotationTaskPageQuery query) {
Page<AnnotationTaskResponse> page = new Page<>(query.getPageNo(), query.getPageSize());
IPage<AnnotationTaskResponse> result = annotationTaskMapper.selectTaskPage(page, query);
return PageResult.of(result);
}
```
建议补一个统一转换方法:
```java
public class PageResult<T> {
private List<T> records;
private Long total;
private Integer pageNo;
private Integer pageSize;
public static <T> PageResult<T> of(IPage<T> page) {
PageResult<T> 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<T>` 首参”的方式。
Mapper 接口示例:
```java
public interface AnnotationTaskMapper extends BaseMapper<AnnotationTask> {
IPage<AnnotationTaskResponse> selectTaskPage(
Page<AnnotationTaskResponse> page,
@Param("query") AnnotationTaskPageQuery query
);
}
```
Mapper XML 设计原则:
1. `Page<T>` 放在第一个参数位置。
2. 过滤条件统一从 `query.xxx` 取值。
3. 列表查询只查当前页需要字段,不做 `select *`
4. 排序规则必须显式写出,避免分页结果不稳定。
#### 9.2.5 聚合分页场景
任务列表需要返回 `resourceCount`,属于典型聚合分页场景。推荐写法:
```xml
<select id="selectTaskPage" resultType="com.labelsys.backend.dto.response.AnnotationTaskResponse">
SELECT
t.id,
t.task_name AS taskName,
t.task_type AS taskType,
t.task_status AS taskStatus,
t.is_deleted AS isDeleted,
t.created_at AS createdAt,
COUNT(tr.id) AS resourceCount
FROM annotation_task t
LEFT JOIN annotation_task_resource tr ON tr.task_id = t.id
WHERE t.company_id = #{query.companyId}
AND t.is_deleted = COALESCE(#{query.isDeleted}, FALSE)
<if test="query.keyword != null and query.keyword != ''">
AND t.task_name ILIKE CONCAT('%', #{query.keyword}, '%')
</if>
<if test="query.taskType != null and query.taskType != ''">
AND t.task_type = #{query.taskType}
</if>
<if test="query.taskStatus != null and query.taskStatus != ''">
AND t.task_status = #{query.taskStatus}
</if>
<if test="query.resourceId != null">
AND EXISTS (
SELECT 1
FROM annotation_task_resource tr2
WHERE tr2.task_id = t.id
AND tr2.resource_id = #{query.resourceId}
)
</if>
GROUP BY t.id, t.task_name, t.task_type, t.task_status, t.is_deleted, t.created_at
ORDER BY t.created_at DESC, t.id DESC
</select>
```
设计结论:
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<br/>requires_manual_review = true]
E -- 否 --> G[写入 annotation_result<br/>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<br/>storage_mode = EXTERNAL]
E -- INLINE --> J[合并 qaContentJson + diffSummary]
J --> K[写 annotation_result_history<br/>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 调整和接口落地的基准版本。