From a01a06d4c1d0b7c8525b314ffee44419db941354 Mon Sep 17 00:00:00 2001 From: wh Date: Mon, 27 Apr 2026 23:26:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1723 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 1409 insertions(+), 314 deletions(-) diff --git a/README.md b/README.md index c21b44f..a59581f 100644 --- a/README.md +++ b/README.md @@ -1,401 +1,1496 @@ -# label-backend +# 标注平台后端设计方案 -## 项目简介 +## 1. 文档目标 -`label-backend` 是知识图谱智能标注平台的后端服务,负责资料上传、任务分发、提取标注、问答生成、训练样本导出、系统配置和视频预处理等核心流程。 +本文档只覆盖一期 Java Backend 需要落地的模块,不扩展到训练集导出、微调任务编排二期能力。 -系统采用 Spring Boot 3 + MyBatis-Plus + PostgreSQL + Redis 的技术组合,面向多租户标注场景设计,支持: +## 2. 一期范围 -- 公司级数据隔离 -- 基于 Redis Token 的认证鉴权 -- 提取与问答两阶段标注流程 -- 导出训练数据并对接微调任务 -- 视频预处理与异步回调 -- 审计日志与任务状态追踪 +### 2.1 一期要落地的模块 -代码结构已按扁平标准目录整理,主包位于 `src/main/java/com/label`。 +一期后端只建设以下四个业务模块: -## 功能特性 +1. 资源管理模块 `source_resource` +2. 任务管理模块 `annotation_task` +3. 审批标注结果模块 `annotation_result` / `annotation_result_history` +4. 系统配置模块 `sys_config` -- 认证鉴权 - - 使用 UUID Bearer Token + Redis 会话存储 - - 自定义 `@RequireAuth`、`@RequireRole` - - 角色分级:`ADMIN > REVIEWER > ANNOTATOR > UPLOADER` -- 多租户隔离 - - 基于 `CompanyContext` + `TenantLineInnerInterceptor` - - 对租户表自动追加 `company_id` 条件 - - 特殊表通过显式 `companyId` 参数校验 -- 公司与用户管理 - - 公司 CRUD - - 公司内用户创建、状态变更、角色变更 - - 角色变更和禁用后即时刷新或失效 Redis Token -- 资料管理 - - 支持文本、图片、视频三类原始资料 - - 上传到 RustFS,数据库保存元数据 - - 支持按角色查看、查询详情、删除 -- 任务管理 - - 任务池、我的任务、待审批队列、管理员全量视图 - - Redis 分布式锁 + 数据库原子更新保证任务领取并发安全 -- 提取标注 - - AI 预标注 - - 标注结果整体覆盖更新 - - 提交、审批通过、驳回 -- 问答生成 - - 基于提取审批通过事件生成候选问答对 - - 支持编辑、提交、审批、驳回 -- 训练数据导出 - - 查询已审批样本 - - 创建导出批次 - - 触发微调任务并查询状态 -- 系统配置 - - 支持公司专属配置覆盖全局默认配置 - - 配置项存储于 `sys_config` -- 视频处理 - - 支持触发视频预处理任务 - - 支持异步回调、失败重试、管理员重置 +一期继续复用现有认证鉴权基线: -## 技术栈 +- 用户登录、Token、会话版本控制沿用现有认证实现 +- 岗位权限沿用 `position` +- 数据权限沿用 `role` +- 租户隔离沿用 `company_id` -- Java 21 -- Spring Boot 3.1.5 -- Spring MVC -- MyBatis-Plus 3.5.3.1 -- PostgreSQL -- Redis -- Spring AOP -- springdoc-openapi -- Testcontainers -- RustFS / S3 兼容对象存储 -## 项目结构 +### 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 -label_backend/ -├── assembly/ # 分发包描述与占位目录 -├── docs/ # 设计、计划、规范文档 -├── scripts/ # 启动脚本 -├── src/ -│ ├── main/ -│ │ ├── java/com/label/ # 主代码 -│ │ └── resources/ -│ │ ├── application.yml -│ │ ├── logback.xml -│ │ └── sql/ # 初始化 SQL,不打入构建产物 -│ └── test/ -│ ├── java/ # 单元测试与集成测试 -│ └── resources/db/init.sql # Testcontainers 测试初始化 SQL -├── docker-compose.yml -├── Dockerfile -├── pom.xml -└── README.md +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 ``` -Java 包结构: +补充约定: -```text -com.label -├── annotation # 自定义注解,如 RequireAuth / RequireRole / OperationLog -├── aspect # AOP 审计切面 -├── common # 通用能力:auth、context、exception、result、storage、ai、statemachine -├── config # Spring 配置、MyBatis-Plus 配置、认证拦截器注册 -├── controller # 所有 REST 接口 -├── dto # DTO -├── entity # 实体 -├── event # 领域事件 -├── interceptor # 认证拦截器 -├── listener # 事件监听器 -├── mapper # MyBatis Mapper -├── service # 业务服务 -└── 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 接口对象与数据库实体分层 -初始化脚本位于 [init.sql](d:/workspace/label/label_backend/src/main/resources/sql/init.sql),包含 11 张核心表: +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` 只在任务快照和配置表中保存,接口返回时必须脱敏或不返回。 -- `sys_company` - - 租户公司表 -- `sys_user` - - 公司用户表,包含角色与状态 -- `source_data` - - 原始资料元数据,支持 `TEXT` / `IMAGE` / `VIDEO` -- `annotation_task` - - 标注任务表,支持 `EXTRACTION` / `QA_GENERATION` -- `annotation_result` - - 提取阶段 JSON 结果 -- `training_dataset` - - 训练样本数据,存储 GLM 格式 JSON -- `export_batch` - - 导出批次与微调任务状态 -- `sys_config` - - 全局与公司级配置 -- `sys_operation_log` - - 审计日志,只追加不更新 -- `annotation_task_history` - - 任务状态变更历史 -- `video_process_job` - - 视频预处理任务与回调状态 +## 4. 权限模型 -当前主要状态机: +### 4.1 岗位权限 -- `source_data.status` - - `PENDING` / `PREPROCESSING` / `EXTRACTING` / `QA_REVIEW` / `APPROVED` -- `annotation_task.status` - - `UNCLAIMED` / `IN_PROGRESS` / `SUBMITTED` / `APPROVED` / `REJECTED` -- `training_dataset.status` - - `PENDING_REVIEW` / `APPROVED` / `REJECTED` -- `video_process_job.status` - - `PENDING` / `RETRYING` / `SUCCESS` / `FAILED` +一期沿用现有岗位枚举: -## 配置说明 +- `ANNOTATOR` +- `DATA_TRAINER` +- `REVIEWER` +- `ADMIN` -主配置文件位于 [application.yml](d:/workspace/label/label_backend/src/main/resources/application.yml)。 +权限建议如下: -### 环境变量 +| 模块 | 查询 | 新增/修改 | 删除 | 特殊说明 | +|---|---|---|---|---| +| 资源管理 | 全岗位可查 | 全岗位可上传 | 全岗位可删自身可见资源 | 删除时仍受数据权限约束 | +| 任务管理 | 全岗位可查 | 全岗位可创建 | 全岗位可删未执行任务 | 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`。运行态按下列规则推导: + +| 运行态名称 | 计算规则 | |---|---| -| `SPRING_DATASOURCE_URL` | PostgreSQL JDBC 地址 | -| `SPRING_DATASOURCE_USERNAME` | PostgreSQL 用户名 | -| `SPRING_DATASOURCE_PASSWORD` | PostgreSQL 密码 | -| `SPRING_DATA_REDIS_HOST` | Redis 主机 | -| `SPRING_DATA_REDIS_PORT` | Redis 端口 | -| `SPRING_DATA_REDIS_PASSWORD` | Redis 密码 | -| `RUSTFS_ENDPOINT` | RustFS / S3 兼容服务地址 | -| `RUSTFS_ACCESS_KEY` | RustFS Access Key | -| `RUSTFS_SECRET_KEY` | RustFS Secret Key | -| `AI_SERVICE_BASE_URL` | AI 服务地址 | -| `VIDEO_CALLBACK_SECRET` | 视频处理回调共享密钥 | +| `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. 对象存储路径规范 -- `auth.enabled` - - `true` 时启用真实 Token 鉴权 - - `false` 时使用 mock 身份,便于本地开发 -- `auth.mock-company-id` - - 开发模式下的模拟公司 ID -- `auth.mock-user-id` - - 开发模式下的模拟用户 ID -- `auth.mock-role` - - 开发模式下的模拟角色 -- `token.ttl-seconds` - - Token 有效期,默认 7200 秒 -- `springdoc.api-docs.path` - - OpenAPI 文档路径,默认 `/v3/api-docs` -- `springdoc.swagger-ui.path` - - Swagger UI 路径,默认 `/swagger-ui.html` +### 6.1 评审结论 -## API接口 +旧参考路径存在三个问题: -以下为当前主要接口分组。 +1. 直接把业务含义写死在一级目录,后续跨租户清理困难。 +2. 部分路径使用原始文件名,不利于重名控制和脱敏。 +3. 原始资源、衍生资源、审核外置文件、导出文件没有统一前缀策略。 -### 1. 认证接口 +### 6.2 一期统一规范 -- `POST /api/auth/login` - - 登录并返回 Bearer Token -- `POST /api/auth/logout` - - 登出并立即失效当前 Token -- `GET /api/auth/me` - - 获取当前登录用户信息 +统一规则: -### 2. 公司管理 +1. 路径全部使用小写。 +2. 不直接使用用户原始文件名做对象 Key。 +3. 优先使用业务主键和年月做路径分层。 +4. 按“租户 / 资源域 / 类型 / 年月 / 业务 ID”组织。 -- `GET /api/companies` -- `POST /api/companies` -- `PUT /api/companies/{id}` -- `PUT /api/companies/{id}/status` -- `DELETE /api/companies/{id}` +### 6.3 Bucket 规划 -### 3. 用户管理 +| Bucket | 用途 | +|---|---| +| `source-data` | 原始文本、图片、视频及视频衍生物 | +| `annotation-artifacts` | 外置问答 JSON、diff JSON、人工审核快照 | +| `finetune-export` | 导出 JSONL 文件 | -- `GET /api/users` -- `POST /api/users` -- `PUT /api/users/{id}` -- `PUT /api/users/{id}/status` -- `PUT /api/users/{id}/role` +### 6.4 路径格式 -### 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` | -- `POST /api/source/upload` -- `GET /api/source/list` -- `GET /api/source/{id}` -- `DELETE /api/source/{id}` +### 6.5 命名规则 -### 5. 任务管理 +1. `resourceId`、`taskId`、`resultId` 使用数据库主键或雪花 ID。 +2. `yyyyMM` 例如 `202604`。 +3. 文件扩展名按后端识别后的标准扩展名写入,不沿用用户大小写。 +4. 资源删除时优先删对象,再更新数据库状态。 -- `GET /api/tasks/pool` -- `GET /api/tasks/mine` -- `GET /api/tasks/pending-review` -- `GET /api/tasks` -- `POST /api/tasks` -- `GET /api/tasks/{id}` -- `POST /api/tasks/{id}/claim` -- `POST /api/tasks/{id}/unclaim` -- `POST /api/tasks/{id}/reclaim` -- `PUT /api/tasks/{id}/reassign` -### 6. 提取标注 +## 7. source 场景流程与状态流转/回退 -- `GET /api/extraction/{taskId}` -- `PUT /api/extraction/{taskId}` -- `POST /api/extraction/{taskId}/submit` -- `POST /api/extraction/{taskId}/approve` -- `POST /api/extraction/{taskId}/reject` +### 7.1 文本/图片上传场景 -### 7. 问答生成 +流程: -- `GET /api/qa/{taskId}` -- `PUT /api/qa/{taskId}` -- `POST /api/qa/{taskId}/submit` -- `POST /api/qa/{taskId}/approve` -- `POST /api/qa/{taskId}/reject` +1. 前端上传文件到后端。 +2. 后端完成文件校验和对象存储上传。 +3. 后端创建 `source_resource`,状态写入 `UPLOADED`。 +4. 当前一期不引入视频预处理作业,资源校验通过后直接置为 `READY`。 -### 8. 导出与微调 +状态流转: -- `GET /api/training/samples` -- `POST /api/export/batch` -- `POST /api/export/{batchId}/finetune` -- `GET /api/export/{batchId}/status` -- `GET /api/export/list` +`UPLOADED -> READY -> ARCHIVED` -### 9. 系统配置 +回退规则: -- `GET /api/config` -- `PUT /api/config/{key}` +- 如果对象存储上传失败:不写库。 +- 如果写库失败:回滚并删除已上传对象。 +- 如果资源已被任务引用,不允许直接物理删除,只允许逻辑归档到 `ARCHIVED`。 -### 10. 视频处理 +### 7.2 视频资源当前处理策略 -- `POST /api/video/process` -- `GET /api/video/jobs/{jobId}` -- `POST /api/video/jobs/{jobId}/reset` -- `POST /api/video/callback` +一期暂不建设独立的视频资源预处理作业,视频资源和文本/图片资源保持同样的主流程: -## 定时任务 +1. 视频上传成功后创建 `source_resource`,状态为 `UPLOADED`。 +2. 完成基础校验后直接改为 `READY`。 +3. 当前不做拆帧、转写、衍生文件生产。 -当前项目中**没有启用 Spring `@Scheduled` 定时同步任务**。 +状态流转: -现有异步能力主要通过以下方式完成: +`UPLOADED -> READY -> ARCHIVED` -- 事务提交后事件监听 - - 提取审批通过后触发问答生成 -- 外部 AI 服务异步回调 - - 视频处理完成后回调 `/api/video/callback` -- Redis 分布式锁 - - 用于任务领取并发控制 +### 7.3 任务驱动场景 -如果后续需要周期性任务,建议单独引入明确的调度场景,不要复用当前业务链路中的事件机制。 +资源与任务解耦,资源不会因为某个任务开始执行就改成任务态。即: -## 部署说明 +- 创建任务不会把资源状态从 `READY` 改成 `EXTRACTING` +- 任务重跑不会污染资源主状态 -### 1. 数据库初始化 +这是本次重写的核心结论之一。 -初始化 SQL 位于: +## 8. 模块详细设计 -- 开发/部署初始化脚本 - - [src/main/resources/sql/init.sql](d:/workspace/label/label_backend/src/main/resources/sql/init.sql) +## 8.1 资源管理模块 -说明: +### 8.1.1 模块职责 -- `src/main/resources/sql/init.sql` 会随源码保存,但**不会被打入 jar、target/classes 或分发包** -- `docker-compose.yml` 通过挂载该文件完成 PostgreSQL 初始化 +1. 接收文本、图片、视频文件上传。 +2. 保存资源元数据与对象存储定位信息。 +3. 提供分页查询、详情查询、删除能力。 +4. 为任务创建与结果审核提供统一的资源视图,不引入独立视频预处理作业。 -### 2. 本地构建 +### 8.1.2 接口列表 -```bash -mvn clean package -DskipTests +| 方法 | 路径 | 说明 | +|---|---|---| +| `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; +} ``` -构建产物: -... -- `target/label-backend-1.0.0-SNAPSHOT.zip` -- `target/label-backend-1.0.0-SNAPSHOT.tar.gz` +### 8.1.4 关键规则 -### 3. 分发包结构 +1. 只允许上传 `TEXT / IMAGE / VIDEO`。 +2. `source_status = READY` 的资源才能创建任务。 +3. 已被任务引用的资源删除时,优先做逻辑归档,不直接删除数据库记录。 +4. 删除资源时必须同步清理 RustFS 对象;如清理失败则整体删除失败。 -分发包由 [distribution.xml](d:/workspace/label/label_backend/assembly/distribution.xml) 组装,解压后结构如下: + +## 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 -label-backend-/ -├── bin/ -│ └── start.sh -├── etc/ -│ ├── application.yml -│ └── logback.xml -├── libs/ -│ ├── label-backend-.jar -│ └── *.jar -└── logs/ +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 ``` -### 4. 启动脚本 +## 10.3 `sys_company` -启动脚本位于 [start.sh](d:/workspace/label/label_backend/scripts/start.sh)。 +| 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | +|---|---|---|---|---|---| +| `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` -- 在 Docker 容器中检测到 `/.dockerenv` 时,前台 `exec java ...` -- 在宿主机环境中使用 `nohup` 后台启动 -- 日志默认写入 `logs/startup.log` +| 字段 | 类型 | 可空 | 默认值 | 索引/约束 | 说明 | +|---|---|---|---|---|---| +| `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` | 无 | 更新时间 | -### 5. Docker Compose 启动 +实际索引/约束: -```bash -docker compose up -d +- `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] ``` -当前 `docker-compose.yml` 会启动: +### 11.2 AI 处理到结果入库 -- PostgreSQL -- Redis -- RustFS(当前使用 MinIO 作为 S3 兼容替代) -- backend -- ai-service 占位服务 -- frontend 占位服务 +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` 期间都允许审核员主动发起人工审核;差异仅在于是否允许后续自动归档。 -### 6. Docker 镜像构建 - -```bash -docker build -t label-backend:latest . +```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[等待人工审核或自动归档] ``` -`Dockerfile` 使用多阶段构建,并从项目根目录的 `scripts/start.sh` 复制启动脚本。 +### 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`,从运行态结果集中移除。 -1. 开发模式下 `auth.enabled=false` - - 此时会使用 mock 用户身份,不适合生产环境 - - 生产部署前必须显式启用真实鉴权 -2. 多租户隔离仍依赖 `CompanyContext` + `TenantLineInnerInterceptor` - - 租户表查询默认依赖租户拦截器 - - 个别特殊场景通过显式 `companyId` 参数校验 -3. `sys_config`、`sys_company`、`video_process_job` 属于特殊表 - - 其中部分表被排除出自动租户注入,需在服务层显式控制 -4. SQL 已迁移到 `src/main/resources/sql` - - 仅作为源码级初始化文件保留 - - 不会打进构建产物 -5. 集成测试依赖 Testcontainers - - 运行完整集成测试需要本机可用 Docker 环境 -6. 认证实现已移除 Shiro - - 当前使用自定义拦截器、注解与 Redis Token -7. 用户上下文 ThreadLocal 已移除 - - 当前只保留 `CompanyContext` - - 用户主体通过请求属性中的 `TokenPrincipal` 传递 +```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` 的记录不参与自动归档扫描。 -- 统一扁平目录结构,避免再次引入按业务域分层的旧目录 -- DTO 统一放在 `dto/`,不再拆分 `request/response` -- Service 统一放在 `service/`,不拆 `service/impl` -- 业务规则优先放在 Service,Controller 只负责 HTTP 协议层 -- 新增接口需同步补齐 Swagger 注解与测试 -- 所有对外接口参数必须在 Swagger 中明确体现名称、类型和含义 -- 固定结构请求体禁止继续使用匿名 `Map` 或 `Map`,必须定义 DTO 并补齐 `@Schema` 字段说明 -- 固定结构响应应优先使用明确 DTO,或至少为 Swagger 暴露对象补齐字段级 `@Schema` 注解 -- 路径参数、查询参数、请求体、分页包装和通用返回体都必须维护可读的 OpenAPI 文档说明 -- 需要保持历史兼容的原始 JSON 字符串请求体可以继续使用 `String`,但必须在 Swagger `@RequestBody` 中说明完整 JSON body 的提交方式和兼容原因 -- 修改 Controller 参数、请求 DTO、响应 DTO 或对外实体后,必须运行 `mvn -Dtest=OpenApiAnnotationTest test`,确保 Swagger 参数名称、类型和含义没有回退 -- 目录、配置、打包方式变化后,README、设计文档和部署说明必须同步更新 +```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 调整和接口落地的基准版本。