停止追踪specs,docs等目录文件
This commit is contained in:
@@ -1,517 +0,0 @@
|
|||||||
# Deploy Optimization Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** 将 label_backend 从 Spring Boot fat JAR 方式改造为薄 jar + Maven Assembly 分发包,并统一日志级别为 INFO。
|
|
||||||
|
|
||||||
**Architecture:** 移除 `spring-boot-maven-plugin`,改用 `maven-jar-plugin`(薄 jar)+ `maven-dependency-plugin`(复制依赖到 `target/libs/`)+ `maven-assembly-plugin`(组装 zip/tar.gz)。新增 `start.sh`(Docker/VM 双模式启动)和 `logback.xml`(60 MB 滚动)。Dockerfile 改为多阶段构建,从 `target/libs/` 和 `src/main/resources/` 复制构建产物。
|
|
||||||
|
|
||||||
**Tech Stack:** Maven 3.9, JDK 17, Spring Boot 3.2.5, maven-assembly-plugin 3.x, logback 1.4.x (Spring Boot 管理版本)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 文件结构
|
|
||||||
|
|
||||||
| 操作 | 路径 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| 新建 | `src/main/resources/logback.xml` | INFO 级,60 MB 滚动日志配置 |
|
|
||||||
| 新建 | `src/main/scripts/start.sh` | Docker/VM 双模式启动脚本 |
|
|
||||||
| 新建 | `src/main/assembly/distribution.xml` | Assembly 描述符 |
|
|
||||||
| 新建 | `src/main/assembly/empty-logs/.gitkeep` | logs/ 目录占位(Assembly 引用) |
|
|
||||||
| 修改 | `pom.xml` | 替换 spring-boot-maven-plugin |
|
|
||||||
| 修改 | `Dockerfile` | 多阶段构建,复制 etc/ + libs/ |
|
|
||||||
| 批量修改 | 11 个 Service 类 | `log.debug` → `log.info`(21 处) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 1: 创建 logback.xml
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/resources/logback.xml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 创建 logback.xml 文件**
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<configuration scan="true" scanPeriod="60 seconds">
|
|
||||||
|
|
||||||
<property name="LOG_PATH" value="${LOG_PATH:-logs}"/>
|
|
||||||
<property name="APP_NAME" value="label-backend"/>
|
|
||||||
<property name="LOG_PATTERN"
|
|
||||||
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
|
|
||||||
|
|
||||||
<!-- 控制台输出(Docker 日志采集依赖 stdout) -->
|
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
|
||||||
<encoder charset="UTF-8">
|
|
||||||
<pattern>${LOG_PATTERN}</pattern>
|
|
||||||
</encoder>
|
|
||||||
</appender>
|
|
||||||
|
|
||||||
<!-- 滚动文件:60 MB / 个,按日分组,保留 30 天,总上限 3 GB -->
|
|
||||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
|
||||||
<file>${LOG_PATH}/${APP_NAME}.log</file>
|
|
||||||
<encoder charset="UTF-8">
|
|
||||||
<pattern>${LOG_PATTERN}</pattern>
|
|
||||||
</encoder>
|
|
||||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
|
||||||
<fileNamePattern>${LOG_PATH}/${APP_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
|
|
||||||
<maxFileSize>60MB</maxFileSize>
|
|
||||||
<maxHistory>30</maxHistory>
|
|
||||||
<totalSizeCap>3GB</totalSizeCap>
|
|
||||||
</rollingPolicy>
|
|
||||||
</appender>
|
|
||||||
|
|
||||||
<root level="INFO">
|
|
||||||
<appender-ref ref="CONSOLE"/>
|
|
||||||
<appender-ref ref="FILE"/>
|
|
||||||
</root>
|
|
||||||
|
|
||||||
</configuration>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 验证文件存在**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls src/main/resources/logback.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
预期输出:`src/main/resources/logback.xml`
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/main/resources/logback.xml
|
|
||||||
git commit -m "feat(deploy): 添加 logback.xml(INFO 级,60 MB 滚动)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 2: 创建 start.sh 启动脚本
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/scripts/start.sh`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 创建目录并写入 start.sh**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p src/main/scripts
|
|
||||||
```
|
|
||||||
|
|
||||||
文件内容 `src/main/scripts/start.sh`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
# label-backend 启动脚本
|
|
||||||
# - Docker 环境(检测 /.dockerenv):exec 前台运行,保持容器进程存活
|
|
||||||
# - 裸机 / VM:nohup 后台运行,日志追加至 logs/startup.log
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
BASEDIR=$(cd "$(dirname "$0")/.." && pwd)
|
|
||||||
LIBDIR="$BASEDIR/libs"
|
|
||||||
CONFDIR="$BASEDIR/etc"
|
|
||||||
LOGDIR="$BASEDIR/logs"
|
|
||||||
|
|
||||||
mkdir -p "$LOGDIR"
|
|
||||||
|
|
||||||
JVM_OPTS="${JVM_OPTS:--Xms512m -Xmx1024m}"
|
|
||||||
MAIN_CLASS="com.label.LabelBackendApplication"
|
|
||||||
JAVA_ARGS="$JVM_OPTS \
|
|
||||||
-Dspring.config.location=file:$CONFDIR/application.yml \
|
|
||||||
-Dlogging.config=file:$CONFDIR/logback.xml \
|
|
||||||
-cp $LIBDIR/*"
|
|
||||||
|
|
||||||
if [ -f /.dockerenv ]; then
|
|
||||||
# Docker 容器:exec 替换当前进程,PID=1 接管信号
|
|
||||||
exec java $JAVA_ARGS $MAIN_CLASS
|
|
||||||
else
|
|
||||||
# 裸机 / VM:nohup 后台运行
|
|
||||||
nohup java $JAVA_ARGS $MAIN_CLASS >> "$LOGDIR/startup.log" 2>&1 &
|
|
||||||
echo "label-backend started, PID=$!"
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 验证文件存在**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls src/main/scripts/start.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
预期输出:`src/main/scripts/start.sh`
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/main/scripts/start.sh
|
|
||||||
git commit -m "feat(deploy): 添加 start.sh(Docker exec / VM nohup 双模式)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 3: 创建 Assembly 描述符和目录占位
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/assembly/distribution.xml`
|
|
||||||
- Create: `src/main/assembly/empty-logs/.gitkeep`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 创建 assembly 目录结构**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p src/main/assembly/empty-logs
|
|
||||||
touch src/main/assembly/empty-logs/.gitkeep
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 创建 distribution.xml**
|
|
||||||
|
|
||||||
文件内容 `src/main/assembly/distribution.xml`:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0
|
|
||||||
https://maven.apache.org/xsd/assembly-2.1.0.xsd">
|
|
||||||
<id>dist</id>
|
|
||||||
<formats>
|
|
||||||
<format>zip</format>
|
|
||||||
<format>tar.gz</format>
|
|
||||||
</formats>
|
|
||||||
<includeBaseDirectory>true</includeBaseDirectory>
|
|
||||||
|
|
||||||
<!-- bin/start.sh(0755 可执行) -->
|
|
||||||
<files>
|
|
||||||
<file>
|
|
||||||
<source>src/main/scripts/start.sh</source>
|
|
||||||
<outputDirectory>bin</outputDirectory>
|
|
||||||
<fileMode>0755</fileMode>
|
|
||||||
</file>
|
|
||||||
</files>
|
|
||||||
|
|
||||||
<fileSets>
|
|
||||||
<!-- etc/:application.yml + logback.xml -->
|
|
||||||
<fileSet>
|
|
||||||
<directory>src/main/resources</directory>
|
|
||||||
<outputDirectory>etc</outputDirectory>
|
|
||||||
<includes>
|
|
||||||
<include>application.yml</include>
|
|
||||||
<include>logback.xml</include>
|
|
||||||
</includes>
|
|
||||||
</fileSet>
|
|
||||||
|
|
||||||
<!-- libs/:薄 jar + 所有运行时依赖 -->
|
|
||||||
<fileSet>
|
|
||||||
<directory>${project.build.directory}/libs</directory>
|
|
||||||
<outputDirectory>libs</outputDirectory>
|
|
||||||
<includes>
|
|
||||||
<include>**/*.jar</include>
|
|
||||||
</includes>
|
|
||||||
</fileSet>
|
|
||||||
|
|
||||||
<!-- logs/ 空目录占位 -->
|
|
||||||
<fileSet>
|
|
||||||
<directory>src/main/assembly/empty-logs</directory>
|
|
||||||
<outputDirectory>logs</outputDirectory>
|
|
||||||
</fileSet>
|
|
||||||
</fileSets>
|
|
||||||
|
|
||||||
</assembly>
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 3: 验证文件存在**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls src/main/assembly/distribution.xml src/main/assembly/empty-logs/.gitkeep
|
|
||||||
```
|
|
||||||
|
|
||||||
预期输出:两个文件路径均显示
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/main/assembly/
|
|
||||||
git commit -m "feat(deploy): 添加 Assembly 描述符 distribution.xml"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 4: 更新 pom.xml(替换 spring-boot-maven-plugin)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `pom.xml`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 替换 `<build><plugins>` 段落**
|
|
||||||
|
|
||||||
将 `pom.xml` 的 `<build>` 段(当前仅含 spring-boot-maven-plugin)替换为:
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<build>
|
|
||||||
<plugins>
|
|
||||||
|
|
||||||
<!-- 薄 jar:仅打包编译后的 class,输出到 target/libs/ -->
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-jar-plugin</artifactId>
|
|
||||||
<configuration>
|
|
||||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
|
||||||
<archive>
|
|
||||||
<manifest>
|
|
||||||
<mainClass>com.label.LabelBackendApplication</mainClass>
|
|
||||||
<addClasspath>false</addClasspath>
|
|
||||||
</manifest>
|
|
||||||
</archive>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
<!-- 将所有运行时依赖复制到 target/libs/ -->
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-dependency-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>copy-dependencies</id>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals><goal>copy-dependencies</goal></goals>
|
|
||||||
<configuration>
|
|
||||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
|
||||||
<includeScope>runtime</includeScope>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
<!-- 组装分发包(zip + tar.gz) -->
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-assembly-plugin</artifactId>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<id>create-distribution</id>
|
|
||||||
<phase>package</phase>
|
|
||||||
<goals><goal>single</goal></goals>
|
|
||||||
<configuration>
|
|
||||||
<descriptors>
|
|
||||||
<descriptor>src/main/assembly/distribution.xml</descriptor>
|
|
||||||
</descriptors>
|
|
||||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
|
||||||
<appendAssemblyId>false</appendAssemblyId>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
```
|
|
||||||
|
|
||||||
即用上述内容完整替换 `pom.xml` 中现有的 `<build>...</build>` 块(原内容为含 spring-boot-maven-plugin 的单插件配置)。
|
|
||||||
|
|
||||||
- [ ] **Step 2: 验证语法并试构建**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mvn validate -q
|
|
||||||
```
|
|
||||||
|
|
||||||
预期输出:无错误(只做 pom.xml 解析校验,不实际编译)
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add pom.xml
|
|
||||||
git commit -m "feat(deploy): pom.xml 替换 fat JAR → 薄 jar + maven-dependency + maven-assembly"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 5: 更新 Dockerfile
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `Dockerfile`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 替换 Dockerfile 全文**
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# 构建阶段:Maven + JDK 17 编译,生成薄 jar 及依赖
|
|
||||||
FROM maven:3.9-eclipse-temurin-17-alpine AS builder
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 优先复制 pom.xml 利用 Docker 层缓存(依赖不变时跳过 go-offline)
|
|
||||||
COPY pom.xml .
|
|
||||||
RUN mvn dependency:go-offline -q
|
|
||||||
|
|
||||||
COPY src ./src
|
|
||||||
RUN mvn clean package -DskipTests -q
|
|
||||||
|
|
||||||
# 运行阶段:仅含 JRE 的精简镜像
|
|
||||||
FROM eclipse-temurin:17-jre-alpine
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# 复制部署结构:bin/ libs/ etc/
|
|
||||||
COPY --from=builder /app/src/main/scripts/start.sh bin/start.sh
|
|
||||||
COPY --from=builder /app/target/libs/ libs/
|
|
||||||
COPY --from=builder /app/src/main/resources/application.yml etc/application.yml
|
|
||||||
COPY --from=builder /app/src/main/resources/logback.xml etc/logback.xml
|
|
||||||
|
|
||||||
RUN mkdir -p logs && chmod +x bin/start.sh
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# start.sh 检测到 /.dockerenv 后以 exec 前台方式运行
|
|
||||||
ENTRYPOINT ["bin/start.sh"]
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 验证 Dockerfile 语法**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build --no-cache --dry-run . 2>/dev/null || echo "docker not available; syntax check skipped"
|
|
||||||
```
|
|
||||||
|
|
||||||
预期:无语法错误(或 docker 不可用时跳过)
|
|
||||||
|
|
||||||
- [ ] **Step 3: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add Dockerfile
|
|
||||||
git commit -m "feat(deploy): Dockerfile 改为多阶段构建(薄 jar + start.sh)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 6: 批量替换 log.debug → log.info(11 文件,21 处)
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/main/java/com/label/module/video/service/VideoProcessService.java` (5 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/source/service/SourceService.java` (2 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/user/service/UserService.java` (3 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/user/service/AuthService.java` (2 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/config/service/SysConfigService.java` (2 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/service/ExtractionApprovedEventListener.java` (2 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/task/service/TaskService.java` (1 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/service/QaService.java` (1 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/export/service/FinetuneService.java` (1 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/task/service/TaskClaimService.java` (1 处)
|
|
||||||
- Modify: `src/main/java/com/label/module/export/service/ExportService.java` (1 处)
|
|
||||||
|
|
||||||
- [ ] **Step 1: 批量替换**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
find src/main/java -name "*.java" \
|
|
||||||
-exec sed -i 's/log\.debug(/log.info(/g' {} +
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 验证替换完整,无 log.debug 残留**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
grep -r "log\.debug" src/main/java && echo "FAIL: 仍有 log.debug 残留" || echo "PASS: 无 log.debug"
|
|
||||||
```
|
|
||||||
|
|
||||||
预期输出:`PASS: 无 log.debug`
|
|
||||||
|
|
||||||
- [ ] **Step 3: 验证替换数量正确(应有 21 处 log.info 新增)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git diff --stat src/main/java | tail -1
|
|
||||||
```
|
|
||||||
|
|
||||||
预期:显示 11 个文件,21 处改动(`21 insertions(+), 21 deletions(-)`)
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/main/java/
|
|
||||||
git commit -m "refactor(log): log.debug 全量替换为 log.info(11 文件,21 处)"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Task 7: 全量构建验证
|
|
||||||
|
|
||||||
**Files:** 只读操作,不修改文件
|
|
||||||
|
|
||||||
- [ ] **Step 1: 执行完整构建(跳过测试)**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mvn clean package -DskipTests
|
|
||||||
```
|
|
||||||
|
|
||||||
预期:`BUILD SUCCESS`,无 ERROR 输出
|
|
||||||
|
|
||||||
- [ ] **Step 2: 验证 target/libs/ 目录存在且包含 jar**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls target/libs/*.jar | head -5
|
|
||||||
ls target/libs/label-backend-1.0.0-SNAPSHOT.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
预期:显示薄 jar 及多个依赖 jar
|
|
||||||
|
|
||||||
- [ ] **Step 3: 验证分发包已生成**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ls target/label-backend-1.0.0-SNAPSHOT.zip
|
|
||||||
ls target/label-backend-1.0.0-SNAPSHOT.tar.gz
|
|
||||||
```
|
|
||||||
|
|
||||||
预期:两个文件均存在
|
|
||||||
|
|
||||||
- [ ] **Step 4: 验证分发包内部结构**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 使用 unzip 检查 zip 内容(Windows Git Bash 或 Linux 均可用)
|
|
||||||
unzip -l target/label-backend-1.0.0-SNAPSHOT.zip | grep -E "bin/|etc/|libs/|logs/"
|
|
||||||
```
|
|
||||||
|
|
||||||
预期输出包含:
|
|
||||||
```
|
|
||||||
label-backend-1.0.0-SNAPSHOT/bin/start.sh
|
|
||||||
label-backend-1.0.0-SNAPSHOT/etc/application.yml
|
|
||||||
label-backend-1.0.0-SNAPSHOT/etc/logback.xml
|
|
||||||
label-backend-1.0.0-SNAPSHOT/libs/label-backend-1.0.0-SNAPSHOT.jar
|
|
||||||
label-backend-1.0.0-SNAPSHOT/logs/
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: 验证 logback.xml 已包含在 etc/ 内**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
unzip -l target/label-backend-1.0.0-SNAPSHOT.zip | grep logback
|
|
||||||
```
|
|
||||||
|
|
||||||
预期:`label-backend-1.0.0-SNAPSHOT/etc/logback.xml`
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit 构建验证记录(如有必要则修正问题后提交)**
|
|
||||||
|
|
||||||
若 Step 1-5 全部通过,无需额外提交。若发现问题(如缺少文件、路径错误),修正对应 Task 后重新执行本 Task。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 自检
|
|
||||||
|
|
||||||
### Spec 覆盖检查
|
|
||||||
|
|
||||||
| deploy.md 需求 | 对应任务 |
|
|
||||||
|---------------|---------|
|
|
||||||
| 1. 分发包结构 bin/etc/libs/logs | Task 3 (distribution.xml) + Task 7 验证 |
|
|
||||||
| 2. start.sh nohup 后台启动 | Task 2 |
|
|
||||||
| 3. logback.xml INFO + 60 MB 滚动 | Task 1 |
|
|
||||||
| 4. logback.xml + application.yml 在 etc/ | Task 3 (distribution.xml fileSets) |
|
|
||||||
| 5. maven-jar-plugin + maven-dependency-plugin | Task 4 |
|
|
||||||
| 6. Maven Assembly Plugin → zip/tar.gz | Task 3 + Task 4 |
|
|
||||||
| 7. Dockerfile 复制 etc/ + 调用 start.sh | Task 5 |
|
|
||||||
| 8. log.debug → log.info | Task 6 |
|
|
||||||
|
|
||||||
所有 8 条需求均有对应任务。✅
|
|
||||||
|
|
||||||
### 类型一致性
|
|
||||||
|
|
||||||
- `MAIN_CLASS="com.label.LabelBackendApplication"` — 与 `src/main/java/com/label/LabelBackendApplication.java` 一致 ✅
|
|
||||||
- `maven-jar-plugin` 输出路径 `target/libs/` — 与 Dockerfile `COPY --from=builder /app/target/libs/` 一致 ✅
|
|
||||||
- `distribution.xml` `<directory>${project.build.directory}/libs</directory>` — 与 `maven-dependency-plugin` 的 `outputDirectory` 一致 ✅
|
|
||||||
|
|
||||||
## GSTACK REVIEW REPORT
|
|
||||||
|
|
||||||
| Review | Trigger | Why | Runs | Status | Findings |
|
|
||||||
|--------|---------|-----|------|--------|----------|
|
|
||||||
| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — |
|
|
||||||
| Codex Review | `/codex review` | Independent 2nd opinion | 0 | — | — |
|
|
||||||
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 0 | — | — |
|
|
||||||
| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — |
|
|
||||||
|
|
||||||
**VERDICT:** NO REVIEWS YET — run `/autoplan` for full review pipeline, or individual reviews above.
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
|||||||
# Auth And Company Optimization Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** Replace the remaining Shiro authorization layer with project-owned Redis token authentication and add company CRUD APIs.
|
|
||||||
|
|
||||||
**Architecture:** Keep the existing UUID token, Redis session storage, and `CompanyContext` tenant injection. Add project-owned `@RequireAuth` and `@RequireRole` annotations plus a Spring MVC `AuthInterceptor`, then remove Shiro config/classes/dependencies. Add `CompanyService` and `CompanyController` for `sys_company` management.
|
|
||||||
|
|
||||||
**Tech Stack:** Java 21, Spring Boot 3.1.5, Spring MVC HandlerInterceptor, RedisTemplate, MyBatis-Plus, JUnit 5, Mockito, AssertJ.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: Replace Shiro With Custom Auth Interceptor
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/java/com/label/annotation/RequireAuth.java`
|
|
||||||
- Create: `src/main/java/com/label/annotation/RequireRole.java`
|
|
||||||
- Create: `src/main/java/com/label/interceptor/AuthInterceptor.java`
|
|
||||||
- Create: `src/main/java/com/label/common/auth/TokenPrincipal.java`
|
|
||||||
- Create: `src/main/java/com/label/common/context/UserContext.java`
|
|
||||||
- Modify: `src/main/java/com/label/config/ShiroConfig.java`
|
|
||||||
- Modify: `src/main/java/com/label/common/shiro/TokenFilter.java`
|
|
||||||
- Modify: `src/main/java/com/label/common/shiro/BearerToken.java`
|
|
||||||
- Modify: `src/main/java/com/label/common/shiro/UserRealm.java`
|
|
||||||
- Modify: `src/main/java/com/label/controller/*.java`
|
|
||||||
- Modify: `src/main/java/com/label/service/*.java`
|
|
||||||
- Modify: `pom.xml`
|
|
||||||
- Test: `src/test/java/com/label/unit/AuthInterceptorTest.java`
|
|
||||||
|
|
||||||
- [x] Write failing tests for token loading, TTL refresh, role hierarchy, and context cleanup.
|
|
||||||
- [x] Implement annotations, principal, context, and interceptor.
|
|
||||||
- [x] Register the interceptor via Spring MVC config.
|
|
||||||
- [x] Replace controller `@RequiresRoles` usage with `@RequireRole`.
|
|
||||||
- [x] Remove Shiro-only classes, tests, dependencies, and exception handling.
|
|
||||||
- [x] Run `mvn -q "-Dtest=AuthInterceptorTest,OpenApiAnnotationTest" test` and `mvn -q -DskipTests compile`.
|
|
||||||
|
|
||||||
### Task 2: Add Company Management
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/java/com/label/service/CompanyService.java`
|
|
||||||
- Create: `src/main/java/com/label/controller/CompanyController.java`
|
|
||||||
- Modify: `src/main/java/com/label/mapper/SysUserMapper.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/CompanyServiceTest.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/OpenApiAnnotationTest.java`
|
|
||||||
|
|
||||||
- [x] Write failing tests for create/list/update/status/delete behavior.
|
|
||||||
- [x] Implement service validation and duplicate checks.
|
|
||||||
- [x] Implement admin-only controller endpoints under `/api/companies`.
|
|
||||||
- [x] Run `mvn -q "-Dtest=CompanyServiceTest,OpenApiAnnotationTest" test` and `mvn -q -DskipTests compile`.
|
|
||||||
|
|
||||||
### Task 3: Configuration And Verification
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/main/resources/application.yml`
|
|
||||||
- Modify: `src/test/java/com/label/unit/ApplicationConfigTest.java`
|
|
||||||
|
|
||||||
- [x] Rename `shiro.auth.*` config to `auth.*`.
|
|
||||||
- [x] Update safe defaults and type-aliases package.
|
|
||||||
- [x] Run targeted unit tests and compile.
|
|
||||||
- [x] Run `mvn clean test` once and record any external environment blockers.
|
|
||||||
|
|
||||||
### Verification Notes
|
|
||||||
|
|
||||||
- `mvn -q "-Dtest=LabelBackendApplicationTests,ApplicationConfigTest,AuthInterceptorTest,CompanyServiceTest,OpenApiAnnotationTest" test` passed.
|
|
||||||
- `mvn -q -DskipTests compile` passed.
|
|
||||||
- `mvn clean test` compiled main/test sources and passed unit tests, then failed only because 10 Testcontainers integration tests could not find a valid Docker environment.
|
|
||||||
@@ -1,528 +0,0 @@
|
|||||||
# label_backend 标准目录扁平化 Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** 把 `com.label.module.*` 与 `com.label.common.aop/common.config` 迁移到规范要求的扁平目录,保证行为不变。
|
|
||||||
|
|
||||||
**Architecture:** 先写迁移守卫测试,再按 `基础设施 -> 数据层 -> 服务层 -> 控制层 -> 清理回归` 的顺序做 `git mv` 和导包修正,每阶段都用最小测试集验证。
|
|
||||||
|
|
||||||
**Tech Stack:** Java 21、Spring Boot 3.1.5、MyBatis-Plus、Shiro、JUnit 5、AssertJ、Maven
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目标目录
|
|
||||||
|
|
||||||
- `src/main/java/com/label/annotation`
|
|
||||||
- `src/main/java/com/label/aspect`
|
|
||||||
- `src/main/java/com/label/config`
|
|
||||||
- `src/main/java/com/label/controller`
|
|
||||||
- `src/main/java/com/label/dto`
|
|
||||||
- `src/main/java/com/label/entity`
|
|
||||||
- `src/main/java/com/label/event`
|
|
||||||
- `src/main/java/com/label/listener`
|
|
||||||
- `src/main/java/com/label/mapper`
|
|
||||||
- `src/main/java/com/label/service`
|
|
||||||
- `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: 锁定基础设施迁移目标
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
- Modify: `src/main/java/com/label/common/aop/OperationLog.java`
|
|
||||||
- Modify: `src/main/java/com/label/common/aop/AuditAspect.java`
|
|
||||||
- Modify: `src/main/java/com/label/common/config/MybatisPlusConfig.java`
|
|
||||||
- Modify: `src/main/java/com/label/common/config/OpenApiConfig.java`
|
|
||||||
- Modify: `src/main/java/com/label/common/config/RedisConfig.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/event/ExtractionApprovedEvent.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/service/ExtractionApprovedEventListener.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/service/ExtractionService.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试**
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.label.unit;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
|
|
||||||
@DisplayName("标准目录扁平化迁移守卫测试")
|
|
||||||
class PackageStructureMigrationTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("基础设施类已迁移到目标目录")
|
|
||||||
void infrastructureTypesMoved() {
|
|
||||||
assertClassExists("com.label.annotation.OperationLog");
|
|
||||||
assertClassExists("com.label.aspect.AuditAspect");
|
|
||||||
assertClassExists("com.label.config.MybatisPlusConfig");
|
|
||||||
assertClassExists("com.label.config.OpenApiConfig");
|
|
||||||
assertClassExists("com.label.config.RedisConfig");
|
|
||||||
assertClassExists("com.label.event.ExtractionApprovedEvent");
|
|
||||||
assertClassExists("com.label.listener.ExtractionApprovedEventListener");
|
|
||||||
|
|
||||||
assertClassMissing("com.label.common.aop.OperationLog");
|
|
||||||
assertClassMissing("com.label.common.aop.AuditAspect");
|
|
||||||
assertClassMissing("com.label.common.config.MybatisPlusConfig");
|
|
||||||
assertClassMissing("com.label.common.config.OpenApiConfig");
|
|
||||||
assertClassMissing("com.label.common.config.RedisConfig");
|
|
||||||
assertClassMissing("com.label.module.annotation.event.ExtractionApprovedEvent");
|
|
||||||
assertClassMissing("com.label.module.annotation.service.ExtractionApprovedEventListener");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertClassExists(String fqcn) {
|
|
||||||
assertThatCode(() -> Class.forName(fqcn)).doesNotThrowAnyException();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertClassMissing(String fqcn) {
|
|
||||||
assertThatThrownBy(() -> Class.forName(fqcn)).isInstanceOf(ClassNotFoundException.class);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 跑红**
|
|
||||||
|
|
||||||
Run: `mvn -q "-Dtest=PackageStructureMigrationTest#infrastructureTypesMoved" test`
|
|
||||||
|
|
||||||
Expected: FAIL,提示新包类不存在。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 最小实现**
|
|
||||||
|
|
||||||
先执行迁移:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git mv src/main/java/com/label/common/aop/OperationLog.java src/main/java/com/label/annotation/OperationLog.java
|
|
||||||
git mv src/main/java/com/label/common/aop/AuditAspect.java src/main/java/com/label/aspect/AuditAspect.java
|
|
||||||
git mv src/main/java/com/label/common/config/MybatisPlusConfig.java src/main/java/com/label/config/MybatisPlusConfig.java
|
|
||||||
git mv src/main/java/com/label/common/config/OpenApiConfig.java src/main/java/com/label/config/OpenApiConfig.java
|
|
||||||
git mv src/main/java/com/label/common/config/RedisConfig.java src/main/java/com/label/config/RedisConfig.java
|
|
||||||
git mv src/main/java/com/label/module/annotation/event/ExtractionApprovedEvent.java src/main/java/com/label/event/ExtractionApprovedEvent.java
|
|
||||||
git mv src/main/java/com/label/module/annotation/service/ExtractionApprovedEventListener.java src/main/java/com/label/listener/ExtractionApprovedEventListener.java
|
|
||||||
```
|
|
||||||
|
|
||||||
再做精确替换:
|
|
||||||
|
|
||||||
```java
|
|
||||||
// OperationLog.java
|
|
||||||
package com.label.annotation;
|
|
||||||
|
|
||||||
// AuditAspect.java
|
|
||||||
package com.label.aspect;
|
|
||||||
import com.label.annotation.OperationLog;
|
|
||||||
|
|
||||||
// MybatisPlusConfig.java / OpenApiConfig.java / RedisConfig.java
|
|
||||||
package com.label.config;
|
|
||||||
|
|
||||||
// ExtractionApprovedEvent.java
|
|
||||||
package com.label.event;
|
|
||||||
|
|
||||||
// ExtractionApprovedEventListener.java
|
|
||||||
package com.label.listener;
|
|
||||||
import com.label.event.ExtractionApprovedEvent;
|
|
||||||
import com.label.module.annotation.entity.TrainingDataset;
|
|
||||||
import com.label.module.annotation.mapper.AnnotationResultMapper;
|
|
||||||
import com.label.module.annotation.mapper.TrainingDatasetMapper;
|
|
||||||
import com.label.module.source.entity.SourceData;
|
|
||||||
import com.label.module.source.mapper.SourceDataMapper;
|
|
||||||
import com.label.module.task.service.TaskService;
|
|
||||||
```
|
|
||||||
|
|
||||||
并把 `src/main/java/com/label/module/annotation/service/ExtractionService.java` 中事件 import 改成 `com.label.event.ExtractionApprovedEvent`。
|
|
||||||
|
|
||||||
- [ ] **Step 4: 跑绿**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
- `mvn -q "-Dtest=PackageStructureMigrationTest#infrastructureTypesMoved" test`
|
|
||||||
- `mvn -q -DskipTests compile`
|
|
||||||
|
|
||||||
Expected: 两条命令都 `BUILD SUCCESS`。
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git add src/test/java/com/label/unit/PackageStructureMigrationTest.java src/main/java/com/label/annotation src/main/java/com/label/aspect src/main/java/com/label/config src/main/java/com/label/event src/main/java/com/label/listener src/main/java/com/label/module/annotation/service/ExtractionService.java
|
|
||||||
git commit -m "refactor: flatten infrastructure packages"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 2: 迁移 DTO、Entity、Mapper
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
- Modify: `src/test/java/com/label/unit/OpenApiAnnotationTest.java`
|
|
||||||
- Modify: `src/test/java/com/label/integration/AuthIntegrationTest.java`
|
|
||||||
- Modify: `src/test/java/com/label/integration/ExtractionApprovalIntegrationTest.java`
|
|
||||||
- Modify: `src/test/java/com/label/integration/QaApprovalIntegrationTest.java`
|
|
||||||
- Modify: `src/test/java/com/label/integration/UserManagementIntegrationTest.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/service/ExtractionService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/service/QaService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/config/service/SysConfigService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/config/controller/SysConfigController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/export/service/ExportService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/export/service/FinetuneService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/export/controller/ExportController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/source/service/SourceService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/source/controller/SourceController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/task/service/TaskClaimService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/task/service/TaskService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/task/controller/TaskController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/user/service/AuthService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/user/service/UserService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/user/controller/AuthController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/user/controller/UserController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/video/service/VideoProcessService.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/video/controller/VideoController.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试**
|
|
||||||
|
|
||||||
在 `PackageStructureMigrationTest.java` 里追加:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Test
|
|
||||||
@DisplayName("DTO、实体、Mapper 已迁移到扁平数据层")
|
|
||||||
void dataTypesMoved() {
|
|
||||||
for (String fqcn : java.util.List.of(
|
|
||||||
"com.label.dto.LoginRequest", "com.label.dto.LoginResponse", "com.label.dto.UserInfoResponse",
|
|
||||||
"com.label.dto.TaskResponse", "com.label.dto.SourceResponse",
|
|
||||||
"com.label.entity.AnnotationResult", "com.label.entity.TrainingDataset", "com.label.entity.SysConfig",
|
|
||||||
"com.label.entity.ExportBatch", "com.label.entity.SourceData", "com.label.entity.AnnotationTask",
|
|
||||||
"com.label.entity.AnnotationTaskHistory", "com.label.entity.SysCompany", "com.label.entity.SysUser",
|
|
||||||
"com.label.entity.VideoProcessJob",
|
|
||||||
"com.label.mapper.AnnotationResultMapper", "com.label.mapper.TrainingDatasetMapper",
|
|
||||||
"com.label.mapper.SysConfigMapper", "com.label.mapper.ExportBatchMapper", "com.label.mapper.SourceDataMapper",
|
|
||||||
"com.label.mapper.AnnotationTaskMapper", "com.label.mapper.TaskHistoryMapper",
|
|
||||||
"com.label.mapper.SysCompanyMapper", "com.label.mapper.SysUserMapper", "com.label.mapper.VideoProcessJobMapper")) {
|
|
||||||
assertClassExists(fqcn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 跑红**
|
|
||||||
|
|
||||||
Run: `mvn -q "-Dtest=PackageStructureMigrationTest#dataTypesMoved" test`
|
|
||||||
|
|
||||||
Expected: FAIL,提示 `com.label.dto.LoginRequest` 等不存在。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 最小实现**
|
|
||||||
|
|
||||||
执行迁移:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git mv src/main/java/com/label/module/user/dto/LoginRequest.java src/main/java/com/label/dto/LoginRequest.java
|
|
||||||
git mv src/main/java/com/label/module/user/dto/LoginResponse.java src/main/java/com/label/dto/LoginResponse.java
|
|
||||||
git mv src/main/java/com/label/module/user/dto/UserInfoResponse.java src/main/java/com/label/dto/UserInfoResponse.java
|
|
||||||
git mv src/main/java/com/label/module/task/dto/TaskResponse.java src/main/java/com/label/dto/TaskResponse.java
|
|
||||||
git mv src/main/java/com/label/module/source/dto/SourceResponse.java src/main/java/com/label/dto/SourceResponse.java
|
|
||||||
git mv src/main/java/com/label/module/annotation/entity/AnnotationResult.java src/main/java/com/label/entity/AnnotationResult.java
|
|
||||||
git mv src/main/java/com/label/module/annotation/entity/TrainingDataset.java src/main/java/com/label/entity/TrainingDataset.java
|
|
||||||
git mv src/main/java/com/label/module/config/entity/SysConfig.java src/main/java/com/label/entity/SysConfig.java
|
|
||||||
git mv src/main/java/com/label/module/export/entity/ExportBatch.java src/main/java/com/label/entity/ExportBatch.java
|
|
||||||
git mv src/main/java/com/label/module/source/entity/SourceData.java src/main/java/com/label/entity/SourceData.java
|
|
||||||
git mv src/main/java/com/label/module/task/entity/AnnotationTask.java src/main/java/com/label/entity/AnnotationTask.java
|
|
||||||
git mv src/main/java/com/label/module/task/entity/AnnotationTaskHistory.java src/main/java/com/label/entity/AnnotationTaskHistory.java
|
|
||||||
git mv src/main/java/com/label/module/user/entity/SysCompany.java src/main/java/com/label/entity/SysCompany.java
|
|
||||||
git mv src/main/java/com/label/module/user/entity/SysUser.java src/main/java/com/label/entity/SysUser.java
|
|
||||||
git mv src/main/java/com/label/module/video/entity/VideoProcessJob.java src/main/java/com/label/entity/VideoProcessJob.java
|
|
||||||
git mv src/main/java/com/label/module/annotation/mapper/AnnotationResultMapper.java src/main/java/com/label/mapper/AnnotationResultMapper.java
|
|
||||||
git mv src/main/java/com/label/module/annotation/mapper/TrainingDatasetMapper.java src/main/java/com/label/mapper/TrainingDatasetMapper.java
|
|
||||||
git mv src/main/java/com/label/module/config/mapper/SysConfigMapper.java src/main/java/com/label/mapper/SysConfigMapper.java
|
|
||||||
git mv src/main/java/com/label/module/export/mapper/ExportBatchMapper.java src/main/java/com/label/mapper/ExportBatchMapper.java
|
|
||||||
git mv src/main/java/com/label/module/source/mapper/SourceDataMapper.java src/main/java/com/label/mapper/SourceDataMapper.java
|
|
||||||
git mv src/main/java/com/label/module/task/mapper/AnnotationTaskMapper.java src/main/java/com/label/mapper/AnnotationTaskMapper.java
|
|
||||||
git mv src/main/java/com/label/module/task/mapper/TaskHistoryMapper.java src/main/java/com/label/mapper/TaskHistoryMapper.java
|
|
||||||
git mv src/main/java/com/label/module/user/mapper/SysCompanyMapper.java src/main/java/com/label/mapper/SysCompanyMapper.java
|
|
||||||
git mv src/main/java/com/label/module/user/mapper/SysUserMapper.java src/main/java/com/label/mapper/SysUserMapper.java
|
|
||||||
git mv src/main/java/com/label/module/video/mapper/VideoProcessJobMapper.java src/main/java/com/label/mapper/VideoProcessJobMapper.java
|
|
||||||
```
|
|
||||||
|
|
||||||
统一替换包声明:
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.label.dto;
|
|
||||||
package com.label.entity;
|
|
||||||
package com.label.mapper;
|
|
||||||
```
|
|
||||||
|
|
||||||
然后把上面 `Files` 列表中的旧 `com.label.module.*.(dto|entity|mapper)` import 全部改到新包。
|
|
||||||
|
|
||||||
- [ ] **Step 4: 跑绿**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
- `mvn -q "-Dtest=PackageStructureMigrationTest#dataTypesMoved,OpenApiAnnotationTest,AuthIntegrationTest,ExtractionApprovalIntegrationTest,QaApprovalIntegrationTest,UserManagementIntegrationTest" test`
|
|
||||||
|
|
||||||
Expected: PASS。
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git add src/main/java/com/label/dto src/main/java/com/label/entity src/main/java/com/label/mapper src/test/java/com/label/unit/PackageStructureMigrationTest.java src/test/java/com/label/unit/OpenApiAnnotationTest.java src/test/java/com/label/integration/AuthIntegrationTest.java src/test/java/com/label/integration/ExtractionApprovalIntegrationTest.java src/test/java/com/label/integration/QaApprovalIntegrationTest.java src/test/java/com/label/integration/UserManagementIntegrationTest.java src/main/java/com/label/module
|
|
||||||
git commit -m "refactor: flatten dto entity and mapper packages"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 3: 迁移业务服务层
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/controller/ExtractionController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/annotation/controller/QaController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/config/controller/SysConfigController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/export/controller/ExportController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/source/controller/SourceController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/task/controller/TaskController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/user/controller/AuthController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/user/controller/UserController.java`
|
|
||||||
- Modify: `src/main/java/com/label/module/video/controller/VideoController.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试**
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Test
|
|
||||||
@DisplayName("服务类已迁移到扁平 service 目录")
|
|
||||||
void serviceTypesMoved() {
|
|
||||||
for (String fqcn : java.util.List.of(
|
|
||||||
"com.label.service.ExtractionService", "com.label.service.QaService",
|
|
||||||
"com.label.service.SysConfigService", "com.label.service.ExportService",
|
|
||||||
"com.label.service.FinetuneService", "com.label.service.SourceService",
|
|
||||||
"com.label.service.TaskClaimService", "com.label.service.TaskService",
|
|
||||||
"com.label.service.AuthService", "com.label.service.UserService",
|
|
||||||
"com.label.service.VideoProcessService")) {
|
|
||||||
assertClassExists(fqcn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 跑红**
|
|
||||||
|
|
||||||
Run: `mvn -q "-Dtest=PackageStructureMigrationTest#serviceTypesMoved" test`
|
|
||||||
|
|
||||||
Expected: FAIL,提示 `com.label.service.*` 不存在。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 最小实现**
|
|
||||||
|
|
||||||
执行迁移:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git mv src/main/java/com/label/module/annotation/service/ExtractionService.java src/main/java/com/label/service/ExtractionService.java
|
|
||||||
git mv src/main/java/com/label/module/annotation/service/QaService.java src/main/java/com/label/service/QaService.java
|
|
||||||
git mv src/main/java/com/label/module/config/service/SysConfigService.java src/main/java/com/label/service/SysConfigService.java
|
|
||||||
git mv src/main/java/com/label/module/export/service/ExportService.java src/main/java/com/label/service/ExportService.java
|
|
||||||
git mv src/main/java/com/label/module/export/service/FinetuneService.java src/main/java/com/label/service/FinetuneService.java
|
|
||||||
git mv src/main/java/com/label/module/source/service/SourceService.java src/main/java/com/label/service/SourceService.java
|
|
||||||
git mv src/main/java/com/label/module/task/service/TaskClaimService.java src/main/java/com/label/service/TaskClaimService.java
|
|
||||||
git mv src/main/java/com/label/module/task/service/TaskService.java src/main/java/com/label/service/TaskService.java
|
|
||||||
git mv src/main/java/com/label/module/user/service/AuthService.java src/main/java/com/label/service/AuthService.java
|
|
||||||
git mv src/main/java/com/label/module/user/service/UserService.java src/main/java/com/label/service/UserService.java
|
|
||||||
git mv src/main/java/com/label/module/video/service/VideoProcessService.java src/main/java/com/label/service/VideoProcessService.java
|
|
||||||
```
|
|
||||||
|
|
||||||
统一替换:
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.label.service;
|
|
||||||
```
|
|
||||||
|
|
||||||
并把 `ExtractionController`、`QaController`、`SysConfigController`、`ExportController`、`SourceController`、`TaskController`、`AuthController`、`UserController`、`VideoController` 的服务导包改到 `com.label.service.*`。
|
|
||||||
|
|
||||||
- [ ] **Step 4: 跑绿**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
- `mvn -q "-Dtest=PackageStructureMigrationTest#serviceTypesMoved" test`
|
|
||||||
- `mvn -q -DskipTests compile`
|
|
||||||
|
|
||||||
Expected: PASS。
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git add src/main/java/com/label/service src/main/java/com/label/module/annotation/controller/ExtractionController.java src/main/java/com/label/module/annotation/controller/QaController.java src/main/java/com/label/module/config/controller/SysConfigController.java src/main/java/com/label/module/export/controller/ExportController.java src/main/java/com/label/module/source/controller/SourceController.java src/main/java/com/label/module/task/controller/TaskController.java src/main/java/com/label/module/user/controller/AuthController.java src/main/java/com/label/module/user/controller/UserController.java src/main/java/com/label/module/video/controller/VideoController.java src/test/java/com/label/unit/PackageStructureMigrationTest.java
|
|
||||||
git commit -m "refactor: flatten service packages"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 4: 迁移控制器并收敛 OpenAPI 测试
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
- Modify: `src/test/java/com/label/unit/OpenApiAnnotationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/OpenApiAnnotationTest.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试**
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Test
|
|
||||||
@DisplayName("控制器已迁移到扁平 controller 目录")
|
|
||||||
void controllerTypesMoved() {
|
|
||||||
for (String fqcn : java.util.List.of(
|
|
||||||
"com.label.controller.AuthController", "com.label.controller.UserController",
|
|
||||||
"com.label.controller.SourceController", "com.label.controller.TaskController",
|
|
||||||
"com.label.controller.ExtractionController", "com.label.controller.QaController",
|
|
||||||
"com.label.controller.ExportController", "com.label.controller.SysConfigController",
|
|
||||||
"com.label.controller.VideoController")) {
|
|
||||||
assertClassExists(fqcn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 跑红**
|
|
||||||
|
|
||||||
Run: `mvn -q "-Dtest=PackageStructureMigrationTest#controllerTypesMoved" test`
|
|
||||||
|
|
||||||
Expected: FAIL,提示 `com.label.controller.*` 不存在。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 最小实现**
|
|
||||||
|
|
||||||
执行迁移:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git mv src/main/java/com/label/module/annotation/controller/ExtractionController.java src/main/java/com/label/controller/ExtractionController.java
|
|
||||||
git mv src/main/java/com/label/module/annotation/controller/QaController.java src/main/java/com/label/controller/QaController.java
|
|
||||||
git mv src/main/java/com/label/module/config/controller/SysConfigController.java src/main/java/com/label/controller/SysConfigController.java
|
|
||||||
git mv src/main/java/com/label/module/export/controller/ExportController.java src/main/java/com/label/controller/ExportController.java
|
|
||||||
git mv src/main/java/com/label/module/source/controller/SourceController.java src/main/java/com/label/controller/SourceController.java
|
|
||||||
git mv src/main/java/com/label/module/task/controller/TaskController.java src/main/java/com/label/controller/TaskController.java
|
|
||||||
git mv src/main/java/com/label/module/user/controller/AuthController.java src/main/java/com/label/controller/AuthController.java
|
|
||||||
git mv src/main/java/com/label/module/user/controller/UserController.java src/main/java/com/label/controller/UserController.java
|
|
||||||
git mv src/main/java/com/label/module/video/controller/VideoController.java src/main/java/com/label/controller/VideoController.java
|
|
||||||
```
|
|
||||||
|
|
||||||
统一替换:
|
|
||||||
|
|
||||||
```java
|
|
||||||
package com.label.controller;
|
|
||||||
```
|
|
||||||
|
|
||||||
并把 `OpenApiAnnotationTest.java` 的 import 替换成:
|
|
||||||
|
|
||||||
```java
|
|
||||||
import com.label.controller.AuthController;
|
|
||||||
import com.label.controller.UserController;
|
|
||||||
import com.label.controller.SourceController;
|
|
||||||
import com.label.controller.TaskController;
|
|
||||||
import com.label.controller.ExtractionController;
|
|
||||||
import com.label.controller.QaController;
|
|
||||||
import com.label.controller.ExportController;
|
|
||||||
import com.label.controller.SysConfigController;
|
|
||||||
import com.label.controller.VideoController;
|
|
||||||
import com.label.dto.LoginRequest;
|
|
||||||
import com.label.dto.LoginResponse;
|
|
||||||
import com.label.dto.UserInfoResponse;
|
|
||||||
import com.label.dto.TaskResponse;
|
|
||||||
import com.label.dto.SourceResponse;
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: 跑绿**
|
|
||||||
|
|
||||||
Run: `mvn -q "-Dtest=PackageStructureMigrationTest#controllerTypesMoved,OpenApiAnnotationTest" test`
|
|
||||||
|
|
||||||
Expected: PASS。
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git add src/main/java/com/label/controller src/test/java/com/label/unit/PackageStructureMigrationTest.java src/test/java/com/label/unit/OpenApiAnnotationTest.java
|
|
||||||
git commit -m "refactor: flatten controller packages"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 5: 清理旧包残留并做全量回归
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/test/java/com/label/unit/PackageStructureMigrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/LabelBackendApplicationTests.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/ApplicationConfigTest.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/ShiroConfigTest.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/StateMachineTest.java`
|
|
||||||
- Test: `src/test/java/com/label/unit/TokenFilterTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/AuthIntegrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/ExportIntegrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/ExtractionApprovalIntegrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/MultiTenantIsolationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/QaApprovalIntegrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/ShiroFilterIntegrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/SourceIntegrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/SysConfigIntegrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/TaskClaimConcurrencyTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/UserManagementIntegrationTest.java`
|
|
||||||
- Test: `src/test/java/com/label/integration/VideoCallbackIdempotencyTest.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写最终守卫测试**
|
|
||||||
|
|
||||||
在 `PackageStructureMigrationTest.java` 中补充:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Test
|
|
||||||
@DisplayName("源码中不再引用旧的 module、common.aop、common.config 包")
|
|
||||||
void sourceTreeHasNoLegacyPackageReferences() throws Exception {
|
|
||||||
try (java.util.stream.Stream<java.nio.file.Path> paths = java.nio.file.Files.walk(java.nio.file.Path.of("src"))) {
|
|
||||||
java.util.List<String> violations = paths
|
|
||||||
.filter(path -> path.toString().endsWith(".java"))
|
|
||||||
.map(path -> {
|
|
||||||
try {
|
|
||||||
String text = java.nio.file.Files.readString(path);
|
|
||||||
boolean legacy = text.contains("com.label.module.")
|
|
||||||
|| text.contains("com.label.common.aop")
|
|
||||||
|| text.contains("com.label.common.config");
|
|
||||||
return legacy ? path.toString() : null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(java.util.Objects::nonNull)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
org.assertj.core.api.Assertions.assertThat(violations).isEmpty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 跑红并定位残留**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
- `mvn -q "-Dtest=PackageStructureMigrationTest#sourceTreeHasNoLegacyPackageReferences" test`
|
|
||||||
- `rg -n "com\.label\.module\.|com\.label\.common\.aop|com\.label\.common\.config" src/main/java src/test/java`
|
|
||||||
|
|
||||||
Expected: 初次 FAIL,并列出残留文件。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 清理残留并确认旧目录为空**
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
Get-ChildItem -Path src/main/java/com/label/module -Recurse
|
|
||||||
```
|
|
||||||
|
|
||||||
Expected: 没有 Java 文件残留;确认后删除空目录。
|
|
||||||
|
|
||||||
- [ ] **Step 4: 全量回归**
|
|
||||||
|
|
||||||
Run: `mvn clean test`
|
|
||||||
|
|
||||||
Expected: `BUILD SUCCESS`。
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
git add src/main/java src/test/java
|
|
||||||
git commit -m "refactor: complete backend directory flattening"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 自检
|
|
||||||
|
|
||||||
- 覆盖了 `annotation / aspect / config / event / listener / dto / entity / mapper / service / controller`
|
|
||||||
- 没有引入 `service.impl`
|
|
||||||
- 没有拆分 `dto/request`、`dto/response`
|
|
||||||
- 最终门槛是 `mvn clean test`
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,244 +0,0 @@
|
|||||||
# label_backend 标准目录扁平化设计
|
|
||||||
|
|
||||||
**日期**: 2026-04-14
|
|
||||||
**范围**: `label_backend` 主工程 Java 包结构调整
|
|
||||||
**目标**: 将当前按业务域分层的 `com.label.module.*` 结构重组为符合《微服务开发规范文档》的扁平标准目录结构,同时保持现有业务行为不变。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 背景与现状
|
|
||||||
|
|
||||||
当前项目主代码位于 `src/main/java/com/label`,整体结构分为两部分:
|
|
||||||
|
|
||||||
- `com.label.common.*`:放置公共能力,如配置、异常、AOP、鉴权、Redis、状态机、存储与 AI 客户端
|
|
||||||
- `com.label.module.*`:按业务域划分的模块目录,如 `user`、`task`、`source`、`annotation`、`export`、`config`、`video`
|
|
||||||
|
|
||||||
现有结构具备一定领域边界,但与《微服务开发规范文档》中规定的标准扁平目录结构不一致。后续如果继续沿用两套组织方式,会增加新代码接入成本,也会让公共能力与业务层级的归属变得不稳定。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 设计目标
|
|
||||||
|
|
||||||
本次调整目标如下:
|
|
||||||
|
|
||||||
- 将主代码目录统一调整为规范文档中的扁平结构
|
|
||||||
- 保持包职责清晰,按“层级职责”而不是“业务模块”组织类
|
|
||||||
- 在不改变业务逻辑和接口行为的前提下完成迁移
|
|
||||||
- 规范包归属与少量命名,不引入新的技术分层
|
|
||||||
- 为后续新增功能提供统一、可预测的目录组织方式
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 明确不做的事情
|
|
||||||
|
|
||||||
为控制改造范围,本次不包含以下内容:
|
|
||||||
|
|
||||||
- 不新增 `service.impl`
|
|
||||||
- 不将 `dto` 拆分为 `request`、`response`、`common`
|
|
||||||
- 不主动修改接口 URL、接口契约或返回结构
|
|
||||||
- 不进行与目录调整无关的业务重构
|
|
||||||
- 不改造数据库结构、SQL 文件或资源文件布局
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 目标目录结构
|
|
||||||
|
|
||||||
目标结构定义如下:
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/main/java/com/label/
|
|
||||||
├── annotation/
|
|
||||||
├── aspect/
|
|
||||||
├── common/
|
|
||||||
│ ├── ai/
|
|
||||||
│ ├── context/
|
|
||||||
│ ├── exception/
|
|
||||||
│ ├── redis/
|
|
||||||
│ ├── result/
|
|
||||||
│ ├── shiro/
|
|
||||||
│ ├── statemachine/
|
|
||||||
│ └── storage/
|
|
||||||
├── config/
|
|
||||||
├── constant/
|
|
||||||
├── controller/
|
|
||||||
├── dto/
|
|
||||||
├── entity/
|
|
||||||
├── event/
|
|
||||||
├── feign/
|
|
||||||
├── listener/
|
|
||||||
├── mapper/
|
|
||||||
├── scheduled/
|
|
||||||
├── service/
|
|
||||||
├── typehandler/
|
|
||||||
├── util/
|
|
||||||
└── LabelBackendApplication.java
|
|
||||||
```
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- `common` 只保留真正跨业务复用的基础能力
|
|
||||||
- `config` 从原 `common.config` 提升到顶层
|
|
||||||
- `annotation` 与 `aspect` 从原 `common.aop` 中拆分
|
|
||||||
- `event` 与 `listener` 分离,避免事件定义与监听逻辑混放
|
|
||||||
- `controller`、`service`、`mapper`、`entity`、`dto` 全部收敛为顶层扁平结构
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 包迁移规则
|
|
||||||
|
|
||||||
### 5.1 顶层迁移规则
|
|
||||||
|
|
||||||
- `com.label.module.*.controller` -> `com.label.controller`
|
|
||||||
- `com.label.module.*.service` -> `com.label.service`
|
|
||||||
- `com.label.module.*.mapper` -> `com.label.mapper`
|
|
||||||
- `com.label.module.*.entity` -> `com.label.entity`
|
|
||||||
- `com.label.module.*.dto` -> `com.label.dto`
|
|
||||||
- `com.label.module.annotation.event` -> `com.label.event`
|
|
||||||
- 事件监听类 -> `com.label.listener`
|
|
||||||
- `com.label.common.config` -> `com.label.config`
|
|
||||||
- `com.label.common.aop` 中注解类 -> `com.label.annotation`
|
|
||||||
- `com.label.common.aop` 中切面类 -> `com.label.aspect`
|
|
||||||
|
|
||||||
### 5.2 典型映射示例
|
|
||||||
|
|
||||||
- `com.label.module.user.controller.AuthController` -> `com.label.controller.AuthController`
|
|
||||||
- `com.label.module.user.service.UserService` -> `com.label.service.UserService`
|
|
||||||
- `com.label.module.task.mapper.AnnotationTaskMapper` -> `com.label.mapper.AnnotationTaskMapper`
|
|
||||||
- `com.label.module.source.entity.SourceData` -> `com.label.entity.SourceData`
|
|
||||||
- `com.label.module.user.dto.LoginRequest` -> `com.label.dto.LoginRequest`
|
|
||||||
- `com.label.common.aop.OperationLog` -> `com.label.annotation.OperationLog`
|
|
||||||
- `com.label.common.aop.AuditAspect` -> `com.label.aspect.AuditAspect`
|
|
||||||
- `com.label.module.annotation.event.ExtractionApprovedEvent` -> `com.label.event.ExtractionApprovedEvent`
|
|
||||||
- `com.label.module.annotation.service.ExtractionApprovedEventListener` -> `com.label.listener.ExtractionApprovedEventListener`
|
|
||||||
- `com.label.common.config.RedisConfig` -> `com.label.config.RedisConfig`
|
|
||||||
|
|
||||||
### 5.3 命名策略
|
|
||||||
|
|
||||||
- 以“最小必要变更”为原则,优先迁移包路径,不主动重命名类
|
|
||||||
- 仅在类职责与包语义明显不匹配时做必要归位
|
|
||||||
- 现阶段主代码类名不存在重名冲突,因此不需要为扁平化提前引入前缀或后缀
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 实施顺序
|
|
||||||
|
|
||||||
为降低迁移风险,实际执行采用“目标一次性扁平化,操作分阶段迁移”的方式。
|
|
||||||
|
|
||||||
### 阶段 1:基础公共层归位
|
|
||||||
|
|
||||||
先迁移以下内容,建立新骨架:
|
|
||||||
|
|
||||||
- `annotation`
|
|
||||||
- `aspect`
|
|
||||||
- `config`
|
|
||||||
- `event`
|
|
||||||
- `listener`
|
|
||||||
|
|
||||||
目标是先完成最基础的结构纠偏,并尽早暴露切面、配置和事件扫描问题。
|
|
||||||
|
|
||||||
### 阶段 2:数据承载层迁移
|
|
||||||
|
|
||||||
再迁移下列包:
|
|
||||||
|
|
||||||
- `entity`
|
|
||||||
- `dto`
|
|
||||||
- `mapper`
|
|
||||||
|
|
||||||
这些类依赖通常较窄,适合优先扁平化,也能及早验证 MyBatis 相关扫描与引用是否正常。
|
|
||||||
|
|
||||||
### 阶段 3:业务服务层迁移
|
|
||||||
|
|
||||||
迁移所有业务服务类到 `service`,原则如下:
|
|
||||||
|
|
||||||
- 不新增实现层
|
|
||||||
- 不调整现有业务编排
|
|
||||||
- 只修正导包、注解引用和必要的包路径依赖
|
|
||||||
|
|
||||||
### 阶段 4:控制层迁移
|
|
||||||
|
|
||||||
最后迁移全部控制器到 `controller`。控制层依赖最广,放在后面可以减少中间态反复修改。
|
|
||||||
|
|
||||||
### 阶段 5:启动与扫描校正
|
|
||||||
|
|
||||||
统一检查并修正以下内容:
|
|
||||||
|
|
||||||
- `@MapperScan`
|
|
||||||
- 组件扫描隐式路径
|
|
||||||
- OpenAPI 相关配置
|
|
||||||
- 事件监听与切面装配
|
|
||||||
- 测试代码中的旧包引用
|
|
||||||
|
|
||||||
### 阶段 6:编译与回归验证
|
|
||||||
|
|
||||||
迁移完成后进行全量编译与测试回归,确认结构调整没有引入行为回归。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 风险与控制策略
|
|
||||||
|
|
||||||
### 风险 1:导包失效
|
|
||||||
|
|
||||||
大量类迁移后,主代码和测试代码中的 import 会同时失效。
|
|
||||||
|
|
||||||
控制策略:
|
|
||||||
|
|
||||||
- 按阶段迁移并同步修复引用
|
|
||||||
- 每个阶段结束后至少执行一次编译检查
|
|
||||||
|
|
||||||
### 风险 2:Spring 扫描路径异常
|
|
||||||
|
|
||||||
若配置类、切面、监听器或 Mapper 的扫描路径与旧包结构耦合,迁移后可能导致 Bean 缺失。
|
|
||||||
|
|
||||||
控制策略:
|
|
||||||
|
|
||||||
- 显式检查启动类与配置类中的扫描配置
|
|
||||||
- 将路径校正作为独立阶段处理
|
|
||||||
|
|
||||||
### 风险 3:事件监听失效
|
|
||||||
|
|
||||||
监听器从 `service` 拆到 `listener` 后,若注解或组件扫描不正确,会导致事件未被消费。
|
|
||||||
|
|
||||||
控制策略:
|
|
||||||
|
|
||||||
- 迁移时同步校验事件类与监听器依赖关系
|
|
||||||
- 通过现有集成测试覆盖事件链路
|
|
||||||
|
|
||||||
### 风险 4:测试回归失败
|
|
||||||
|
|
||||||
测试代码同样引用旧包名,若只改主代码,会造成测试集整体失效。
|
|
||||||
|
|
||||||
控制策略:
|
|
||||||
|
|
||||||
- 测试代码与主代码同步迁移
|
|
||||||
- 将 `mvn test` 作为最终验收门槛
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 验收标准
|
|
||||||
|
|
||||||
本次结构调整完成后,应满足以下标准:
|
|
||||||
|
|
||||||
1. `src/main/java/com/label/module` 目录被完全移除
|
|
||||||
2. 主代码包结构符合标准扁平目录规范
|
|
||||||
3. 项目能够成功编译
|
|
||||||
4. 现有测试能够通过,至少覆盖当前可运行的启动测试、单元测试和集成测试
|
|
||||||
5. 调整范围内不存在残留旧包引用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 实现原则
|
|
||||||
|
|
||||||
执行本设计时应遵循以下原则:
|
|
||||||
|
|
||||||
- 先保证结构归位,再追求风格统一
|
|
||||||
- 先保证行为不变,再做命名优化
|
|
||||||
- 所有改动以“目录标准化”为中心,不引入额外架构决策
|
|
||||||
- 每次修改都要让工程更接近规范,而不是制造新的混合结构
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 结论
|
|
||||||
|
|
||||||
本次改造将 `label_backend` 从“按业务域分层 + 公共包混合”的现状,统一整理为规范文档定义的扁平标准目录结构。该调整不会改变系统能力边界,但会显著提升代码组织一致性、后续开发可预测性以及新成员理解成本。
|
|
||||||
|
|
||||||
实施时采用“目标结构一次确定、操作按阶段推进”的方式,以降低大规模包迁移带来的编译与装配风险。
|
|
||||||
@@ -1,570 +0,0 @@
|
|||||||
# label-backend Swagger 接口黑盒测试用例
|
|
||||||
|
|
||||||
## 1. 文档说明
|
|
||||||
|
|
||||||
- 生成时间:2026-04-14
|
|
||||||
- 适用项目:`label-backend`
|
|
||||||
- 覆盖范围:当前 `controller` 中已开放的全部 Swagger/OpenAPI 接口
|
|
||||||
- 生成依据:
|
|
||||||
- `src/main/java/com/label/controller/**/*.java`
|
|
||||||
- `src/main/java/com/label/service/**/*.java`
|
|
||||||
- `src/main/resources/sql/init.sql`
|
|
||||||
- `src/test/java/com/label/integration/**/*.java`
|
|
||||||
- 说明:
|
|
||||||
- 当前会话内未能直接访问 `http://127.0.0.1:8080/v3/api-docs`,本文档按代码、设计和现有集成测试反推生成。
|
|
||||||
- 文中“预期结果”以黑盒测试应验证的业务契约为主;若当前实现存在契约空洞,用例应保留,用于发现缺陷。
|
|
||||||
|
|
||||||
## 2. 测试前提
|
|
||||||
|
|
||||||
### 2.1 基础环境
|
|
||||||
|
|
||||||
- Base URL:`http://127.0.0.1:8080`
|
|
||||||
- OpenAPI:`GET /v3/api-docs`
|
|
||||||
- Swagger UI:`GET /swagger-ui.html`
|
|
||||||
- 默认返回体:`{ "code": "...", "message": "...", "data": ... }`
|
|
||||||
|
|
||||||
### 2.2 认证前提
|
|
||||||
|
|
||||||
- 若要执行认证、鉴权、越权、Token 失效类用例,建议运行时配置 `auth.enabled=true`。
|
|
||||||
- 若当前运行环境仍为 `auth.enabled=false` 的 mock 模式,则以下用例中所有 `401/403` 校验需要在真实认证模式下执行。
|
|
||||||
|
|
||||||
### 2.3 种子数据建议
|
|
||||||
|
|
||||||
基于 `src/main/resources/sql/init.sql`,至少准备以下账号:
|
|
||||||
|
|
||||||
| 公司 | 用户名 | 密码 | 角色 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `DEMO` | `admin` | `admin123` | `ADMIN` |
|
|
||||||
| `DEMO` | `reviewer01` | `review123` | `REVIEWER` |
|
|
||||||
| `DEMO` | `annotator01` | `annot123` | `ANNOTATOR` |
|
|
||||||
| `DEMO` | `uploader01` | `upload123` | `UPLOADER` |
|
|
||||||
|
|
||||||
额外建议准备:
|
|
||||||
|
|
||||||
- 第二家公司 `TESTB`
|
|
||||||
- `TESTB` 下至少 1 个 `ADMIN` 账号
|
|
||||||
- `DEMO`、`TESTB` 各自的资料、任务、配置、导出批次、视频任务样本
|
|
||||||
|
|
||||||
### 2.4 通用 Header
|
|
||||||
|
|
||||||
- JSON 接口:`Content-Type: application/json`
|
|
||||||
- 文件上传:`multipart/form-data`
|
|
||||||
- 受保护接口:`Authorization: Bearer <token>`
|
|
||||||
- 视频回调启用密钥时:`X-Callback-Secret: <VIDEO_CALLBACK_SECRET>`
|
|
||||||
|
|
||||||
## 3. 通用黑盒用例
|
|
||||||
|
|
||||||
除 `POST /api/auth/login` 与 `POST /api/video/callback` 外,所有受保护接口均应复用以下通用用例。
|
|
||||||
|
|
||||||
| 用例ID | 适用范围 | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| `G-AUTH-001` | 全部受保护接口 | 缺少 Token | 不传 `Authorization` | HTTP `401`,`code=UNAUTHORIZED` |
|
|
||||||
| `G-AUTH-002` | 全部受保护接口 | Token 格式错误 | 传 `Authorization: BearerX xxx` 或 `Basic xxx` | HTTP `401`,`code=UNAUTHORIZED` |
|
|
||||||
| `G-AUTH-003` | 全部受保护接口 | Token 无效或过期 | 传不存在/已失效 Token | HTTP `401`,`code=UNAUTHORIZED` |
|
|
||||||
| `G-ROLE-001` | 有角色要求的接口 | 角色不足 | 用低权限账号访问高权限接口 | HTTP `403`,`code=FORBIDDEN` |
|
|
||||||
| `G-TENANT-001` | 租户数据相关接口 | 跨租户访问数据 | 用 `TESTB` Token 访问 `DEMO` 数据 | 返回 `404`、空列表或业务拒绝;不能读到 `DEMO` 数据 |
|
|
||||||
| `G-PAGE-001` | 分页接口 | 大页码限制 | 传超大 `pageSize` | 返回成功,`pageSize` 被限制到系统上限,不出现异常 |
|
|
||||||
| `G-ERR-001` | 全部接口 | 非法请求不能打穿到 5xx | 传缺参/错参/非法状态 | 返回可解释的 `4xx` 或失败业务码,不应无意义 `500` |
|
|
||||||
|
|
||||||
## 4. 认证管理
|
|
||||||
|
|
||||||
### 4.1 `POST /api/auth/login`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `AUTH-LOGIN-001` | 正常登录 | `{"companyCode":"DEMO","username":"admin","password":"admin123"}` | HTTP `200`,`code=SUCCESS`,返回 `token/userId/username/role/expiresIn` |
|
|
||||||
| `AUTH-LOGIN-002` | 密码错误 | `password=wrong` | HTTP `401`,`code=USER_NOT_FOUND` |
|
|
||||||
| `AUTH-LOGIN-003` | 公司代码不存在 | `companyCode=NO_SUCH` | HTTP `401`,`code=USER_NOT_FOUND` |
|
|
||||||
| `AUTH-LOGIN-004` | 账号被禁用 | 先将用户禁用,再登录 | HTTP `403`,`code=USER_DISABLED` |
|
|
||||||
| `AUTH-LOGIN-005` | 缺少必填字段 | 缺 `companyCode`、`username` 或 `password` | 返回失败,不能生成有效 Token,不应出现无提示 `500` |
|
|
||||||
|
|
||||||
### 4.2 `POST /api/auth/logout`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `AUTH-LOGOUT-001` | 正常退出 | 用有效 Token 调用退出 | HTTP `200`,`code=SUCCESS` |
|
|
||||||
| `AUTH-LOGOUT-002` | 退出后 Token 立即失效 | 退出后继续访问 `/api/auth/me` | HTTP `401`,`code=UNAUTHORIZED` |
|
|
||||||
| `AUTH-LOGOUT-003` | 无 Token 退出 | 复用 `G-AUTH-001` | HTTP `401` |
|
|
||||||
|
|
||||||
### 4.3 `GET /api/auth/me`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `AUTH-ME-001` | 查询当前用户信息 | 用 `admin` Token 调用 | HTTP `200`,返回 `id/username/realName/role/companyId/companyName` |
|
|
||||||
| `AUTH-ME-002` | 已退出 Token 查询自己 | 先登录再退出,再调 `/me` | HTTP `401`,`code=UNAUTHORIZED` |
|
|
||||||
| `AUTH-ME-003` | 被禁用账号的旧 Token 查询自己 | 先登录,管理员禁用该用户,再调 `/me` | HTTP `401`,`code=UNAUTHORIZED` |
|
|
||||||
|
|
||||||
## 5. 公司管理
|
|
||||||
|
|
||||||
说明:本组接口复用 `G-AUTH-001~003`、`G-ROLE-001`。
|
|
||||||
|
|
||||||
### 5.1 `GET /api/companies`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `COMPANY-LIST-001` | 管理员分页查询公司 | `?page=1&pageSize=20` | HTTP `200`,返回分页结构 |
|
|
||||||
| `COMPANY-LIST-002` | 按状态筛选 | `?status=ACTIVE` | 仅返回对应状态公司 |
|
|
||||||
| `COMPANY-LIST-003` | 非管理员访问 | 用 `REVIEWER` 或 `UPLOADER` 调用 | HTTP `403`,`code=FORBIDDEN` |
|
|
||||||
|
|
||||||
### 5.2 `POST /api/companies`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `COMPANY-CREATE-001` | 创建公司成功 | `{"companyName":"测试公司B","companyCode":"TESTB"}` | HTTP `201`,创建成功,状态默认 `ACTIVE` |
|
|
||||||
| `COMPANY-CREATE-002` | 公司代码重复 | 使用已存在 `companyCode` | HTTP `409`,`code=DUPLICATE_COMPANY_CODE` |
|
|
||||||
| `COMPANY-CREATE-003` | 公司名称重复 | 使用已存在 `companyName` | HTTP `409`,`code=DUPLICATE_COMPANY_NAME` |
|
|
||||||
| `COMPANY-CREATE-004` | 公司名为空 | `companyName` 空或空白 | HTTP `400`,`code=INVALID_COMPANY_FIELD` |
|
|
||||||
| `COMPANY-CREATE-005` | 公司代码为空 | `companyCode` 空或空白 | HTTP `400`,`code=INVALID_COMPANY_FIELD` |
|
|
||||||
|
|
||||||
### 5.3 `PUT /api/companies/{id}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `COMPANY-UPDATE-001` | 更新公司成功 | 修改 `companyName/companyCode` | HTTP `200`,字段更新成功 |
|
|
||||||
| `COMPANY-UPDATE-002` | 更新不存在公司 | `id` 不存在 | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `COMPANY-UPDATE-003` | 更新为重复代码 | `companyCode` 改成已存在值 | HTTP `409`,`code=DUPLICATE_COMPANY_CODE` |
|
|
||||||
| `COMPANY-UPDATE-004` | 更新为重复名称 | `companyName` 改成已存在值 | HTTP `409`,`code=DUPLICATE_COMPANY_NAME` |
|
|
||||||
|
|
||||||
### 5.4 `PUT /api/companies/{id}/status`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `COMPANY-STATUS-001` | 禁用公司成功 | `{"status":"DISABLED"}` | HTTP `200`,公司状态变为 `DISABLED` |
|
|
||||||
| `COMPANY-STATUS-002` | 恢复公司成功 | `{"status":"ACTIVE"}` | HTTP `200`,公司状态变为 `ACTIVE` |
|
|
||||||
| `COMPANY-STATUS-003` | 非法状态值 | `{"status":"UNKNOWN"}` | HTTP `400`,`code=INVALID_COMPANY_STATUS` |
|
|
||||||
| `COMPANY-STATUS-004` | 公司不存在 | 不存在 `id` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
|
|
||||||
### 5.5 `DELETE /api/companies/{id}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `COMPANY-DELETE-001` | 删除空公司成功 | 删除无用户的公司 | HTTP `200`,删除成功 |
|
|
||||||
| `COMPANY-DELETE-002` | 公司下仍有用户 | 删除已有用户公司 | HTTP `409`,`code=COMPANY_HAS_USERS` |
|
|
||||||
| `COMPANY-DELETE-003` | 删除不存在公司 | 不存在 `id` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
|
|
||||||
## 6. 用户管理
|
|
||||||
|
|
||||||
说明:本组接口复用 `G-AUTH-001~003`、`G-ROLE-001`、`G-TENANT-001`。
|
|
||||||
|
|
||||||
### 6.1 `GET /api/users`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `USER-LIST-001` | 管理员查看本公司用户 | `?page=1&pageSize=20` | HTTP `200`,仅返回当前公司用户 |
|
|
||||||
| `USER-LIST-002` | 跨租户隔离 | 用 `TESTB` 管理员查看列表 | 不出现 `DEMO` 用户 |
|
|
||||||
| `USER-LIST-003` | 非管理员访问 | 用 `REVIEWER` 调用 | HTTP `403` |
|
|
||||||
|
|
||||||
### 6.2 `POST /api/users`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `USER-CREATE-001` | 创建用户成功 | `{"username":"qa01","password":"qa123456","realName":"测试员","role":"ANNOTATOR"}` | HTTP `200`,创建成功,状态默认为 `ACTIVE` |
|
|
||||||
| `USER-CREATE-002` | 用户名重复 | 同公司创建同名用户 | HTTP `409`,`code=DUPLICATE_USERNAME` |
|
|
||||||
| `USER-CREATE-003` | 角色非法 | `role=VIEWER` 或其他未支持角色 | HTTP `400`,`code=INVALID_ROLE` |
|
|
||||||
| `USER-CREATE-004` | 跨租户污染校验 | `TESTB` 管理员创建用户后,`DEMO` 不可见 | 创建成功且仅属于当前公司 |
|
|
||||||
|
|
||||||
### 6.3 `PUT /api/users/{id}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `USER-UPDATE-001` | 更新真实姓名成功 | `{"realName":"新名字"}` | HTTP `200`,真实姓名更新 |
|
|
||||||
| `USER-UPDATE-002` | 更新密码成功 | `{"password":"newpass123"}`,再重新登录 | 新密码可登录,旧密码失效 |
|
|
||||||
| `USER-UPDATE-003` | 更新不存在用户 | 不存在 `id` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `USER-UPDATE-004` | 更新他租户用户 | 用 `TESTB` 管理员修改 `DEMO` 用户 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 6.4 `PUT /api/users/{id}/status`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `USER-STATUS-001` | 禁用用户成功 | `{"status":"DISABLED"}` | HTTP `200`,用户状态更新成功 |
|
|
||||||
| `USER-STATUS-002` | 禁用后旧 Token 失效 | 禁用后用该用户旧 Token 调 `/api/auth/me` | HTTP `401`,`code=UNAUTHORIZED` |
|
|
||||||
| `USER-STATUS-003` | 恢复用户成功 | `{"status":"ACTIVE"}` | HTTP `200` |
|
|
||||||
| `USER-STATUS-004` | 非法状态值 | `{"status":"LOCKED"}` | HTTP `400`,`code=INVALID_STATUS` |
|
|
||||||
|
|
||||||
### 6.5 `PUT /api/users/{id}/role`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `USER-ROLE-001` | 修改角色成功 | `{"role":"REVIEWER"}` | HTTP `200`,用户角色更新成功 |
|
|
||||||
| `USER-ROLE-002` | 角色变更立即生效 | 同一 Token 变更前访问 reviewer 接口 `403`,变更后重试 | 变更后无需重新登录即可访问成功 |
|
|
||||||
| `USER-ROLE-003` | 非法角色值 | `{"role":"VIEWER"}` | HTTP `400`,`code=INVALID_ROLE` |
|
|
||||||
| `USER-ROLE-004` | 修改不存在用户 | 不存在 `id` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
|
|
||||||
## 7. 资料管理
|
|
||||||
|
|
||||||
说明:本组接口复用 `G-AUTH-001~003`、`G-ROLE-001`、`G-TENANT-001`。
|
|
||||||
|
|
||||||
### 7.1 `POST /api/source/upload`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `SOURCE-UPLOAD-001` | 上传文本资料成功 | `multipart/form-data`,传 `file` + `dataType=TEXT` | HTTP `201`,返回资料 `id/fileName/dataType/status` |
|
|
||||||
| `SOURCE-UPLOAD-002` | 上传图片资料成功 | `dataType=IMAGE` | HTTP `201` |
|
|
||||||
| `SOURCE-UPLOAD-003` | 上传视频资料成功 | `dataType=VIDEO` | HTTP `201` |
|
|
||||||
| `SOURCE-UPLOAD-004` | 空文件上传 | 文件为空 | HTTP `400`,`code=FILE_EMPTY` |
|
|
||||||
| `SOURCE-UPLOAD-005` | 不支持的资料类型 | `dataType=PDF` | HTTP `400`,`code=INVALID_TYPE` |
|
|
||||||
| `SOURCE-UPLOAD-006` | Swagger 文案兼容性检查 | `dataType=text` 小写 | 应与文档约定一致;若失败需修正文档或实现 |
|
|
||||||
| `SOURCE-UPLOAD-007` | 低权限无权上传 | 用无上传权限账号访问 | HTTP `403` |
|
|
||||||
|
|
||||||
### 7.2 `GET /api/source/list`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `SOURCE-LIST-001` | 上传员查看自己资料 | 用 `uploader01` 调用 | 仅返回自己上传的数据 |
|
|
||||||
| `SOURCE-LIST-002` | 管理员查看公司全部资料 | 用 `admin` 调用 | 返回公司内全部资料 |
|
|
||||||
| `SOURCE-LIST-003` | 按类型筛选 | `?dataType=TEXT` | 仅返回对应类型 |
|
|
||||||
| `SOURCE-LIST-004` | 按状态筛选 | `?status=PENDING` | 仅返回对应状态 |
|
|
||||||
| `SOURCE-LIST-005` | 跨租户隔离 | `TESTB` 调用 | 看不到 `DEMO` 数据 |
|
|
||||||
|
|
||||||
### 7.3 `GET /api/source/{id}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `SOURCE-DETAIL-001` | 查看资料详情成功 | 查询本公司资料 | HTTP `200`,返回详情及下载地址 |
|
|
||||||
| `SOURCE-DETAIL-002` | 查询不存在资料 | `id` 不存在 | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `SOURCE-DETAIL-003` | 跨租户查看详情 | 用 `TESTB` Token 查 `DEMO` 资料 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 7.4 `DELETE /api/source/{id}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `SOURCE-DELETE-001` | 删除 `PENDING` 资料成功 | 删除未进入流水线资料 | HTTP `200` |
|
|
||||||
| `SOURCE-DELETE-002` | 删除不存在资料 | `id` 不存在 | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `SOURCE-DELETE-003` | 删除已进入流水线资料 | 状态为 `PREPROCESSING/EXTRACTING/QA_REVIEW/APPROVED` | HTTP `409`,`code=SOURCE_IN_PIPELINE` |
|
|
||||||
| `SOURCE-DELETE-004` | 非管理员删除 | 用 `UPLOADER` 删除资料 | HTTP `403` |
|
|
||||||
|
|
||||||
## 8. 任务管理
|
|
||||||
|
|
||||||
说明:本组接口复用 `G-AUTH-001~003`、`G-ROLE-001`、`G-TENANT-001`。
|
|
||||||
|
|
||||||
### 8.1 `GET /api/tasks/pool`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-POOL-001` | 标注员查看任务池 | 用 `ANNOTATOR` 调用 | 仅返回 `EXTRACTION + UNCLAIMED` 任务 |
|
|
||||||
| `TASK-POOL-002` | Reviewer/Admin 调用任务池 | 用 `REVIEWER` 或 `ADMIN` 调用 | 返回 `SUBMITTED` 待审任务 |
|
|
||||||
| `TASK-POOL-003` | 跨租户隔离 | `TESTB` 调用 | 仅能看到本公司任务 |
|
|
||||||
|
|
||||||
### 8.2 `GET /api/tasks/mine`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-MINE-001` | 查询我的任务成功 | `?page=1&pageSize=20` | 返回当前用户的 `IN_PROGRESS/SUBMITTED/REJECTED` 任务 |
|
|
||||||
| `TASK-MINE-002` | 按状态过滤 | `?status=REJECTED` | 仅返回对应状态 |
|
|
||||||
| `TASK-MINE-003` | 未领取任务不应出现在 mine | 准备 `UNCLAIMED` 任务 | 列表中不出现该任务 |
|
|
||||||
|
|
||||||
### 8.3 `GET /api/tasks/pending-review`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-REVIEW-001` | Reviewer 查询待审批任务 | `?taskType=EXTRACTION` | 仅返回 `SUBMITTED` 且符合类型的任务 |
|
|
||||||
| `TASK-REVIEW-002` | 非 Reviewer 访问 | 用 `ANNOTATOR` 调用 | HTTP `403` |
|
|
||||||
| `TASK-REVIEW-003` | 跨租户隔离 | `TESTB` 调用 | 看不到 `DEMO` 待审任务 |
|
|
||||||
|
|
||||||
### 8.4 `GET /api/tasks`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-LIST-001` | 管理员查询全部任务 | `?status=SUBMITTED&taskType=QA_GENERATION` | 返回过滤后的本公司任务 |
|
|
||||||
| `TASK-LIST-002` | 非管理员访问 | 用 `REVIEWER` 调用 | HTTP `403` |
|
|
||||||
|
|
||||||
### 8.5 `POST /api/tasks`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-CREATE-001` | 创建提取任务成功 | `{"sourceId":<id>,"taskType":"EXTRACTION"}` | HTTP `200`,返回新任务,状态 `UNCLAIMED` |
|
|
||||||
| `TASK-CREATE-002` | 创建 QA 任务成功 | `{"sourceId":<id>,"taskType":"QA_GENERATION"}` | HTTP `200` |
|
|
||||||
| `TASK-CREATE-003` | 缺少 `sourceId` | 仅传 `taskType` | 应返回明确失败,不应出现裸 `500` |
|
|
||||||
| `TASK-CREATE-004` | 缺少 `taskType` | 仅传 `sourceId` | 应返回明确失败,不应出现裸 `500` |
|
|
||||||
|
|
||||||
### 8.6 `GET /api/tasks/{id}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-DETAIL-001` | 查询任务详情成功 | 查询本公司任务 | HTTP `200`,返回任务详情 |
|
|
||||||
| `TASK-DETAIL-002` | 查询不存在任务 | 不存在 `id` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `TASK-DETAIL-003` | 跨租户查看任务 | 用 `TESTB` 查询 `DEMO` 任务 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 8.7 `POST /api/tasks/{id}/claim`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-CLAIM-001` | 正常领取未领取任务 | `ANNOTATOR` 领取 `UNCLAIMED` 任务 | HTTP `200`,任务状态变为 `IN_PROGRESS` |
|
|
||||||
| `TASK-CLAIM-002` | 并发抢任务 | 10 并发领取同一任务 | 恰好 1 个成功,其余 HTTP `409`,`code=TASK_CLAIMED` |
|
|
||||||
| `TASK-CLAIM-003` | 重复领取已被占用任务 | 第二个用户再领取 | HTTP `409`,`code=TASK_CLAIMED` |
|
|
||||||
|
|
||||||
### 8.8 `POST /api/tasks/{id}/unclaim`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-UNCLAIM-001` | 放弃进行中任务成功 | 当前领取人放弃 `IN_PROGRESS` 任务 | HTTP `200`,状态回到 `UNCLAIMED` |
|
|
||||||
| `TASK-UNCLAIM-002` | 非法状态放弃 | 对 `SUBMITTED/REJECTED/APPROVED` 任务调用 | HTTP `409`,`code=INVALID_STATE_TRANSITION` |
|
|
||||||
| `TASK-UNCLAIM-003` | 非领取人放弃他人任务 | 用别的标注员调用 | 应被拒绝,不能让他人释放任务 |
|
|
||||||
|
|
||||||
### 8.9 `POST /api/tasks/{id}/reclaim`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-RECLAIM-001` | 原领取人重领被驳回任务 | 原领取人对 `REJECTED` 任务调用 | HTTP `200`,状态变为 `IN_PROGRESS` |
|
|
||||||
| `TASK-RECLAIM-002` | 非原领取人重领 | 其他用户调用 | HTTP `403`,`code=FORBIDDEN` |
|
|
||||||
| `TASK-RECLAIM-003` | 非驳回状态重领 | 对 `IN_PROGRESS` 任务调用 | HTTP `409`,`code=INVALID_STATE_TRANSITION` |
|
|
||||||
|
|
||||||
### 8.10 `PUT /api/tasks/{id}/reassign`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `TASK-REASSIGN-001` | 管理员强制指派成功 | `{"userId":<targetUserId>}` | HTTP `200`,任务归属变更到目标用户 |
|
|
||||||
| `TASK-REASSIGN-002` | 指派不存在任务 | 不存在 `id` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `TASK-REASSIGN-003` | 缺少 `userId` | 空请求体或缺字段 | 应返回明确失败,不应裸 `500` |
|
|
||||||
| `TASK-REASSIGN-004` | 指派后状态一致性 | 指派后查询任务 | 任务应处于可执行状态,归属人与时间被更新 |
|
|
||||||
|
|
||||||
## 9. 提取标注
|
|
||||||
|
|
||||||
说明:本组接口复用 `G-AUTH-001~003`、`G-ROLE-001`、`G-TENANT-001`。
|
|
||||||
|
|
||||||
### 9.1 `GET /api/extraction/{taskId}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `EXT-GET-001` | 获取提取结果成功 | 查询本公司任务 | HTTP `200`,返回 `taskId/sourceType/sourceFilePath/isFinal/resultJson` |
|
|
||||||
| `EXT-GET-002` | 查询不存在任务 | 不存在 `taskId` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `EXT-GET-003` | 跨租户查询结果 | 用 `TESTB` 查询 `DEMO` 任务 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 9.2 `PUT /api/extraction/{taskId}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求体 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `EXT-PUT-001` | 更新结果成功 | 传合法 JSON 字符串 | HTTP `200` |
|
|
||||||
| `EXT-PUT-002` | 非法 JSON | 传坏 JSON | HTTP `400`,`code=INVALID_JSON` |
|
|
||||||
| `EXT-PUT-003` | 任务不存在 | 不存在 `taskId` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
|
|
||||||
### 9.3 `POST /api/extraction/{taskId}/submit`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `EXT-SUBMIT-001` | 提交成功 | `IN_PROGRESS` 任务提交 | HTTP `200`,任务变为 `SUBMITTED` |
|
|
||||||
| `EXT-SUBMIT-002` | 非法状态提交 | `UNCLAIMED/REJECTED/APPROVED` 时提交 | HTTP `409`,`code=INVALID_STATE_TRANSITION` |
|
|
||||||
|
|
||||||
### 9.4 `POST /api/extraction/{taskId}/approve`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `EXT-APPROVE-001` | 审批通过成功 | Reviewer 审批 `SUBMITTED` 任务 | HTTP `200`,原任务 `APPROVED`,`isFinal=true` |
|
|
||||||
| `EXT-APPROVE-002` | 审批通过触发后续链路 | 审批完成后查数据库/接口 | 自动创建 `QA_GENERATION` 任务,`source_data.status=QA_REVIEW`,创建 `training_dataset` |
|
|
||||||
| `EXT-APPROVE-003` | 自审拦截 | 提交人与审批人相同 | HTTP `403`,`code=SELF_REVIEW_FORBIDDEN` |
|
|
||||||
| `EXT-APPROVE-004` | 非法状态审批 | 未提交任务直接审批 | HTTP `409`,`code=INVALID_STATE_TRANSITION` |
|
|
||||||
|
|
||||||
### 9.5 `POST /api/extraction/{taskId}/reject`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `EXT-REJECT-001` | 驳回成功 | `{"reason":"实体识别有误"}` | HTTP `200`,任务变为 `REJECTED` |
|
|
||||||
| `EXT-REJECT-002` | 驳回原因为空 | 空 body 或空 reason | HTTP `400`,`code=REASON_REQUIRED` |
|
|
||||||
| `EXT-REJECT-003` | 自审驳回 | 提交人与驳回人相同 | HTTP `403`,`code=SELF_REVIEW_FORBIDDEN` |
|
|
||||||
| `EXT-REJECT-004` | 驳回后可重领重提 | 驳回后原领取人调 `/reclaim` 再 `/submit` | 可重领,任务恢复到 `SUBMITTED` |
|
|
||||||
|
|
||||||
## 10. 问答生成
|
|
||||||
|
|
||||||
说明:本组接口复用 `G-AUTH-001~003`、`G-ROLE-001`、`G-TENANT-001`。
|
|
||||||
|
|
||||||
### 10.1 `GET /api/qa/{taskId}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `QA-GET-001` | 获取候选问答对成功 | 查询本公司 QA 任务 | HTTP `200`,返回 `taskId/sourceType/items` |
|
|
||||||
| `QA-GET-002` | 查询不存在任务 | 不存在 `taskId` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `QA-GET-003` | 跨租户查询 | 用 `TESTB` 查询 `DEMO` 任务 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 10.2 `PUT /api/qa/{taskId}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求体 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `QA-PUT-001` | 更新候选问答对成功 | `{"items":[...]}` | HTTP `200` |
|
|
||||||
| `QA-PUT-002` | 非法 JSON | 传坏 JSON | HTTP `400`,`code=INVALID_JSON` |
|
|
||||||
| `QA-PUT-003` | 缺失 items 字段 | 传空对象 `{}` | 不应报 5xx;若允许则生成空 conversations |
|
|
||||||
|
|
||||||
### 10.3 `POST /api/qa/{taskId}/submit`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `QA-SUBMIT-001` | 提交成功 | `IN_PROGRESS` 任务提交 | HTTP `200`,任务变为 `SUBMITTED` |
|
|
||||||
| `QA-SUBMIT-002` | 非法状态提交 | 对 `UNCLAIMED/REJECTED/APPROVED` 任务提交 | HTTP `409`,`code=INVALID_STATE_TRANSITION` |
|
|
||||||
|
|
||||||
### 10.4 `POST /api/qa/{taskId}/approve`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `QA-APPROVE-001` | QA 审批通过成功 | Reviewer 审批 `SUBMITTED` QA 任务 | HTTP `200` |
|
|
||||||
| `QA-APPROVE-002` | 审批通过完成整条流水线 | 审批完成后查状态 | `training_dataset.status=APPROVED`,任务 `APPROVED`,`source_data.status=APPROVED` |
|
|
||||||
| `QA-APPROVE-003` | 自审拦截 | 提交人与审批人相同 | HTTP `403`,`code=SELF_REVIEW_FORBIDDEN` |
|
|
||||||
|
|
||||||
### 10.5 `POST /api/qa/{taskId}/reject`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `QA-REJECT-001` | 驳回成功 | `{"reason":"问题描述不准确"}` | HTTP `200`,任务变为 `REJECTED` |
|
|
||||||
| `QA-REJECT-002` | 驳回原因为空 | 空 body 或空 reason | HTTP `400`,`code=REASON_REQUIRED` |
|
|
||||||
| `QA-REJECT-003` | 驳回后候选数据删除 | 驳回后检查 `training_dataset` | 候选问答对被删除,`source_data` 维持 `QA_REVIEW` |
|
|
||||||
| `QA-REJECT-004` | 驳回后可重领重提 | 原领取人调 `/reclaim` 再 `/submit` | 可重领,重新提交成功 |
|
|
||||||
|
|
||||||
## 11. 导出与微调
|
|
||||||
|
|
||||||
说明:本组接口复用 `G-AUTH-001~003`、`G-ROLE-001`、`G-TENANT-001`。
|
|
||||||
|
|
||||||
### 11.1 `GET /api/training/samples`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `EXPORT-SAMPLE-001` | 查询已审批样本成功 | `?page=1&pageSize=20` | HTTP `200`,返回 `APPROVED` 样本 |
|
|
||||||
| `EXPORT-SAMPLE-002` | 仅看未导出样本 | `?exported=false` | 只返回 `export_batch_id` 为空的数据 |
|
|
||||||
| `EXPORT-SAMPLE-003` | 按样本类型过滤 | `?sampleType=TEXT` | 仅返回对应类型 |
|
|
||||||
| `EXPORT-SAMPLE-004` | 非管理员访问 | 用 `ANNOTATOR` 调用 | HTTP `403` |
|
|
||||||
|
|
||||||
### 11.2 `POST /api/export/batch`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `EXPORT-BATCH-001` | 创建导出批次成功 | `{"sampleIds":[<approvedId1>,<approvedId2>]}` | HTTP `201`,返回批次信息 |
|
|
||||||
| `EXPORT-BATCH-002` | 样本列表为空 | `{"sampleIds":[]}` | HTTP `400`,`code=EMPTY_SAMPLES` |
|
|
||||||
| `EXPORT-BATCH-003` | 包含未审批样本 | 混入 `PENDING_REVIEW/REJECTED` 样本 | HTTP `400`,`code=INVALID_SAMPLES` |
|
|
||||||
| `EXPORT-BATCH-004` | 包含他租户样本 | 混入其他公司样本 ID | HTTP `400`,`code=INVALID_SAMPLES` |
|
|
||||||
| `EXPORT-BATCH-005` | 批次创建后回写样本导出信息 | 创建成功后查询样本 | `exportBatchId/exportedAt` 已写入 |
|
|
||||||
|
|
||||||
### 11.3 `POST /api/export/{batchId}/finetune`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `FINETUNE-TRIGGER-001` | 首次触发微调成功 | 对 `NOT_STARTED` 批次调用 | HTTP `200`,返回 `glmJobId/finetuneStatus=RUNNING` |
|
|
||||||
| `FINETUNE-TRIGGER-002` | 重复触发微调 | 对已启动批次再次调用 | HTTP `409`,`code=FINETUNE_ALREADY_STARTED` |
|
|
||||||
| `FINETUNE-TRIGGER-003` | 触发不存在批次 | 不存在 `batchId` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
|
|
||||||
### 11.4 `GET /api/export/{batchId}/status`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `FINETUNE-STATUS-001` | 未启动批次查询状态 | `glmJobId` 为空批次 | HTTP `200`,返回 `NOT_STARTED/progress=0` |
|
|
||||||
| `FINETUNE-STATUS-002` | 已启动批次查询状态 | 对已提交微调任务批次调用 | HTTP `200`,返回 `batchId/glmJobId/finetuneStatus/progress/errorMessage` |
|
|
||||||
| `FINETUNE-STATUS-003` | 跨租户状态隔离 | 用 `TESTB` 查 `DEMO` 批次 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 11.5 `GET /api/export/list`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `EXPORT-LIST-001` | 分页查询导出批次成功 | `?page=1&pageSize=20` | HTTP `200`,仅返回当前公司批次 |
|
|
||||||
| `EXPORT-LIST-002` | 跨租户隔离 | `TESTB` 调用 | 看不到 `DEMO` 批次 |
|
|
||||||
|
|
||||||
## 12. 系统配置
|
|
||||||
|
|
||||||
说明:本组接口复用 `G-AUTH-001~003`、`G-ROLE-001`、`G-TENANT-001`。
|
|
||||||
|
|
||||||
### 12.1 `GET /api/config`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `CONFIG-LIST-001` | 查询合并配置成功 | 管理员查询 | HTTP `200`,返回当前公司可见配置列表 |
|
|
||||||
| `CONFIG-LIST-002` | 公司配置覆盖全局默认 | 先写公司专属 `model_default`,再查询 | 返回 `scope=COMPANY` 且值为公司专属值 |
|
|
||||||
| `CONFIG-LIST-003` | 未配置公司专属时回退全局 | 清除公司专属后查询 | 返回 `scope=GLOBAL` |
|
|
||||||
| `CONFIG-LIST-004` | 跨租户隔离 | `DEMO` 配置不影响 `TESTB` | 另一公司仍看到自己的配置或全局默认 |
|
|
||||||
|
|
||||||
### 12.2 `PUT /api/config/{key}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `CONFIG-UPSERT-001` | 新增公司专属配置成功 | `{"value":"glm-4-plus","description":"默认模型"}` | HTTP `200`,创建成功 |
|
|
||||||
| `CONFIG-UPSERT-002` | 更新同键配置成功 | 同一个 `key` 连续两次 `PUT` | HTTP `200`,第二次为更新而不是重复插入 |
|
|
||||||
| `CONFIG-UPSERT-003` | 未知配置键 | `PUT /api/config/unknown_key` | HTTP `400`,`code=UNKNOWN_CONFIG_KEY` |
|
|
||||||
| `CONFIG-UPSERT-004` | 空配置值 | `{"value":""}` | HTTP `400`,`code=INVALID_CONFIG_VALUE` |
|
|
||||||
|
|
||||||
## 13. 视频处理
|
|
||||||
|
|
||||||
说明:
|
|
||||||
|
|
||||||
- `POST /api/video/callback` 为公开接口,不复用 `G-AUTH-001~003`
|
|
||||||
- 其余视频接口复用 `G-AUTH-001~003`、`G-ROLE-001`、`G-TENANT-001`
|
|
||||||
|
|
||||||
### 13.1 `POST /api/video/process`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `VIDEO-CREATE-001` | 创建视频处理任务成功 | `{"sourceId":<videoSourceId>,"jobType":"FRAME_EXTRACT","params":"{\"frameInterval\":30}"}` | 创建成功,返回 job,资料状态进入 `PREPROCESSING` |
|
|
||||||
| `VIDEO-CREATE-002` | 使用另一种任务类型成功 | `jobType=VIDEO_TO_TEXT` | 创建成功 |
|
|
||||||
| `VIDEO-CREATE-003` | 缺少必要字段 | 缺 `sourceId` 或 `jobType` | 返回失败,`code=INVALID_PARAMS`,且不能创建 job |
|
|
||||||
| `VIDEO-CREATE-004` | 非法任务类型 | `jobType=UNKNOWN` | HTTP `400`,`code=INVALID_JOB_TYPE` |
|
|
||||||
| `VIDEO-CREATE-005` | 非视频资料创建视频任务 | 对非 `VIDEO` 资料调用 | 应返回明确失败,不应产生错误状态迁移 |
|
|
||||||
| `VIDEO-CREATE-006` | 跨租户创建视频任务 | 用 `TESTB` 处理 `DEMO` 资料 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 13.2 `GET /api/video/jobs/{jobId}`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `VIDEO-GET-001` | 查询视频任务成功 | 查询本公司 `jobId` | HTTP `200`,返回任务状态、重试次数、输出路径等 |
|
|
||||||
| `VIDEO-GET-002` | 查询不存在任务 | 不存在 `jobId` | HTTP `404`,`code=NOT_FOUND` |
|
|
||||||
| `VIDEO-GET-003` | 跨租户查询 | 用 `TESTB` 查 `DEMO` 任务 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 13.3 `POST /api/video/jobs/{jobId}/reset`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 操作 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `VIDEO-RESET-001` | 重置失败任务成功 | 对 `FAILED` 任务调用 | HTTP `200`,job 状态变为 `PENDING`,`retryCount=0` |
|
|
||||||
| `VIDEO-RESET-002` | 非失败任务不允许重置 | 对 `PENDING/RETRYING/SUCCESS` 调用 | HTTP `400`,`code=INVALID_TRANSITION` |
|
|
||||||
| `VIDEO-RESET-003` | 跨租户重置 | 用 `TESTB` 调 `DEMO` 任务 | HTTP `404` 或不可见 |
|
|
||||||
|
|
||||||
### 13.4 `POST /api/video/callback`
|
|
||||||
|
|
||||||
| 用例ID | 场景 | 请求 | 预期结果 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `VIDEO-CALLBACK-001` | SUCCESS 回调成功 | `{"jobId":123,"status":"SUCCESS","outputPath":"processed/frames.zip"}` | HTTP `200`,job 变 `SUCCESS`,`source_data.status=PENDING` |
|
|
||||||
| `VIDEO-CALLBACK-002` | SUCCESS 回调幂等 | 对同一 `jobId` 连续两次发送相同 SUCCESS 回调 | 两次都成功,第二次不重复改状态、不重复产出副作用 |
|
|
||||||
| `VIDEO-CALLBACK-003` | FAILED 回调触发重试 | 对未达重试上限 job 发 `FAILED` | job 变 `RETRYING`,`retryCount+1` |
|
|
||||||
| `VIDEO-CALLBACK-004` | 超过最大重试次数 | `retryCount=maxRetries-1` 时再发 `FAILED` | job 变 `FAILED`,`source_data.status=PENDING` |
|
|
||||||
| `VIDEO-CALLBACK-005` | 回调 job 不存在 | `jobId` 不存在 | 接口不应打穿服务;应安全返回,无脏数据写入 |
|
|
||||||
| `VIDEO-CALLBACK-006` | 启用共享密钥时密钥错误 | 不传或传错 `X-Callback-Secret` | 返回失败,`code=UNAUTHORIZED` |
|
|
||||||
| `VIDEO-CALLBACK-007` | 回调缺少 `jobId/status` | 缺关键字段 | 返回明确失败,不应裸 `500` |
|
|
||||||
|
|
||||||
## 14. 推荐执行顺序
|
|
||||||
|
|
||||||
建议按以下顺序执行,能更快定位问题来源:
|
|
||||||
|
|
||||||
1. 认证管理
|
|
||||||
2. 公司管理
|
|
||||||
3. 用户管理
|
|
||||||
4. 资料管理
|
|
||||||
5. 任务管理
|
|
||||||
6. 提取标注
|
|
||||||
7. 问答生成
|
|
||||||
8. 导出与微调
|
|
||||||
9. 系统配置
|
|
||||||
10. 视频处理
|
|
||||||
|
|
||||||
## 15. 高风险回归点
|
|
||||||
|
|
||||||
每次版本回归至少覆盖以下高风险用例:
|
|
||||||
|
|
||||||
| 优先级 | 用例ID | 说明 |
|
|
||||||
|---|---|---|
|
|
||||||
| P0 | `AUTH-LOGIN-001` | 登录主链路 |
|
|
||||||
| P0 | `AUTH-LOGOUT-002` | 退出后 Token 立即失效 |
|
|
||||||
| P0 | `USER-STATUS-002` | 禁用账号后旧 Token 失效 |
|
|
||||||
| P0 | `SOURCE-DELETE-003` | 资料进入流水线后不可删除 |
|
|
||||||
| P0 | `TASK-CLAIM-002` | 并发抢任务只允许一个成功 |
|
|
||||||
| P0 | `EXT-APPROVE-002` | 提取审批通过自动推进 QA 链路 |
|
|
||||||
| P0 | `QA-APPROVE-002` | QA 审批通过完成整条流水线 |
|
|
||||||
| P0 | `EXPORT-BATCH-003` | 非 APPROVED 样本不可导出 |
|
|
||||||
| P0 | `CONFIG-LIST-004` | 配置跨租户隔离 |
|
|
||||||
| P0 | `VIDEO-CALLBACK-002` | 视频回调幂等 |
|
|
||||||
|
|
||||||
## 16. 后续落地建议
|
|
||||||
|
|
||||||
本文档适合作为三类产物的源:
|
|
||||||
|
|
||||||
- Swagger 手工测试清单
|
|
||||||
- Postman/Apifox/Newman 自动化用例集
|
|
||||||
- `src/test/java/com/label/blackbox` 下的 API 黑盒自动化测试
|
|
||||||
|
|
||||||
若需要继续落地,建议优先把以下用例自动化:
|
|
||||||
|
|
||||||
- 认证与 Token 生命周期
|
|
||||||
- 任务领取并发
|
|
||||||
- 提取/问答审批状态流转
|
|
||||||
- 导出样本校验
|
|
||||||
- 视频回调幂等与重试
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# 规格质量检查清单:label_backend 知识图谱智能标注平台
|
|
||||||
|
|
||||||
**用途**: 在进入规划阶段前验证规格说明的完整性和质量
|
|
||||||
**创建日期**: 2026-04-09
|
|
||||||
**功能**: [查看规格说明](../spec.md)
|
|
||||||
|
|
||||||
## 内容质量
|
|
||||||
|
|
||||||
- [x] 无实现细节(无编程语言、框架、API 引用)
|
|
||||||
- [x] 聚焦用户价值和业务需求
|
|
||||||
- [x] 面向非技术干系人编写
|
|
||||||
- [x] 所有必填章节均已完成
|
|
||||||
|
|
||||||
## 需求完整性
|
|
||||||
|
|
||||||
- [x] 无 [NEEDS CLARIFICATION] 标记残留
|
|
||||||
- [x] 需求可测试且无歧义
|
|
||||||
- [x] 成功标准可度量
|
|
||||||
- [x] 成功标准与技术无关(无实现细节)
|
|
||||||
- [x] 所有验收场景均已定义
|
|
||||||
- [x] 已识别边界情况
|
|
||||||
- [x] 范围边界清晰
|
|
||||||
- [x] 已识别依赖和假设
|
|
||||||
|
|
||||||
## 功能就绪性
|
|
||||||
|
|
||||||
- [x] 所有功能性需求均有明确验收标准
|
|
||||||
- [x] 用户场景覆盖主流程(认证、上传、标注、审批、导出)
|
|
||||||
- [x] 功能满足成功标准中定义的可度量结果
|
|
||||||
- [x] 无实现细节渗入规格说明
|
|
||||||
|
|
||||||
## 备注
|
|
||||||
|
|
||||||
所有检查项均通过。规格说明已就绪,可进行 `/speckit.plan` 规划阶段。
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
# API 契约:认证与用户管理
|
|
||||||
|
|
||||||
**统一响应格式**:
|
|
||||||
- 成功:`{"code": "SUCCESS", "data": {...}}`
|
|
||||||
- 成功(无数据):`{"code": "SUCCESS", "data": null}`
|
|
||||||
- 失败:`{"code": "ERROR_CODE", "message": "描述"}`
|
|
||||||
- 分页成功:`{"code": "SUCCESS", "data": {"items": [...], "total": 100, "page": 1, "pageSize": 20}}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/auth/login
|
|
||||||
|
|
||||||
**权限**: 匿名
|
|
||||||
**描述**: 用户登录,返回会话凭证
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"companyCode": "COMPANY_A",
|
|
||||||
"username": "zhangsan",
|
|
||||||
"password": "plaintext_password"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**成功响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"token": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"userId": 1,
|
|
||||||
"username": "zhangsan",
|
|
||||||
"role": "ANNOTATOR",
|
|
||||||
"expiresIn": 7200
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**失败响应**:
|
|
||||||
- `401` `USER_NOT_FOUND`: 用户名或密码错误(不区分哪个错误,防止枚举)
|
|
||||||
- `403` `USER_DISABLED`: 账号已禁用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/auth/logout
|
|
||||||
|
|
||||||
**权限**: 已登录(Bearer Token)
|
|
||||||
**描述**: 退出登录,立即删除 Redis 会话
|
|
||||||
|
|
||||||
**请求头**: `Authorization: Bearer {token}`
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/auth/me
|
|
||||||
|
|
||||||
**权限**: 已登录
|
|
||||||
**描述**: 获取当前用户信息
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"id": 1,
|
|
||||||
"username": "zhangsan",
|
|
||||||
"realName": "张三",
|
|
||||||
"role": "ANNOTATOR",
|
|
||||||
"companyId": 10,
|
|
||||||
"companyName": "测试公司"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/users
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 分页查询本公司用户列表
|
|
||||||
|
|
||||||
**查询参数**: `page`(默认 1)、`pageSize`(默认 20,最大 100)、`role`(可选过滤)、`status`(可选过滤)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"items": [
|
|
||||||
{"id": 1, "username": "zhangsan", "realName": "张三", "role": "ANNOTATOR", "status": "ACTIVE"}
|
|
||||||
],
|
|
||||||
"total": 50,
|
|
||||||
"page": 1,
|
|
||||||
"pageSize": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/users
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 创建用户
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"username": "lisi",
|
|
||||||
"password": "initial_password",
|
|
||||||
"realName": "李四",
|
|
||||||
"role": "ANNOTATOR"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应** `201`: `{"code": "SUCCESS", "data": {"id": 2, "username": "lisi", ...}}`
|
|
||||||
**失败**: `409` `USERNAME_EXISTS`: 用户名已存在
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PUT /api/users/{id}
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 更新用户基本信息
|
|
||||||
|
|
||||||
**请求体**: `{"realName": "新姓名"}`
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PUT /api/users/{id}/status
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 启用或禁用账号,立即驱逐权限缓存
|
|
||||||
|
|
||||||
**请求体**: `{"status": "DISABLED"}`
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PUT /api/users/{id}/role
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 变更用户角色,立即驱逐权限缓存
|
|
||||||
|
|
||||||
**请求体**: `{"role": "REVIEWER"}`
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**: `400` `INVALID_ROLE`: 角色值不合法
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# API 契约:系统配置
|
|
||||||
|
|
||||||
*所有接口需要 ADMIN 权限*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/config
|
|
||||||
|
|
||||||
**描述**: 获取所有配置项(公司级配置 + 全局默认配置合并,公司级优先)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"configKey": "prompt_extract_text",
|
|
||||||
"configValue": "请提取以下文本中的主语-谓语-宾语三元组...",
|
|
||||||
"description": "文本三元组提取 Prompt 模板",
|
|
||||||
"scope": "GLOBAL",
|
|
||||||
"updatedAt": "2026-04-09T00:00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"configKey": "model_default",
|
|
||||||
"configValue": "glm-4-turbo",
|
|
||||||
"description": "默认 AI 辅助模型",
|
|
||||||
"scope": "COMPANY",
|
|
||||||
"updatedAt": "2026-04-09T09:00:00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`scope` 字段:`GLOBAL`(来自全局默认)、`COMPANY`(来自公司级覆盖)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PUT /api/config/{key}
|
|
||||||
|
|
||||||
**描述**: 更新单项配置(若公司级配置不存在则创建;若存在则覆盖)
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"configValue": "glm-4-turbo",
|
|
||||||
"description": "升级到 GLM-4-Turbo 模型"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**: `400` `UNKNOWN_CONFIG_KEY`: 未知的配置键(防止拼写错误创建无效配置)
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# API 契约:训练数据导出与微调
|
|
||||||
|
|
||||||
*所有接口需要 ADMIN 权限*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/training/samples
|
|
||||||
|
|
||||||
**描述**: 分页查询已审批、可导出的训练样本
|
|
||||||
|
|
||||||
**查询参数**: `page`、`pageSize`、`sampleType`(TEXT / IMAGE / VIDEO_FRAME,可选)、`exported`(true/false,可选)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": 1001,
|
|
||||||
"sampleType": "TEXT",
|
|
||||||
"status": "APPROVED",
|
|
||||||
"exportBatchId": null,
|
|
||||||
"sourceId": 50,
|
|
||||||
"createdAt": "2026-04-09T12:00:00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"total": 500,
|
|
||||||
"page": 1,
|
|
||||||
"pageSize": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/export/batch
|
|
||||||
|
|
||||||
**描述**: 创建导出批次,合并选定样本为 JSONL 并上传 RustFS
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sampleIds": [1001, 1002, 1003]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**成功响应** `201`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"id": 10,
|
|
||||||
"batchUuid": "550e8400-e29b-41d4-a716-446655440000",
|
|
||||||
"sampleCount": 3,
|
|
||||||
"datasetFilePath": "export/550e8400.jsonl",
|
|
||||||
"finetuneStatus": "NOT_STARTED"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**失败**:
|
|
||||||
- `400` `INVALID_SAMPLES`: 部分样本不处于 APPROVED 状态
|
|
||||||
- `400` `EMPTY_SAMPLES`: sampleIds 为空
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/export/{batchId}/finetune
|
|
||||||
|
|
||||||
**描述**: 向 GLM AI 服务提交微调任务
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"glmJobId": "glm-finetune-abc123",
|
|
||||||
"finetuneStatus": "RUNNING"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**失败**: `409` `FINETUNE_ALREADY_STARTED`: 微调任务已提交
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/export/{batchId}/status
|
|
||||||
|
|
||||||
**描述**: 查询微调任务状态(向 AI 服务实时查询)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"batchId": 10,
|
|
||||||
"glmJobId": "glm-finetune-abc123",
|
|
||||||
"finetuneStatus": "RUNNING",
|
|
||||||
"progress": 45,
|
|
||||||
"errorMessage": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/export/list
|
|
||||||
|
|
||||||
**描述**: 分页查询所有导出批次
|
|
||||||
|
|
||||||
**查询参数**: `page`、`pageSize`
|
|
||||||
|
|
||||||
**响应** `200`: 批次列表(含 finetuneStatus、sampleCount、createdAt 等字段)
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
# API 契约:提取阶段标注工作台
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/extraction/{taskId}
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR(且为任务持有者)
|
|
||||||
**描述**: 获取当前提取结果(含 AI 预标注候选,供人工编辑)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"taskId": 101,
|
|
||||||
"sourceType": "TEXT",
|
|
||||||
"sourceFilePath": "text/202604/50.txt",
|
|
||||||
"isFinal": false,
|
|
||||||
"resultJson": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"subject": "北京",
|
|
||||||
"predicate": "是...首都",
|
|
||||||
"object": "中国",
|
|
||||||
"sourceText": "北京是中国的首都",
|
|
||||||
"startOffset": 0,
|
|
||||||
"endOffset": 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PUT /api/extraction/{taskId}
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR(且为任务持有者)
|
|
||||||
**描述**: 更新提取结果(**整体 JSONB 覆盖,PUT 语义,禁止局部 PATCH**)
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"subject": "北京",
|
|
||||||
"predicate": "是...首都",
|
|
||||||
"object": "中国",
|
|
||||||
"sourceText": "北京是中国的首都",
|
|
||||||
"startOffset": 0,
|
|
||||||
"endOffset": 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**: `400` `INVALID_JSON`: 提交的 JSON 格式不合法
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/extraction/{taskId}/submit
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR(且为任务持有者)
|
|
||||||
**描述**: 提交提取结果,任务状态 IN_PROGRESS → SUBMITTED,进入审批队列
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**: `409` `INVALID_STATE`: 任务当前状态不允许提交
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/extraction/{taskId}/approve
|
|
||||||
|
|
||||||
**权限**: REVIEWER
|
|
||||||
**描述**: 审批通过。**两阶段操作**:
|
|
||||||
1. 同步(同一事务):`annotation_result.is_final = true`,任务状态 SUBMITTED → APPROVED,写任务历史
|
|
||||||
2. 异步(事务提交后):AI 生成候选问答对 → 写 training_dataset → 创建 QA_GENERATION 任务 → source_data 状态推进
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**:
|
|
||||||
- `403` `SELF_REVIEW_FORBIDDEN`: 不允许审批自己提交的任务
|
|
||||||
- `409` `INVALID_STATE`: 任务状态不为 SUBMITTED
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/extraction/{taskId}/reject
|
|
||||||
|
|
||||||
**权限**: REVIEWER
|
|
||||||
**描述**: 驳回提取结果,任务状态 SUBMITTED → REJECTED,标注员可重领
|
|
||||||
|
|
||||||
**请求体**: `{"reason": "三元组边界不准确,请重新标注"}`
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**:
|
|
||||||
- `403` `SELF_REVIEW_FORBIDDEN`: 不允许驳回自己提交的任务
|
|
||||||
- `409` `INVALID_STATE`: 任务状态不为 SUBMITTED
|
|
||||||
- `400` `REASON_REQUIRED`: 驳回原因不能为空
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# API 契约:问答生成阶段
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/qa/{taskId}
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR(且为任务持有者)
|
|
||||||
**描述**: 获取候选问答对列表(由提取阶段审批触发 AI 生成)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"taskId": 202,
|
|
||||||
"sourceType": "TEXT",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": 1001,
|
|
||||||
"question": "北京是哪个国家的首都?",
|
|
||||||
"answer": "中国",
|
|
||||||
"status": "PENDING_REVIEW"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PUT /api/qa/{taskId}
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR(且为任务持有者)
|
|
||||||
**描述**: 修改问答对(**整体覆盖,PUT 语义**,每次提交包含完整 items 数组)
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"question": "北京是哪个国家的首都?",
|
|
||||||
"answer": "中国。北京自1949年起成为中华人民共和国的首都。"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/qa/{taskId}/submit
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR(且为任务持有者)
|
|
||||||
**描述**: 提交问答对,任务状态 IN_PROGRESS → SUBMITTED
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**: `409` `INVALID_STATE`: 任务当前状态不允许提交
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/qa/{taskId}/approve
|
|
||||||
|
|
||||||
**权限**: REVIEWER
|
|
||||||
**描述**: 审批通过。同一事务中:先校验任务 → training_dataset 状态 → 任务状态 SUBMITTED → APPROVED → source_data 状态 → 写任务历史
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**:
|
|
||||||
- `403` `SELF_REVIEW_FORBIDDEN`: 不允许审批自己提交的任务
|
|
||||||
- `409` `INVALID_STATE`: 任务状态不为 SUBMITTED
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/qa/{taskId}/reject
|
|
||||||
|
|
||||||
**权限**: REVIEWER
|
|
||||||
**描述**: 驳回问答对,删除候选记录,任务状态 SUBMITTED → REJECTED
|
|
||||||
|
|
||||||
**请求体**: `{"reason": "问题描述不准确,请修改"}`
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**:
|
|
||||||
- `403` `SELF_REVIEW_FORBIDDEN`: 不允许驳回自己提交的任务
|
|
||||||
- `400` `REASON_REQUIRED`: 驳回原因不能为空
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
# API 契约:资料管理
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/source/upload
|
|
||||||
|
|
||||||
**权限**: UPLOADER
|
|
||||||
**描述**: 上传文件,创建 source_data 记录,文件字节流写入 RustFS
|
|
||||||
|
|
||||||
**请求**: `multipart/form-data`,字段:`file`(必填)、`dataType`(TEXT / IMAGE / VIDEO)
|
|
||||||
|
|
||||||
**响应** `201`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"id": 50,
|
|
||||||
"fileName": "document.txt",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"fileSize": 204800,
|
|
||||||
"status": "PENDING",
|
|
||||||
"createdAt": "2026-04-09T10:00:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**失败**:
|
|
||||||
- `400` `INVALID_TYPE`: 不支持的资料类型
|
|
||||||
- `400` `FILE_EMPTY`: 文件为空
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/source/list
|
|
||||||
|
|
||||||
**权限**: UPLOADER
|
|
||||||
**描述**: 分页查询资料列表。UPLOADER 只见自己上传的资料;ADMIN 见本公司全部资料
|
|
||||||
|
|
||||||
**查询参数**: `page`(默认 1)、`pageSize`(默认 20)、`dataType`(可选)、`status`(可选)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": 50,
|
|
||||||
"fileName": "document.txt",
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"status": "PENDING",
|
|
||||||
"uploaderId": 1,
|
|
||||||
"createdAt": "2026-04-09T10:00:00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"total": 120,
|
|
||||||
"page": 1,
|
|
||||||
"pageSize": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/source/{id}
|
|
||||||
|
|
||||||
**权限**: UPLOADER
|
|
||||||
**描述**: 查看资料详情,含 RustFS 预签名临时下载链接(有效期 15 分钟)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"id": 50,
|
|
||||||
"dataType": "TEXT",
|
|
||||||
"fileName": "document.txt",
|
|
||||||
"fileSize": 204800,
|
|
||||||
"status": "EXTRACTING",
|
|
||||||
"presignedUrl": "https://rustfs.example.com/...",
|
|
||||||
"parentSourceId": null,
|
|
||||||
"createdAt": "2026-04-09T10:00:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DELETE /api/source/{id}
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 删除资料(同时删除 RustFS 文件及元数据)
|
|
||||||
|
|
||||||
**前置条件**: 资料状态为 PENDING(不允许删除已进入流水线的资料)
|
|
||||||
|
|
||||||
**响应** `204`: 无响应体
|
|
||||||
**失败**: `409` `SOURCE_IN_PIPELINE`: 资料已进入标注流程,不可删除
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
# API 契约:任务管理
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/tasks/pool
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR
|
|
||||||
**描述**: 查看可领取任务池。角色过滤规则:
|
|
||||||
- ANNOTATOR:仅返回 EXTRACTION 阶段、status=UNCLAIMED 的任务
|
|
||||||
- REVIEWER/ADMIN:仅返回 SUBMITTED 状态(待审批队列)的任务
|
|
||||||
|
|
||||||
**查询参数**: `page`(默认 1)、`pageSize`(默认 20)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"id": 101,
|
|
||||||
"sourceId": 50,
|
|
||||||
"sourceType": "TEXT",
|
|
||||||
"phase": "EXTRACTION",
|
|
||||||
"status": "UNCLAIMED",
|
|
||||||
"createdAt": "2026-04-09T10:00:00"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"total": 30,
|
|
||||||
"page": 1,
|
|
||||||
"pageSize": 20
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/tasks/pending-review
|
|
||||||
|
|
||||||
**权限**: REVIEWER
|
|
||||||
**描述**: REVIEWER 专属审批入口,查看 status=SUBMITTED 的任务列表
|
|
||||||
|
|
||||||
**查询参数**: `page`、`pageSize`、`phase`(可选,EXTRACTION / QA_GENERATION)
|
|
||||||
|
|
||||||
**响应**: 同 `/api/tasks/pool` 结构
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/tasks/{id}/claim
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR
|
|
||||||
**描述**: 领取任务(双重并发保障:Redis SET NX + DB 乐观约束)
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**:
|
|
||||||
- `409` `TASK_CLAIMED`: 任务已被他人领取
|
|
||||||
- `404` `TASK_NOT_FOUND`: 任务不存在
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/tasks/{id}/unclaim
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR(且为任务持有者)
|
|
||||||
**描述**: 放弃任务,退回任务池(status: IN_PROGRESS → UNCLAIMED)
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**: `403` `NOT_TASK_OWNER`: 非任务持有者
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/tasks/mine
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR
|
|
||||||
**描述**: 查询当前用户领取的任务(含 IN_PROGRESS、SUBMITTED、REJECTED 三种状态)
|
|
||||||
|
|
||||||
**查询参数**: `page`、`pageSize`、`status`(可选过滤)
|
|
||||||
|
|
||||||
**响应**: 同任务列表结构,含 `rejectReason` 字段(REJECTED 状态时非空)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/tasks/{id}/reclaim
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR
|
|
||||||
**描述**: 重领被驳回的任务(status 必须为 REJECTED 且 claimedBy = 当前用户,流转 REJECTED → IN_PROGRESS)
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**:
|
|
||||||
- `403` `NOT_TASK_OWNER`: 非原持有者
|
|
||||||
- `409` `INVALID_STATE`: 任务状态不为 REJECTED
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/tasks/{id}
|
|
||||||
|
|
||||||
**权限**: ANNOTATOR
|
|
||||||
**描述**: 查看任务详情(含驳回原因、历史记录摘要)
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"id": 101,
|
|
||||||
"sourceId": 50,
|
|
||||||
"phase": "EXTRACTION",
|
|
||||||
"status": "IN_PROGRESS",
|
|
||||||
"claimedBy": 1,
|
|
||||||
"claimedAt": "2026-04-09T10:05:00",
|
|
||||||
"rejectReason": null,
|
|
||||||
"historyCount": 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/tasks
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 查询全部任务(支持过滤,分页)
|
|
||||||
|
|
||||||
**查询参数**: `page`、`pageSize`、`phase`、`status`、`claimedBy`、`sourceId`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## PUT /api/tasks/{id}/reassign
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 强制转移任务归属(status 保持 IN_PROGRESS,仅 claimedBy 变更)
|
|
||||||
|
|
||||||
**请求体**: `{"newOwnerId": 5, "reason": "原持有者长期未操作"}`
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/tasks
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 为指定资料创建 EXTRACTION 任务
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sourceId": 50,
|
|
||||||
"taskType": "AI_ASSISTED",
|
|
||||||
"aiModel": "glm-4"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
**响应** `201`: `{"code": "SUCCESS", "data": {"id": 101, ...}}`
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
# API 契约:视频处理
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/video/process
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 为已上传的视频资料创建异步处理任务
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sourceId": 50,
|
|
||||||
"jobType": "FRAME_EXTRACT",
|
|
||||||
"params": {
|
|
||||||
"frameInterval": 30,
|
|
||||||
"mode": "FRAME"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
jobType 可选值:`FRAME_EXTRACT`(帧提取)、`VIDEO_TO_TEXT`(片段转文字)
|
|
||||||
|
|
||||||
**响应** `201`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"jobId": 200,
|
|
||||||
"sourceId": 50,
|
|
||||||
"jobType": "FRAME_EXTRACT",
|
|
||||||
"status": "PENDING"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GET /api/video/jobs/{jobId}
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 查询视频处理任务状态
|
|
||||||
|
|
||||||
**响应** `200`:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"code": "SUCCESS",
|
|
||||||
"data": {
|
|
||||||
"id": 200,
|
|
||||||
"status": "RUNNING",
|
|
||||||
"processedUnits": 15,
|
|
||||||
"totalUnits": 50,
|
|
||||||
"retryCount": 0,
|
|
||||||
"errorMessage": null,
|
|
||||||
"startedAt": "2026-04-09T10:05:00"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/video/jobs/{jobId}/reset
|
|
||||||
|
|
||||||
**权限**: ADMIN
|
|
||||||
**描述**: 手动重置 FAILED 状态的任务为 PENDING,允许重新触发(FAILED → PENDING 不在自动状态机中)
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
**失败**: `409` `INVALID_STATE`: 任务状态不为 FAILED
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## POST /api/video/callback(内部接口)
|
|
||||||
|
|
||||||
**权限**: AI 服务内部调用(IP 白名单 / 服务密钥)
|
|
||||||
**描述**: AI 服务回调,通知视频处理结果(幂等:重复成功回调静默忽略)
|
|
||||||
|
|
||||||
**请求体**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"jobId": 200,
|
|
||||||
"success": true,
|
|
||||||
"outputPath": "frames/50/",
|
|
||||||
"errorMessage": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**响应** `200`: `{"code": "SUCCESS", "data": null}`
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
# 数据模型:label_backend
|
|
||||||
|
|
||||||
**日期**: 2026-04-09
|
|
||||||
**分支**: `001-label-backend-spec`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实体关系概览
|
|
||||||
|
|
||||||
```
|
|
||||||
sys_company ─┬─ sys_user (company_id FK)
|
|
||||||
├─ source_data (company_id FK)
|
|
||||||
│ └─ source_data (parent_source_id 自引用,视频溯源链)
|
|
||||||
├─ annotation_task (company_id FK)
|
|
||||||
│ ├─ annotation_result (task_id FK)
|
|
||||||
│ └─ annotation_task_history (task_id FK)
|
|
||||||
├─ training_dataset (company_id FK)
|
|
||||||
├─ export_batch (company_id FK)
|
|
||||||
├─ sys_config (company_id FK,可为 NULL 表示全局默认)
|
|
||||||
├─ sys_operation_log (company_id FK)
|
|
||||||
└─ video_process_job (company_id FK)
|
|
||||||
```
|
|
||||||
|
|
||||||
**多租户规则**:除 `sys_company` 本身外,所有业务表均包含 `company_id NOT NULL`。查询时由 `TenantLineInnerInterceptor` 自动注入 `WHERE company_id = ?`。唯一例外:`sys_config` 允许 `company_id = NULL` 表示全局默认配置。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实体详情
|
|
||||||
|
|
||||||
### 1. sys_company — 公司(租户)
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | 自增主键 |
|
|
||||||
| company_name | VARCHAR(100) | NOT NULL UNIQUE | 公司名称 |
|
|
||||||
| company_code | VARCHAR(50) | NOT NULL UNIQUE | 公司编码 |
|
|
||||||
| status | VARCHAR(10) | NOT NULL DEFAULT 'ACTIVE' | ACTIVE / DISABLED |
|
|
||||||
| created_at | TIMESTAMP | NOT NULL DEFAULT NOW() | |
|
|
||||||
| updated_at | TIMESTAMP | NOT NULL DEFAULT NOW() | |
|
|
||||||
|
|
||||||
**状态**: 无状态机(仅 ACTIVE/DISABLED 标志)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. sys_user — 用户
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | NOT NULL FK→sys_company | 租户隔离键 |
|
|
||||||
| username | VARCHAR(50) | NOT NULL | 同公司内唯一 |
|
|
||||||
| password_hash | VARCHAR(255) | NOT NULL | BCrypt 强度≥10,禁止序列化到响应 |
|
|
||||||
| real_name | VARCHAR(50) | — | |
|
|
||||||
| role | VARCHAR(20) | NOT NULL | UPLOADER / ANNOTATOR / REVIEWER / ADMIN |
|
|
||||||
| status | VARCHAR(10) | NOT NULL DEFAULT 'ACTIVE' | ACTIVE / DISABLED |
|
|
||||||
| created_at / updated_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**约束**: `UNIQUE(company_id, username)`
|
|
||||||
**索引**: `(company_id)`
|
|
||||||
**角色继承**: ADMIN ⊃ REVIEWER ⊃ ANNOTATOR ⊃ UPLOADER(由 Shiro Realm 的 addInheritedRoles() 实现)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. source_data — 原始资料
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | NOT NULL FK→sys_company | |
|
|
||||||
| uploader_id | BIGINT | FK→sys_user | |
|
|
||||||
| data_type | VARCHAR(20) | NOT NULL | TEXT / IMAGE / VIDEO |
|
|
||||||
| file_path | VARCHAR(500) | NOT NULL | RustFS 对象路径 |
|
|
||||||
| file_name | VARCHAR(255) | NOT NULL | 原始文件名 |
|
|
||||||
| file_size | BIGINT | — | 字节数 |
|
|
||||||
| bucket_name | VARCHAR(100) | NOT NULL | RustFS 桶名 |
|
|
||||||
| parent_source_id | BIGINT | FK→source_data | 视频片段转文本时指向原视频 |
|
|
||||||
| status | VARCHAR(20) | NOT NULL DEFAULT 'PENDING' | 见状态机 |
|
|
||||||
| reject_reason | TEXT | — | 保留字段(当前无 REJECTED 状态) |
|
|
||||||
| created_at / updated_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**索引**: `(company_id)`、`(company_id, status)`、`(parent_source_id)`
|
|
||||||
|
|
||||||
**状态机**:
|
|
||||||
```
|
|
||||||
PENDING → EXTRACTING(直接上传的文本/图片)
|
|
||||||
PENDING → PREPROCESSING(视频上传后)
|
|
||||||
PREPROCESSING → PENDING(视频预处理完成后进入标注流程)
|
|
||||||
EXTRACTING → QA_REVIEW(提取任务审批通过后)
|
|
||||||
QA_REVIEW → APPROVED(QA 任务审批通过后,整条流水线完成)
|
|
||||||
```
|
|
||||||
|
|
||||||
*注:source_data 无 REJECTED 状态。QA 阶段驳回作用于 annotation_task(→REJECTED),source_data 保持 QA_REVIEW 不变。*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. annotation_task — 标注任务
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | NOT NULL FK→sys_company | |
|
|
||||||
| source_id | BIGINT | NOT NULL FK→source_data | |
|
|
||||||
| phase | VARCHAR(20) | NOT NULL | EXTRACTION / QA_GENERATION |
|
|
||||||
| task_type | VARCHAR(20) | NOT NULL | AI_ASSISTED / MANUAL |
|
|
||||||
| ai_model | VARCHAR(50) | — | 使用的 AI 模型 |
|
|
||||||
| video_unit_type | VARCHAR(20) | — | FRAME(视频帧模式)/ NULL |
|
|
||||||
| video_unit_info | JSONB | — | `{frame_index, time_sec, frame_path}` |
|
|
||||||
| claimed_by | BIGINT | FK→sys_user | 当前持有者 |
|
|
||||||
| claimed_at | TIMESTAMP | — | |
|
|
||||||
| status | VARCHAR(20) | NOT NULL DEFAULT 'UNCLAIMED' | 见状态机 |
|
|
||||||
| reject_reason | TEXT | — | 驳回原因 |
|
|
||||||
| submitted_at | TIMESTAMP | — | |
|
|
||||||
| completed_at | TIMESTAMP | — | |
|
|
||||||
| created_at / updated_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**索引**: `(company_id)`、`(company_id, phase, status)`(任务池查询)、`(claimed_by, status)`(我的任务)
|
|
||||||
|
|
||||||
**状态机**:
|
|
||||||
```
|
|
||||||
UNCLAIMED → IN_PROGRESS(领取)
|
|
||||||
IN_PROGRESS → SUBMITTED(提交)
|
|
||||||
IN_PROGRESS → UNCLAIMED(放弃)
|
|
||||||
IN_PROGRESS → IN_PROGRESS(ADMIN 强制转移,持有人变更,状态不变)
|
|
||||||
SUBMITTED → APPROVED(审批通过)
|
|
||||||
SUBMITTED → REJECTED(审批驳回)
|
|
||||||
REJECTED → IN_PROGRESS(标注员重领)
|
|
||||||
```
|
|
||||||
|
|
||||||
**并发控制**: 领取时双重保障:① Redis `SET NX task:claim:{taskId}` TTL 30s;② DB `UPDATE ... WHERE status='UNCLAIMED'` 影响行数为 0 时返回错误
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 5. annotation_result — 标注结果(提取阶段)
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | NOT NULL FK→sys_company | |
|
|
||||||
| task_id | BIGINT | NOT NULL FK→annotation_task | |
|
|
||||||
| result_json | JSONB | NOT NULL | 整体覆盖,禁止局部 PATCH |
|
|
||||||
| is_final | BOOLEAN | NOT NULL DEFAULT FALSE | 审批通过后置 TRUE |
|
|
||||||
| submitted_by | BIGINT | FK→sys_user | |
|
|
||||||
| created_at / updated_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**result_json 结构**(文本三元组示例):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"subject": "北京",
|
|
||||||
"predicate": "是...首都",
|
|
||||||
"object": "中国",
|
|
||||||
"source_text": "北京是中国的首都",
|
|
||||||
"start_offset": 0,
|
|
||||||
"end_offset": 8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**result_json 结构**(图片四元组示例):
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"subject": "猫",
|
|
||||||
"relation": "坐在",
|
|
||||||
"object": "椅子",
|
|
||||||
"modifier": "白色的",
|
|
||||||
"bbox": [100, 200, 300, 400],
|
|
||||||
"crop_path": "crops/123/0.jpg"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**索引**: `(task_id)`、`(company_id, is_final)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 6. training_dataset — 训练样本
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | NOT NULL FK→sys_company | |
|
|
||||||
| task_id | BIGINT | NOT NULL FK→annotation_task | |
|
|
||||||
| source_id | BIGINT | NOT NULL FK→source_data | |
|
|
||||||
| extraction_result_id | BIGINT | NOT NULL FK→annotation_result | |
|
|
||||||
| sample_type | VARCHAR(20) | NOT NULL | TEXT / IMAGE / VIDEO_FRAME |
|
|
||||||
| glm_format_json | JSONB | NOT NULL | GLM 微调格式 |
|
|
||||||
| export_batch_id | VARCHAR(50) | — | NULL 表示未导出 |
|
|
||||||
| status | VARCHAR(20) | NOT NULL DEFAULT 'PENDING_REVIEW' | 见状态机 |
|
|
||||||
| reject_reason | TEXT | — | |
|
|
||||||
| reviewed_by | BIGINT | FK→sys_user | |
|
|
||||||
| exported_at | TIMESTAMP | — | |
|
|
||||||
| created_at / updated_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**状态机**:
|
|
||||||
```
|
|
||||||
PENDING_REVIEW → APPROVED(QA 审批通过)
|
|
||||||
PENDING_REVIEW → REJECTED(QA 审批驳回)
|
|
||||||
REJECTED → PENDING_REVIEW(标注员修改后重提)
|
|
||||||
```
|
|
||||||
|
|
||||||
**glm_format_json 结构**:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"conversations": [
|
|
||||||
{"role": "user", "content": "..."},
|
|
||||||
{"role": "assistant", "content": "..."}
|
|
||||||
],
|
|
||||||
"source_type": "TEXT"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**索引**: `(company_id)`、`(company_id, status)`、`(export_batch_id)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 7. export_batch — 导出批次
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | NOT NULL FK→sys_company | |
|
|
||||||
| batch_uuid | VARCHAR(50) | NOT NULL UNIQUE | 批次标识符 |
|
|
||||||
| dataset_file_path | VARCHAR(500) | — | RustFS JSONL 路径 |
|
|
||||||
| sample_count | INT | NOT NULL DEFAULT 0 | |
|
|
||||||
| glm_job_id | VARCHAR(100) | — | 微调任务 ID |
|
|
||||||
| finetune_status | VARCHAR(20) | NOT NULL DEFAULT 'NOT_STARTED' | 见状态 |
|
|
||||||
| error_message | TEXT | — | |
|
|
||||||
| created_by | BIGINT | FK→sys_user | |
|
|
||||||
| created_at / updated_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**finetune_status 值**: NOT_STARTED / RUNNING / SUCCESS / FAILED
|
|
||||||
|
|
||||||
**索引**: `(company_id)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 8. sys_config — 系统配置
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | FK→sys_company,可 NULL | NULL = 全局默认配置 |
|
|
||||||
| config_key | VARCHAR(100) | NOT NULL | |
|
|
||||||
| config_value | TEXT | NOT NULL | |
|
|
||||||
| description | TEXT | — | |
|
|
||||||
| updated_by | BIGINT | FK→sys_user | |
|
|
||||||
| updated_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**约束**: `UNIQUE(company_id, config_key)`
|
|
||||||
**查询规则**: 先按 `(companyId, configKey)` 查;未命中则按 `(NULL, configKey)` 查全局默认。
|
|
||||||
|
|
||||||
**预置全局配置键**:
|
|
||||||
- `prompt_extract_text`、`prompt_extract_image`、`prompt_video_to_text`
|
|
||||||
- `prompt_qa_gen_text`、`prompt_qa_gen_image`
|
|
||||||
- `model_default`(默认:`glm-4`)
|
|
||||||
- `video_frame_interval`(默认:`30`)
|
|
||||||
- `token_ttl_seconds`(默认:`7200`)
|
|
||||||
- `glm_api_base_url`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 9. sys_operation_log — 操作审计日志
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | FK→sys_company | |
|
|
||||||
| operator_id | BIGINT | FK→sys_user | 登录失败时可为 NULL |
|
|
||||||
| operator_name | VARCHAR(50) | NOT NULL | **操作时用户名快照**(不随改名变化) |
|
|
||||||
| operation_type | VARCHAR(50) | NOT NULL | 见枚举列表 |
|
|
||||||
| target_type | VARCHAR(30) | — | |
|
|
||||||
| target_id | BIGINT | — | |
|
|
||||||
| detail | JSONB | — | 补充信息 |
|
|
||||||
| ip_address | VARCHAR(50) | — | |
|
|
||||||
| result | VARCHAR(10) | NOT NULL | SUCCESS / FAIL |
|
|
||||||
| error_message | TEXT | — | |
|
|
||||||
| created_at | TIMESTAMP | NOT NULL DEFAULT NOW() | 分区键 |
|
|
||||||
|
|
||||||
**只追加**:应用层禁止 UPDATE/DELETE,建议 DB 层添加触发器强制执行
|
|
||||||
**分区**:按 `created_at` Range 分区,以月为单位(`sys_operation_log_YYYY_MM`)
|
|
||||||
|
|
||||||
**operation_type 枚举**:
|
|
||||||
`USER_LOGIN`、`USER_LOGOUT`、`USER_CREATE`、`USER_UPDATE`、`USER_DISABLE`、`USER_ROLE_CHANGE`、`SOURCE_UPLOAD`、`SOURCE_DELETE`、`TASK_CREATE`、`TASK_CLAIM`、`TASK_UNCLAIM`、`TASK_SUBMIT`、`EXTRACTION_APPROVE`、`EXTRACTION_REJECT`、`QA_APPROVE`、`QA_REJECT`、`TASK_REASSIGN`、`EXPORT_CREATE`、`FINETUNE_START`、`CONFIG_UPDATE`、`VIDEO_JOB_RESET`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 10. annotation_task_history — 任务流转历史
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | NOT NULL FK→sys_company | |
|
|
||||||
| task_id | BIGINT | NOT NULL FK→annotation_task | |
|
|
||||||
| from_status | VARCHAR(20) | — | 任务初建时为 NULL |
|
|
||||||
| to_status | VARCHAR(20) | NOT NULL | |
|
|
||||||
| operator_id | BIGINT | NOT NULL FK→sys_user | |
|
|
||||||
| operator_role | VARCHAR(20) | NOT NULL | **操作时角色快照** |
|
|
||||||
| note | TEXT | — | 驳回原因、转移说明等 |
|
|
||||||
| created_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**只追加**:每次 annotation_task.status 变更时同步插入,与业务操作在同一事务中
|
|
||||||
**索引**: `(task_id)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 11. video_process_job — 视频异步处理任务
|
|
||||||
|
|
||||||
| 字段 | 类型 | 约束 | 说明 |
|
|
||||||
|------|------|------|------|
|
|
||||||
| id | BIGSERIAL | PK | |
|
|
||||||
| company_id | BIGINT | NOT NULL FK→sys_company | |
|
|
||||||
| source_id | BIGINT | NOT NULL FK→source_data | |
|
|
||||||
| job_type | VARCHAR(20) | NOT NULL | FRAME_EXTRACT / VIDEO_TO_TEXT |
|
|
||||||
| status | VARCHAR(20) | NOT NULL DEFAULT 'PENDING' | 见状态机 |
|
|
||||||
| params | JSONB | NOT NULL | 处理参数 |
|
|
||||||
| total_units | INT | — | 总帧数/片段数 |
|
|
||||||
| processed_units | INT | NOT NULL DEFAULT 0 | |
|
|
||||||
| output_path | VARCHAR(500) | — | |
|
|
||||||
| retry_count | INT | NOT NULL DEFAULT 0 | |
|
|
||||||
| max_retries | INT | NOT NULL DEFAULT 3 | |
|
|
||||||
| error_message | TEXT | — | |
|
|
||||||
| started_at / completed_at | TIMESTAMP | — | |
|
|
||||||
| created_at / updated_at | TIMESTAMP | NOT NULL | |
|
|
||||||
|
|
||||||
**状态机**:
|
|
||||||
```
|
|
||||||
PENDING → RUNNING
|
|
||||||
RUNNING → SUCCESS(处理成功)
|
|
||||||
RUNNING → RETRYING(失败且 retry_count < max_retries)
|
|
||||||
RUNNING → FAILED(失败且 retry_count >= max_retries)
|
|
||||||
RETRYING → RUNNING(AI 服务自动重试)
|
|
||||||
RETRYING → FAILED(超过最大重试次数)
|
|
||||||
```
|
|
||||||
*FAILED → PENDING:由 ADMIN 手动触发接口,不在状态机自动流转中*
|
|
||||||
|
|
||||||
**幂等规则**: 回调时若 `status == SUCCESS` 则静默忽略,不执行任何 DB 写入
|
|
||||||
|
|
||||||
**索引**: `(source_id)`、`(status)`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Redis 数据结构
|
|
||||||
|
|
||||||
| Key 模式 | 类型 | TTL | 内容 |
|
|
||||||
|---------|------|-----|------|
|
|
||||||
| `token:{uuid}` | Hash | 2h(滑动) | `{userId, role, companyId, username}` |
|
|
||||||
| `user:perm:{userId}` | String | 5min | 用户角色字符串 |
|
|
||||||
| `task:claim:{taskId}` | String | 30s | 持有者 userId |
|
|
||||||
|
|
||||||
*禁止在上述三类命名空间之外自造 Key 用于认证、权限或锁目的。*
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# 实施计划:label_backend 知识图谱智能标注平台
|
|
||||||
|
|
||||||
**分支**: `001-label-backend-spec` | **日期**: 2026-04-09 | **规格说明**: [spec.md](spec.md)
|
|
||||||
**输入**: 功能规格说明 `/specs/001-label-backend-spec/spec.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 摘要
|
|
||||||
|
|
||||||
构建面向多租户的知识图谱智能标注平台后端服务,驱动**文本线**(三元组提取 → 问答对生成 → 训练样本)和**图片线**(四元组提取 → 问答对生成 → 训练样本)两条流水线。视频作为预处理入口异步汇入两条流水线。系统基于 Spring Boot 3 + Apache Shiro + MyBatis Plus + PostgreSQL + Redis + RustFS 构建,通过 HTTP 调用 Python FastAPI AI 服务完成 AI 辅助标注和问答生成能力。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技术上下文
|
|
||||||
|
|
||||||
**语言/版本**: Java 17(LTS)
|
|
||||||
**主要依赖**: Spring Boot ≥ 3.0.x、Apache Shiro ≥ 1.13.x、MyBatis Plus ≥ 3.5.x、Spring Data Redis
|
|
||||||
**存储**: PostgreSQL ≥ 14(主库)、Redis ≥ 6.x(会话/权限缓存/分布式锁)、RustFS(S3 兼容对象存储)
|
|
||||||
**测试**: JUnit 5 + Testcontainers(真实 PostgreSQL + Redis 实例)、Spring Boot Test
|
|
||||||
**目标平台**: Linux 服务器,Docker Compose 容器化部署
|
|
||||||
**项目类型**: Web Service(REST API)
|
|
||||||
**性能目标**: 任务领取并发下有且仅有一人成功;权限变更延迟 < 1 秒生效
|
|
||||||
**约束**: 禁止 JWT;禁止 Spring Security;禁止文件字节流存入数据库;AI HTTP 调用禁止在 @Transactional 内同步执行;所有列表接口强制分页
|
|
||||||
**规模**: 多租户(多公司),每公司独立数据空间;11 张核心业务表
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 宪章合规检查
|
|
||||||
|
|
||||||
*门控:Phase 0 研究前必须通过。Phase 1 设计后重检。*
|
|
||||||
|
|
||||||
| # | 宪章原则 | 状态 | 说明 |
|
|
||||||
|---|---------|------|------|
|
|
||||||
| 1 | 环境约束(JDK 17、SB 3、Shiro、MyBatis Plus) | ✅ 通过 | pom.xml 中版本约束与宪章完全对齐;无 Spring Security 引入 |
|
|
||||||
| 2 | 多租户数据隔离(company_id + ThreadLocal) | ✅ 通过 | TenantLineInnerInterceptor 自动注入;CompanyContext 在 finally 块清理 |
|
|
||||||
| 3 | BCrypt 密码 + UUID Token + 禁 JWT | ✅ 通过 | AuthService 使用 BCrypt ≥ 10;UUID v4 Token 存 Redis;无 JWT 库 |
|
|
||||||
| 4 | 分级 RBAC + 权限注解 + 角色变更驱逐缓存 | ✅ 通过 | @RequiresRoles 声明权限;updateRole() 立即删 user:perm:{userId} |
|
|
||||||
| 5 | 双流水线 + 级联触发 + parent_source_id 溯源 | ✅ 通过 | 仅文本线/图片线;审批通过用 @TransactionalEventListener 触发 QA |
|
|
||||||
| 6 | 状态机完整性(StateValidator) | ✅ 通过 | 所有状态变更经 StateValidator.assertTransition();禁止绕过 Mapper 直写 |
|
|
||||||
| 7 | 任务争抢双重保障(Redis SET NX + DB 乐观锁) | ✅ 通过 | task:claim:{taskId} TTL 30s + WHERE status='UNCLAIMED' |
|
|
||||||
| 8 | 异步视频处理幂等 + 重试上限 + FAILED 手动重置 | ✅ 通过 | SUCCESS 回调静默忽略;retry_count ≥ max_retries → FAILED |
|
|
||||||
| 9 | 只追加审计日志 + AOP 切面 + 审计失败不回滚业务 | ✅ 通过 | @OperationLog AOP;sys_operation_log 无 UPDATE/DELETE;异常仅 error 日志 |
|
|
||||||
| 10 | RESTful URL + 统一响应格式 + 强制分页 | ✅ 通过 | Result<T> 包装;无动词路径;PageResult<T> 分页 |
|
|
||||||
| 11 | YAGNI:业务在 Service,Controller 只处理 HTTP | ✅ 通过 | 分层明确;无预测性抽象层 |
|
|
||||||
|
|
||||||
**门控结果:全部通过,可进入 Phase 0。**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
### 规格说明文档(本功能)
|
|
||||||
|
|
||||||
```text
|
|
||||||
specs/001-label-backend-spec/
|
|
||||||
├── plan.md # 本文件(/speckit.plan 输出)
|
|
||||||
├── research.md # Phase 0 输出
|
|
||||||
├── data-model.md # Phase 1 输出
|
|
||||||
├── quickstart.md # Phase 1 输出
|
|
||||||
├── contracts/ # Phase 1 输出(REST API 契约)
|
|
||||||
│ ├── auth.md
|
|
||||||
│ ├── source.md
|
|
||||||
│ ├── tasks.md
|
|
||||||
│ ├── extraction.md
|
|
||||||
│ ├── qa.md
|
|
||||||
│ ├── export.md
|
|
||||||
│ ├── config.md
|
|
||||||
│ └── video.md
|
|
||||||
└── tasks.md # Phase 2 输出(/speckit.tasks 命令创建,非本命令)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 源代码(仓库根目录)
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
└── main/
|
|
||||||
└── java/com/label/
|
|
||||||
├── LabelBackendApplication.java
|
|
||||||
├── common/
|
|
||||||
│ ├── result/ # Result<T>、ResultCode、PageResult<T>
|
|
||||||
│ ├── exception/ # BusinessException、GlobalExceptionHandler
|
|
||||||
│ ├── context/ # CompanyContext(ThreadLocal)
|
|
||||||
│ ├── shiro/ # TokenFilter、UserRealm、ShiroConfig
|
|
||||||
│ ├── redis/ # RedisKeyManager、RedisService
|
|
||||||
│ ├── aop/ # AuditAspect、@OperationLog 注解
|
|
||||||
│ ├── storage/ # RustFsClient(S3 兼容封装)
|
|
||||||
│ ├── ai/ # AiServiceClient(RestClient 封装 8 个端点)
|
|
||||||
│ └── statemachine/ # StateValidator、各状态枚举
|
|
||||||
└── module/
|
|
||||||
├── user/ # AuthController、UserController、AuthService、UserService
|
|
||||||
├── source/ # SourceController、SourceService
|
|
||||||
├── task/ # TaskController、TaskService、TaskClaimService
|
|
||||||
├── annotation/ # ExtractionController、QaController、ExtractionService、QaService
|
|
||||||
├── export/ # ExportController、ExportService、FinetuneService
|
|
||||||
├── config/ # SysConfigController、SysConfigService
|
|
||||||
└── video/ # VideoController、VideoProcessService
|
|
||||||
|
|
||||||
src/
|
|
||||||
└── test/
|
|
||||||
└── java/com/label/
|
|
||||||
├── integration/ # Testcontainers(真实 PG + Redis)集成测试
|
|
||||||
│ ├── AuthIntegrationTest.java
|
|
||||||
│ ├── TaskClaimConcurrencyTest.java
|
|
||||||
│ ├── VideoCallbackIdempotencyTest.java
|
|
||||||
│ ├── MultiTenantIsolationTest.java
|
|
||||||
│ └── ShiroFilterIntegrationTest.java
|
|
||||||
└── unit/ # 纯单元测试(状态机、业务逻辑)
|
|
||||||
└── StateMachineTest.java
|
|
||||||
|
|
||||||
sql/
|
|
||||||
└── init.sql # 全部 DDL(11 张表,按依赖顺序执行)
|
|
||||||
|
|
||||||
docker-compose.yml # postgres、redis、rustfs、backend、ai-service、frontend
|
|
||||||
Dockerfile # eclipse-temurin:17-jre-alpine
|
|
||||||
pom.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
**结构决策**:单一后端服务(Web Service),无前端代码。标准 Maven 项目布局,源代码在 `src/main/java/com/label/`,测试在 `src/test/java/com/label/`,按 `common/` + `module/` 两层分包。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 复杂度追踪
|
|
||||||
|
|
||||||
> 宪章检查无违规,本节留空。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## GSTACK REVIEW REPORT
|
|
||||||
|
|
||||||
| Review | Trigger | Why | Runs | Status | Findings |
|
|
||||||
|--------|---------|-----|------|--------|----------|
|
|
||||||
| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — |
|
|
||||||
| Codex Review | `/codex review` | Independent 2nd opinion | 0 | — | — |
|
|
||||||
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 0 | — | — |
|
|
||||||
| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — | — |
|
|
||||||
|
|
||||||
**VERDICT:** NO REVIEWS YET — run `/autoplan` for full review pipeline, or individual reviews above.
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
# 快速启动指南:label_backend
|
|
||||||
|
|
||||||
**日期**: 2026-04-09
|
|
||||||
**分支**: `001-label-backend-spec`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 前置条件
|
|
||||||
|
|
||||||
- Docker Desktop ≥ 4.x(含 Docker Compose v2)
|
|
||||||
- JDK 17(本地开发时)
|
|
||||||
- Maven ≥ 3.8(本地开发时)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、使用 Docker Compose 启动完整环境
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 克隆仓库
|
|
||||||
git clone <repo-url>
|
|
||||||
cd label_backend
|
|
||||||
|
|
||||||
# 启动所有服务(PostgreSQL + Redis + RustFS + AI Service + Backend + Frontend)
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# 查看后端启动日志
|
|
||||||
docker compose logs -f backend
|
|
||||||
|
|
||||||
# 检查健康状态
|
|
||||||
docker compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
**服务端口**:
|
|
||||||
| 服务 | 端口 |
|
|
||||||
|------|------|
|
|
||||||
| 前端(Nginx) | http://localhost:80 |
|
|
||||||
| 后端 REST API | http://localhost:8080 |
|
|
||||||
| AI 服务(FastAPI) | http://localhost:8000 |
|
|
||||||
| PostgreSQL | localhost:5432 |
|
|
||||||
| Redis | localhost:6379 |
|
|
||||||
| RustFS S3 API | http://localhost:9000 |
|
|
||||||
| RustFS Web 控制台 | http://localhost:9001 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、初始化数据库
|
|
||||||
|
|
||||||
数据库 DDL 通过 `./sql/init.sql` 在 PostgreSQL 容器启动时自动执行(`docker-entrypoint-initdb.d`)。
|
|
||||||
|
|
||||||
若需手动执行:
|
|
||||||
```bash
|
|
||||||
docker compose exec postgres psql -U label -d label_db -f /docker-entrypoint-initdb.d/init.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
**初始账号**(由 `init.sql` 中的 INSERT 语句创建):
|
|
||||||
| 用户名 | 密码 | 角色 | 公司 |
|
|
||||||
|--------|------|------|------|
|
|
||||||
| admin | admin123 | ADMIN | 演示公司 |
|
|
||||||
| reviewer01 | review123 | REVIEWER | 演示公司 |
|
|
||||||
| annotator01 | annot123 | ANNOTATOR | 演示公司 |
|
|
||||||
| uploader01 | upload123 | UPLOADER | 演示公司 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、本地开发模式(不使用 Docker)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启动依赖服务(仅 PostgreSQL + Redis + RustFS,不启动后端)
|
|
||||||
docker compose up -d postgres redis rustfs
|
|
||||||
|
|
||||||
# 设置环境变量
|
|
||||||
export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/label_db
|
|
||||||
export SPRING_DATASOURCE_USERNAME=label
|
|
||||||
export SPRING_DATASOURCE_PASSWORD=label_password
|
|
||||||
export SPRING_REDIS_HOST=localhost
|
|
||||||
export SPRING_REDIS_PORT=6379
|
|
||||||
export SPRING_REDIS_PASSWORD=redis_password
|
|
||||||
export RUSTFS_ENDPOINT=http://localhost:9000
|
|
||||||
export RUSTFS_ACCESS_KEY=minioadmin
|
|
||||||
export RUSTFS_SECRET_KEY=minioadmin
|
|
||||||
export AI_SERVICE_BASE_URL=http://localhost:8000
|
|
||||||
|
|
||||||
# 编译并启动
|
|
||||||
mvn clean spring-boot:run
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、验证安装
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 登录(获取 Token)
|
|
||||||
curl -X POST http://localhost:8080/api/auth/login \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"companyCode":"DEMO","username":"admin","password":"admin123"}'
|
|
||||||
|
|
||||||
# 期望响应:{"code":"SUCCESS","data":{"token":"...","role":"ADMIN",...}}
|
|
||||||
|
|
||||||
# 2. 使用 Token 访问受保护接口(将 {TOKEN} 替换为上一步返回的 token)
|
|
||||||
curl http://localhost:8080/api/auth/me \
|
|
||||||
-H "Authorization: Bearer {TOKEN}"
|
|
||||||
|
|
||||||
# 期望响应:{"code":"SUCCESS","data":{"username":"admin","role":"ADMIN",...}}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、运行测试
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 运行所有测试(Testcontainers 会自动启动真实 PG + Redis 容器)
|
|
||||||
mvn test
|
|
||||||
|
|
||||||
# 运行特定测试(并发任务领取)
|
|
||||||
mvn test -Dtest=TaskClaimConcurrencyTest
|
|
||||||
|
|
||||||
# 运行集成测试套件
|
|
||||||
mvn test -Dtest=*IntegrationTest
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意**: Testcontainers 需要本地 Docker 可用。首次运行会拉取 PostgreSQL 和 Redis 镜像(约 200MB)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、关键配置项说明
|
|
||||||
|
|
||||||
配置文件位于 `src/main/resources/application.yml`。以下配置项可在运行时通过 `PUT /api/config/{key}` 接口(ADMIN 权限)动态调整,无需重启服务:
|
|
||||||
|
|
||||||
| 配置键 | 说明 | 默认值 |
|
|
||||||
|--------|------|--------|
|
|
||||||
| `token_ttl_seconds` | 会话凭证有效期(秒) | 7200(2小时) |
|
|
||||||
| `model_default` | AI 辅助默认模型 | glm-4 |
|
|
||||||
| `video_frame_interval` | 视频帧提取间隔(帧数) | 30 |
|
|
||||||
| `prompt_extract_text` | 文本三元组提取 Prompt | 见 init.sql |
|
|
||||||
| `prompt_extract_image` | 图片四元组提取 Prompt | 见 init.sql |
|
|
||||||
| `prompt_qa_gen_text` | 文本问答生成 Prompt | 见 init.sql |
|
|
||||||
| `prompt_qa_gen_image` | 图片问答生成 Prompt | 见 init.sql |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、标注流水线快速验证
|
|
||||||
|
|
||||||
```bash
|
|
||||||
TOKEN="your-admin-token"
|
|
||||||
|
|
||||||
# 步骤 1:上传文本资料
|
|
||||||
curl -X POST http://localhost:8080/api/source/upload \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-F "file=@sample.txt" -F "dataType=TEXT"
|
|
||||||
|
|
||||||
# 步骤 2:为资料创建提取任务(sourceId 从上一步响应中获取)
|
|
||||||
curl -X POST http://localhost:8080/api/tasks \
|
|
||||||
-H "Authorization: Bearer $TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"sourceId": 1, "taskType": "AI_ASSISTED", "aiModel": "glm-4"}'
|
|
||||||
|
|
||||||
# 步骤 3:标注员领取任务(使用 annotator01 的 Token)
|
|
||||||
ANNOTATOR_TOKEN="annotator-token"
|
|
||||||
curl -X POST http://localhost:8080/api/tasks/1/claim \
|
|
||||||
-H "Authorization: Bearer $ANNOTATOR_TOKEN"
|
|
||||||
|
|
||||||
# 步骤 4:获取 AI 预标注结果
|
|
||||||
curl http://localhost:8080/api/extraction/1 \
|
|
||||||
-H "Authorization: Bearer $ANNOTATOR_TOKEN"
|
|
||||||
|
|
||||||
# 步骤 5:提交标注结果
|
|
||||||
curl -X PUT http://localhost:8080/api/extraction/1 \
|
|
||||||
-H "Authorization: Bearer $ANNOTATOR_TOKEN" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"items":[{"subject":"北京","predicate":"是首都","object":"中国","sourceText":"北京是中国的首都","startOffset":0,"endOffset":8}]}'
|
|
||||||
|
|
||||||
curl -X POST http://localhost:8080/api/extraction/1/submit \
|
|
||||||
-H "Authorization: Bearer $ANNOTATOR_TOKEN"
|
|
||||||
|
|
||||||
# 步骤 6:审批员审批通过(使用 reviewer01 的 Token)
|
|
||||||
REVIEWER_TOKEN="reviewer-token"
|
|
||||||
curl -X POST http://localhost:8080/api/extraction/1/approve \
|
|
||||||
-H "Authorization: Bearer $REVIEWER_TOKEN"
|
|
||||||
```
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
# Phase 0 研究报告:label_backend
|
|
||||||
|
|
||||||
**日期**: 2026-04-09
|
|
||||||
**分支**: `001-label-backend-spec`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技术决策汇总
|
|
||||||
|
|
||||||
所有技术选型均由宪章强制约束,无需评估备选方案。本报告记录关键设计决策的理由,供后续实施参考。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 1:认证机制
|
|
||||||
|
|
||||||
**决策**: UUID v4 Token 存储于 Redis,滑动过期,禁止 JWT
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- JWT 自包含令牌无法按需吊销,无法满足"管理员禁用账号立即生效"的安全要求
|
|
||||||
- UUID Token 在 Redis 中可精确控制生命周期:退出登录或禁用账号时同步删除 Key,下一次请求立即失效
|
|
||||||
- 滑动过期(每次有效请求重置 TTL)确保活跃用户不被意外踢出
|
|
||||||
|
|
||||||
**备选方案放弃理由**:
|
|
||||||
- JWT:无法即时吊销,存在安全窗口
|
|
||||||
- Session Cookie:在无状态 REST API 架构中不适用
|
|
||||||
- OAuth2:过度设计,当前场景无第三方授权需求
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 2:多租户隔离机制
|
|
||||||
|
|
||||||
**决策**: MyBatis Plus `TenantLineInnerInterceptor` + `ThreadLocal CompanyContext`
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- `TenantLineInnerInterceptor` 在 SQL 拦截器层自动在每条查询的 WHERE 子句中注入 `company_id`,覆盖范围广且无需逐方法手动添加条件
|
|
||||||
- ThreadLocal 存储当前请求的 `companyId`,由 Shiro TokenFilter 在解析 Token 时从 Redis 会话数据注入,确保 companyId 来自服务端权威来源而非客户端参数
|
|
||||||
- `finally` 块强制清理 ThreadLocal,防止线程池复用时数据串漏
|
|
||||||
|
|
||||||
**备选方案放弃理由**:
|
|
||||||
- 行级安全(RLS):PostgreSQL 原生支持,但与 MyBatis Plus 集成复杂,且宪章已指定 ThreadLocal 方案
|
|
||||||
- 逐方法手动添加 WHERE:容易遗漏,维护成本高
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 3:任务并发领取控制
|
|
||||||
|
|
||||||
**决策**: Redis `SET NX`(分布式锁)+ 数据库乐观约束(`WHERE status = 'UNCLAIMED'`)双重保障
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 单纯使用数据库乐观锁在高并发下存在写放大问题(大量 UPDATE 竞争)
|
|
||||||
- 单纯使用 Redis 锁若锁过期后 DB 写入失败可能导致数据不一致
|
|
||||||
- 双重保障:Redis 锁(TTL 30s)快速拦截大部分并发请求,减少数据库压力;DB 乐观约束作为最终一致性兜底
|
|
||||||
|
|
||||||
**Key 命名**: `task:claim:{taskId}`(TTL 30s,与宪章 Redis Key 规范一致)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 4:审批触发 QA 任务的异步解耦
|
|
||||||
|
|
||||||
**决策**: Spring `@TransactionalEventListener(phase = AFTER_COMMIT)` + `@Transactional(REQUIRES_NEW)`
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 提取阶段审批通过后需调用 AI HTTP 生成候选问答对,该 HTTP 调用延迟不确定(秒级到分钟级)
|
|
||||||
- 若在 `@Transactional` 内同步调用,数据库连接被长时间占用,且 AI 失败会错误地回滚已完成的审批操作
|
|
||||||
- `AFTER_COMMIT` 保证业务审批先提交再触发事件,避免事务回滚导致的幽灵任务
|
|
||||||
- `REQUIRES_NEW` 为 QA 生成开启独立事务,AI 失败仅影响 QA 任务创建,不影响审批结果
|
|
||||||
|
|
||||||
**事件流**: `approve()` → publish `ExtractionApprovedEvent` → 事务提交 → `onExtractionApproved()` 异步执行(AI 调用 + 创建 QA 任务)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 5:标注结果存储语义
|
|
||||||
|
|
||||||
**决策**: JSONB 整体覆盖(PUT 语义),禁止局部 PATCH
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 三元组/四元组条目具有强关联性(主语-谓语-宾语作为整体,或主体-关系-客体-修饰词作为整体),局部更新易导致不一致
|
|
||||||
- 整体替换简化服务端逻辑,前端每次提交完整 items 数组,服务端直接执行 UPDATE `result_json = ?`
|
|
||||||
- 避免局部追加导致的索引层数据不一致(如删除某条目后残留旧数据)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 6:审计日志事务边界
|
|
||||||
|
|
||||||
**决策**: 审计日志写入不要求与业务操作在同一事务,AOP `finally` 块中独立写入
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 审计写入失败不应回滚业务操作(用户的标注/审批结果比审计日志更重要)
|
|
||||||
- `@Around` 通知在业务方法执行完成(commit 或 rollback)后捕获最终 `result`,可记录准确的成功/失败状态
|
|
||||||
- 审计失败仅 error 级别日志 + 告警,不影响用户体验
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 7:视频预处理幂等回调
|
|
||||||
|
|
||||||
**决策**: 回调处理时检查 `video_process_job.status`,已为 `SUCCESS` 则静默忽略
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- AI 服务可能因网络抖动对同一 jobId 发起多次成功回调
|
|
||||||
- 幂等检查确保第一次成功回调创建标注任务,后续重复回调无任何副作用
|
|
||||||
- 检查粒度:`status == SUCCESS` 即返回,不进行任何 DB 写入
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 8:对象存储路径规范
|
|
||||||
|
|
||||||
**决策**: RustFS(S3 兼容),文件字节流禁止入库,路径按资源类型分桶分目录
|
|
||||||
|
|
||||||
**路径规范**:
|
|
||||||
|
|
||||||
| 资源 | 桶 | 路径格式 |
|
|
||||||
|------|-----|---------|
|
|
||||||
| 文本文件 | `source-data` | `text/{yyyyMM}/{source_id}.txt` |
|
|
||||||
| 图片 | `source-data` | `image/{yyyyMM}/{source_id}.jpg` |
|
|
||||||
| 视频 | `source-data` | `video/{yyyyMM}/{source_id}.mp4` |
|
|
||||||
| 视频帧 | `source-data` | `frames/{source_id}/{frame_index}.jpg` |
|
|
||||||
| 视频转文本 | `source-data` | `video-text/{parent_source_id}/{timestamp}.txt` |
|
|
||||||
| bbox 裁剪图 | `source-data` | `crops/{task_id}/{item_index}.jpg` |
|
|
||||||
| 导出 JSONL | `finetune-export` | `export/{batchUuid}.jsonl` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 决策 9:测试策略
|
|
||||||
|
|
||||||
**决策**: 集成测试使用 Testcontainers(真实 PG + Redis),不允许 Mock 数据库
|
|
||||||
|
|
||||||
**必须覆盖的测试场景**:
|
|
||||||
|
|
||||||
1. **并发任务领取**:10 线程同时争抢同一任务,验证恰好 1 人成功(Redis + DB 双重锁)
|
|
||||||
2. **视频回调幂等**:同一 jobId 两次成功回调,验证只创建 1 个 annotation_task
|
|
||||||
3. **状态机越界拒绝**:非法状态转换(如 APPROVED → IN_PROGRESS)抛出 BusinessException
|
|
||||||
4. **多租户隔离**:公司 A 身份访问公司 B 资源,验证被拒绝
|
|
||||||
5. **Shiro 过滤器链**:无 Token → 401;Token 有效但角色不足 → 403
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 无需澄清事项汇总
|
|
||||||
|
|
||||||
| 项目 | 状态 | 来源 |
|
|
||||||
|------|------|------|
|
|
||||||
| 认证方案 | ✅ 已确定(UUID Token) | 宪章原则三 |
|
|
||||||
| 数据库选型 | ✅ 已确定(PostgreSQL) | 宪章原则一 |
|
|
||||||
| ORM | ✅ 已确定(MyBatis Plus) | 宪章原则一 |
|
|
||||||
| 缓存/锁 | ✅ 已确定(Redis) | 宪章原则一 |
|
|
||||||
| 对象存储 | ✅ 已确定(RustFS S3) | 宪章原则一 |
|
|
||||||
| AI 集成方式 | ✅ 已确定(HTTP RestClient) | 宪章原则一 |
|
|
||||||
| 多租户隔离 | ✅ 已确定(ThreadLocal + Interceptor) | 宪章原则二 |
|
|
||||||
| 并发控制 | ✅ 已确定(双重锁) | 宪章原则七 |
|
|
||||||
| 审批事务边界 | ✅ 已确定(@TransactionalEventListener) | 宪章原则五 |
|
|
||||||
| 测试策略 | ✅ 已确定(Testcontainers) | 宪章开发工作流 |
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
# 功能规格说明:label_backend 知识图谱智能标注平台
|
|
||||||
|
|
||||||
**功能分支**: `001-label-backend-spec`
|
|
||||||
**创建日期**: 2026-04-09
|
|
||||||
**状态**: 草稿
|
|
||||||
**输入**: 根据文档 docs/superpowers/specs/2026-04-09-label-backend-design.md 生成需求规格文档
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 用户场景与测试 *(必填)*
|
|
||||||
|
|
||||||
### 用户故事 1 - 用户登录与身份认证 (优先级: P1)
|
|
||||||
|
|
||||||
公司员工使用用户名和密码登录平台,获取会话凭证后访问受权限保护的功能。会话在持续活跃时保持有效,用户主动退出或管理员禁用账号后会话立即失效。
|
|
||||||
|
|
||||||
**优先级理由**: 认证是所有其他功能的前提,无法登录则所有功能均不可用。
|
|
||||||
|
|
||||||
**独立测试**: 可独立通过以下方式测试:用正确凭证登录,携带返回凭证请求受保护接口,验证正常访问;携带错误凭证或过期凭证,验证被拒绝。
|
|
||||||
|
|
||||||
**验收场景**:
|
|
||||||
|
|
||||||
1. **给定** 用户持有效用户名和密码,**当** 提交登录请求,**则** 系统返回会话凭证,且该凭证可用于后续请求
|
|
||||||
2. **给定** 用户已登录并持有效凭证,**当** 发起正常业务请求,**则** 会话有效期自动延长
|
|
||||||
3. **给定** 用户主动退出登录,**当** 使用旧凭证访问任意受保护接口,**则** 系统立即拒绝,返回未授权响应
|
|
||||||
4. **给定** 管理员禁用某用户账号,**当** 被禁用用户使用现有凭证访问接口,**则** 系统立即拒绝,不设任何宽限期
|
|
||||||
5. **给定** 用户使用错误密码,**当** 提交登录请求,**则** 系统返回认证失败,不泄露用户是否存在
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 用户故事 2 - 原始资料上传 (优先级: P1)
|
|
||||||
|
|
||||||
上传员将文本文件、图片或视频上传至平台,系统存储文件并记录元数据,视频文件额外触发异步预处理流程(帧提取或转文字)。
|
|
||||||
|
|
||||||
**优先级理由**: 资料上传是整条标注流水线的起点,没有资料则无法产生任何标注任务。
|
|
||||||
|
|
||||||
**独立测试**: 可独立测试:上传一个文本文件或图片,验证系统成功接收并记录;上传视频,验证系统创建预处理任务并开始异步处理。
|
|
||||||
|
|
||||||
**验收场景**:
|
|
||||||
|
|
||||||
1. **给定** 上传员已登录,**当** 上传一个文本文件,**则** 系统保存文件并创建资料记录,状态为"待提取"
|
|
||||||
2. **给定** 上传员已登录,**当** 上传一张图片,**则** 系统保存图片并创建资料记录,状态为"待提取"
|
|
||||||
3. **给定** 上传员已登录,**当** 上传一个视频文件,**则** 系统保存视频,创建预处理任务,资料状态变为"预处理中"
|
|
||||||
4. **给定** 视频预处理成功完成,**当** AI 服务回调成功,**则** 每帧(帧模式)或每段转译文本(片段模式)均作为独立资料进入标注队列,原视频状态变为"已完成"
|
|
||||||
5. **给定** 视频预处理因 AI 服务故障失败且已达最大重试次数,**当** 回调失败,**则** 任务标记为失败,管理员可查阅错误信息并手动重新触发
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 用户故事 3 - 提取阶段标注(EXTRACTION) (优先级: P1)
|
|
||||||
|
|
||||||
标注员从任务池中领取一个提取任务,借助 AI 辅助预标注对文本资料完成三元组标注或对图片资料完成四元组标注,提交后由审批员审核。
|
|
||||||
|
|
||||||
**优先级理由**: 提取阶段是双流水线的第一个生产阶段,直接产出结构化知识。
|
|
||||||
|
|
||||||
**独立测试**: 可独立测试:标注员领取任务,修改 AI 预标注结果,提交后验证任务进入"待审批"状态;同一任务被多人同时尝试领取,验证只有一人成功。
|
|
||||||
|
|
||||||
**验收场景**:
|
|
||||||
|
|
||||||
1. **给定** 存在未被领取的提取任务,**当** 标注员请求领取,**则** 任务归属到该标注员,状态变为"进行中"
|
|
||||||
2. **给定** 同一任务被 10 名标注员同时争抢,**当** 所有人同时发起领取请求,**则** 恰好一名标注员领取成功,其余人收到"任务已被他人领取"响应
|
|
||||||
3. **给定** 标注员已领取任务,**当** 请求 AI 辅助预标注,**则** 系统调用 AI 服务返回结构化候选结果(不直接提交,供人工编辑)
|
|
||||||
4. **给定** 标注员完成人工编辑,**当** 提交标注结果,**则** 任务状态变为"已提交",进入审批队列
|
|
||||||
5. **给定** 标注员领取任务后决定放弃,**当** 放弃任务,**则** 任务回到任务池,可被其他标注员重新领取
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 用户故事 4 - 提取阶段审批 (优先级: P1)
|
|
||||||
|
|
||||||
审批员查看提交的提取标注结果,选择通过或驳回。审批通过后系统自动创建问答生成任务;驳回时需填写驳回原因,标注员可重新领取该任务修改后再次提交。
|
|
||||||
|
|
||||||
**优先级理由**: 审批控制标注质量,是推进流水线到下一阶段的门控节点。
|
|
||||||
|
|
||||||
**独立测试**: 可独立测试:审批通过一个提取任务,验证系统自动创建 QA 生成任务;驳回一个任务,验证标注员可重领并修改。
|
|
||||||
|
|
||||||
**验收场景**:
|
|
||||||
|
|
||||||
1. **给定** 审批员进入待审批队列,**当** 查看列表,**则** 只看到状态为"已提交"的任务
|
|
||||||
2. **给定** 审批员查看某提取任务的标注结果,**当** 点击通过,**则** 标注结果标记为最终版,系统自动创建对应的问答生成任务并置于任务池中
|
|
||||||
3. **给定** 审批员本人提交了某提取任务,**当** 该审批员尝试审批自己提交的任务,**则** 系统拒绝,提示不允许自审
|
|
||||||
4. **给定** 审批员认为标注结果不合格,**当** 附带驳回原因并驳回,**则** 任务状态变为"已驳回",标注员可在我的任务列表中看到该任务及原因
|
|
||||||
5. **给定** 标注员查看被驳回的任务,**当** 重新领取并修改后提交,**则** 任务重新进入审批队列
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 用户故事 5 - 问答生成阶段标注与审批(QA_GENERATION) (优先级: P2)
|
|
||||||
|
|
||||||
标注员领取问答生成任务,在 AI 候选问答对基础上完成人工编辑,提交后由审批员审批。审批通过即写入训练样本库;驳回则退回标注员修改。
|
|
||||||
|
|
||||||
**优先级理由**: QA 阶段是流水线的最后生产阶段,直接决定训练样本质量。
|
|
||||||
|
|
||||||
**独立测试**: 可独立测试:领取 QA 任务,修改候选问答对并提交;审批员通过后,验证训练样本库中出现对应记录。
|
|
||||||
|
|
||||||
**验收场景**:
|
|
||||||
|
|
||||||
1. **给定** 存在由提取阶段审批通过自动创建的问答生成任务,**当** 标注员进入任务池,**则** 可以看到并领取该任务
|
|
||||||
2. **给定** 标注员已领取问答生成任务,**当** 整体提交修改后的问答对列表,**则** 任务进入审批队列(每次提交均为完整列表替换,不允许部分追加)
|
|
||||||
3. **给定** 审批员通过问答生成任务,**当** 审批完成,**则** 对应训练样本状态变为"已审批",整条资料流水线标记为完成
|
|
||||||
4. **给定** 审批员驳回问答生成任务,**当** 驳回完成,**则** 候选问答对记录被清除,标注员可重领任务重新生成
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 用户故事 6 - 训练数据导出与微调提交 (优先级: P2)
|
|
||||||
|
|
||||||
管理员从已审批的训练样本中选择一批次,导出为 GLM 微调格式的 JSONL 文件,并可选择一键提交至 GLM 微调服务。
|
|
||||||
|
|
||||||
**优先级理由**: 导出是将标注成果转化为 AI 训练价值的最终步骤。
|
|
||||||
|
|
||||||
**独立测试**: 可独立测试:选择若干已审批样本创建导出批次,验证生成 JSONL 文件;将批次提交微调服务,验证可查询到微调任务状态。
|
|
||||||
|
|
||||||
**验收场景**:
|
|
||||||
|
|
||||||
1. **给定** 管理员查看样本库,**当** 筛选已审批样本,**则** 只返回状态为"已审批"的样本(分页,不可无界查询)
|
|
||||||
2. **给定** 管理员选择若干已审批样本,**当** 创建导出批次,**则** 系统生成 JSONL 文件并存储,返回批次标识;若任意样本不处于已审批状态则整批失败
|
|
||||||
3. **给定** 导出批次已创建,**当** 管理员提交微调任务,**则** 系统向 AI 服务发起微调请求,记录微调任务标识,状态变为"进行中"
|
|
||||||
4. **给定** 微调任务已提交,**当** 管理员查询状态,**则** 返回最新的微调进度信息
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 用户故事 7 - 用户与权限管理 (优先级: P2)
|
|
||||||
|
|
||||||
管理员管理本公司用户,包括创建用户、分配角色、启用/禁用账号。角色变更和账号禁用在保存后立即生效,无延迟窗口。
|
|
||||||
|
|
||||||
**优先级理由**: 人员和权限管理是平台运营的基础管控能力。
|
|
||||||
|
|
||||||
**独立测试**: 可独立测试:创建一个标注员角色用户,验证该用户可以领取任务但无法执行审批;将其角色升为审批员,立即验证可以审批;禁用该用户,验证其现有会话立即失效。
|
|
||||||
|
|
||||||
**验收场景**:
|
|
||||||
|
|
||||||
1. **给定** 管理员创建一个新用户并分配角色,**当** 新用户登录,**则** 该用户拥有该角色对应的权限(高级角色自动包含低级角色权限)
|
|
||||||
2. **给定** 管理员将用户角色从标注员升为审批员,**当** 角色变更保存后,**则** 该用户无需重新登录即可使用审批功能
|
|
||||||
3. **给定** 管理员禁用某用户账号,**当** 被禁用用户下次发起请求,**则** 系统立即返回拒绝响应,不设过渡期
|
|
||||||
4. **给定** 管理员查询用户列表,**当** 获取结果,**则** 仅返回本公司用户,不可看到其他公司用户数据
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 用户故事 8 - 系统配置管理 (优先级: P3)
|
|
||||||
|
|
||||||
管理员维护 AI Prompt 模板、模型参数、Token 有效期等系统配置项,支持公司级配置覆盖全局默认值。
|
|
||||||
|
|
||||||
**优先级理由**: 配置管理是运营支撑能力,可在系统运行后按需调整,不影响核心标注流程。
|
|
||||||
|
|
||||||
**独立测试**: 可独立测试:修改某公司的 Prompt 模板配置,验证该公司后续标注使用新模板,其他公司仍使用全局默认值。
|
|
||||||
|
|
||||||
**验收场景**:
|
|
||||||
|
|
||||||
1. **给定** 管理员查看配置列表,**当** 获取结果,**则** 同时展示本公司专属配置和全局默认配置,公司专属配置对同一 Key 优先
|
|
||||||
2. **给定** 管理员更新某配置项,**当** 保存成功,**则** 后续相关操作立即使用新配置值
|
|
||||||
3. **给定** 某配置项仅有全局默认值无公司级覆盖,**当** 系统查询该配置,**则** 返回全局默认值
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 边界情况
|
|
||||||
|
|
||||||
- 标注员领取任务后长时间未操作——管理员可强制转移任务给其他标注员(状态保持"进行中",持有人变更)
|
|
||||||
- 视频预处理回调因网络抖动发生重复投递——系统对同一任务的重复成功回调静默忽略,不重复创建标注任务
|
|
||||||
- 某租户上传量极大时的无界查询——所有列表接口强制分页,无法绕过分页限制获取全量数据
|
|
||||||
- 审批员同时兼任标注员角色时尝试自审——系统按提交者身份校验,自审请求被拒绝
|
|
||||||
- 跨公司数据访问尝试——每次数据查询自动注入当前用户所属公司标识,无法通过参数篡改访问其他公司数据
|
|
||||||
- 操作日志写入失败——审计写入失败不影响业务操作,仅记录错误并触发告警
|
|
||||||
- 同一账号在多设备登录——每次登录生成独立会话凭证,互不影响;退出某设备仅使该设备凭证失效
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 需求说明 *(必填)*
|
|
||||||
|
|
||||||
### 功能性需求
|
|
||||||
|
|
||||||
**认证与会话**
|
|
||||||
|
|
||||||
- **FR-001**: 系统必须支持基于用户名和密码的登录认证,验证通过后返回会话凭证
|
|
||||||
- **FR-002**: 系统必须在每次有效请求时自动延长会话有效期(滑动过期)
|
|
||||||
- **FR-003**: 系统必须支持主动退出登录,退出后凭证立即失效
|
|
||||||
- **FR-004**: 系统必须在管理员禁用账号后立即使该账号所有有效凭证失效,不设任何宽限期
|
|
||||||
- **FR-005**: 系统必须拒绝无凭证或过期凭证的请求,返回未授权响应
|
|
||||||
|
|
||||||
**访问控制**
|
|
||||||
|
|
||||||
- **FR-006**: 系统必须实现四级角色体系:上传员 ⊂ 标注员 ⊂ 审批员 ⊂ 管理员,高级角色自动继承低级角色权限
|
|
||||||
- **FR-007**: 系统必须在接口层声明每个接口所需的最低角色,角色不足时拒绝访问
|
|
||||||
- **FR-008**: 系统必须在角色变更保存后立即生效,无需等待会话自然过期
|
|
||||||
|
|
||||||
**多租户数据隔离**
|
|
||||||
|
|
||||||
- **FR-009**: 系统必须保证每个公司的数据完全隔离,任何查询均只返回当前用户所属公司的数据
|
|
||||||
- **FR-010**: 系统必须禁止调用方通过请求参数指定公司标识来访问其他公司数据;公司标识必须从服务端会话中获取
|
|
||||||
- **FR-011**: 全局系统配置对所有公司可见,公司级配置对同一配置项优先覆盖全局值
|
|
||||||
|
|
||||||
**资料管理**
|
|
||||||
|
|
||||||
- **FR-012**: 系统必须支持文本、图片、视频三种原始资料的上传,文件内容存储至对象存储服务,数据库只保存元数据和存储路径
|
|
||||||
- **FR-013**: 视频上传后必须触发异步预处理任务,不阻塞上传响应
|
|
||||||
- **FR-014**: 系统必须支持视频帧提取模式(每帧作为独立图片进入图片标注流水线)和视频片段转文本模式(派生文本资料进入文本标注流水线)
|
|
||||||
- **FR-015**: 视频片段转文本产生的派生资料必须记录对原始视频资料的引用,可追溯来源
|
|
||||||
|
|
||||||
**任务管理**
|
|
||||||
|
|
||||||
- **FR-016**: 系统必须支持并发安全的任务领取机制,确保同一任务不会被两名标注员同时持有
|
|
||||||
- **FR-017**: 系统必须支持任务放弃(退回任务池)和管理员强制转移任务归属
|
|
||||||
- **FR-018**: 每次任务状态变更必须记录历史快照(含操作人、操作时间、驳回原因等),不可修改或删除历史记录
|
|
||||||
- **FR-019**: 所有任务列表接口必须强制分页,不允许无界查询
|
|
||||||
|
|
||||||
**提取阶段标注工作台**
|
|
||||||
|
|
||||||
- **FR-020**: 系统必须调用 AI 服务生成候选提取结果供标注员参考编辑,不直接写入最终结果
|
|
||||||
- **FR-021**: 标注员提交的提取结果以整体替换方式存储,禁止局部追加修改
|
|
||||||
- **FR-022**: 审批员审批通过时,系统必须在同一操作中将提取结果标记为最终版并自动创建问答生成任务,该级联操作不得由前端发起独立请求触发
|
|
||||||
- **FR-023**: 系统必须拒绝提交者本人审批或驳回自己提交的任务(禁止自审)
|
|
||||||
- **FR-024**: 审批驳回时,标注员必须可以看到被驳回任务及驳回原因,并可重新领取修改后再次提交
|
|
||||||
|
|
||||||
**问答生成阶段**
|
|
||||||
|
|
||||||
- **FR-025**: 问答生成任务的标注结果采用整体替换,每次提交包含完整问答对列表
|
|
||||||
- **FR-026**: 问答生成阶段审批通过时,对应训练样本必须写入训练样本库,资料状态标记为"已完成"
|
|
||||||
- **FR-027**: 问答生成阶段审批驳回时,候选问答对记录必须被清除,标注员可重领任务重新生成
|
|
||||||
|
|
||||||
**训练数据导出**
|
|
||||||
|
|
||||||
- **FR-028**: 系统必须支持将已审批的训练样本批量导出为 GLM 微调格式,每条样本一行
|
|
||||||
- **FR-029**: 导出时若任意选定样本不处于已审批状态,整批导出请求必须失败
|
|
||||||
- **FR-030**: 系统必须支持将导出批次提交至外部 AI 微调服务,并可追踪微调任务进度
|
|
||||||
|
|
||||||
**审计日志**
|
|
||||||
|
|
||||||
- **FR-031**: 系统必须对所有状态变更操作自动记录审计日志,包含操作人姓名快照、操作类型、结果、IP 地址等信息
|
|
||||||
- **FR-032**: 审计日志只追加不修改,禁止对审计记录执行更新或删除
|
|
||||||
- **FR-033**: 审计日志写入失败不得导致业务操作失败或回滚
|
|
||||||
|
|
||||||
**视频异步处理**
|
|
||||||
|
|
||||||
- **FR-034**: 视频预处理任务必须支持自动重试,达到最大重试次数后置为失败状态,需管理员手动重新触发
|
|
||||||
- **FR-035**: AI 服务对同一视频处理任务的重复成功回调必须被幂等处理,不得重复创建标注任务
|
|
||||||
|
|
||||||
### 核心实体
|
|
||||||
|
|
||||||
- **公司(Company)**: 多租户根节点,每个公司拥有独立的用户、资料和任务数据空间
|
|
||||||
- **用户(User)**: 属于某公司,拥有角色(上传员/标注员/审批员/管理员),通过会话凭证访问系统
|
|
||||||
- **原始资料(SourceData)**: 待标注的文件(文本/图片/视频),拥有状态流转(待处理→提取中→QA审核中→已完成);视频派生资料通过父资料引用保留溯源链
|
|
||||||
- **标注任务(AnnotationTask)**: 标注工作单元,分提取阶段和问答生成阶段,拥有领取、提交、审批、驳回完整生命周期
|
|
||||||
- **标注结果(AnnotationResult)**: 提取阶段的结构化输出(三元组或四元组),以整体 JSON 存储
|
|
||||||
- **训练样本(TrainingDataset)**: 经审批的问答对,GLM 微调格式,待导出
|
|
||||||
- **导出批次(ExportBatch)**: 一批训练样本的导出记录,关联外部微调任务标识
|
|
||||||
- **视频处理任务(VideoProcessJob)**: 视频预处理的异步任务跟踪,包含重试计数和最终输出路径
|
|
||||||
- **系统配置(SysConfig)**: 配置键值对,分全局默认和公司级两层,公司级优先
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 成功标准 *(必填)*
|
|
||||||
|
|
||||||
### 可度量结果
|
|
||||||
|
|
||||||
- **SC-001**: 同一标注任务被多人同时争抢时,有且仅有一人领取成功,其余人立即收到明确的"已被领取"响应,成功率 100%,无数据竞争导致的双重持有
|
|
||||||
- **SC-002**: 管理员禁用账号或变更角色后,该账号的权限变更在下一次请求时立即生效(延迟小于 1 秒)
|
|
||||||
- **SC-003**: 提取阶段审批通过时,问答生成任务在同一次操作中自动出现在任务池,无需任何人工干预步骤
|
|
||||||
- **SC-004**: 视频预处理回调的重复投递(同一任务多次成功回调)不产生重复标注任务,幂等处理成功率 100%
|
|
||||||
- **SC-005**: 跨公司数据访问尝试 100% 被系统拒绝,无任何数据泄露至非所属租户
|
|
||||||
- **SC-006**: 审计日志对所有状态变更操作的覆盖率达到 100%,审计写入失败不影响业务成功率
|
|
||||||
- **SC-007**: 所有列表接口在数据量增长时保持稳定响应,用户无法绕过分页限制一次性获取不受限制数量的记录
|
|
||||||
- **SC-008**: 标注员完成一次任务领取→标注→提交的完整操作流程(不含 AI 辅助预标注等待时间)可在 5 分钟内完成
|
|
||||||
- **SC-009**: 从资料上传到训练样本进入样本库的完整流水线(含两次人工标注和两次审批)中,每个节点的操作人、时间、结果均可查询追溯
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 假设与前提
|
|
||||||
|
|
||||||
- 系统服务于多个公司,每家公司的用户、资料和标注数据完全独立,不存在跨公司协作场景
|
|
||||||
- 每位用户在同一时刻只属于一家公司,不存在用户跨公司兼职的场景
|
|
||||||
- 视频预处理(帧提取、转文字)由外部 AI 服务异步完成,后端只负责触发和回调处理
|
|
||||||
- 微调结果的质量评估不在本平台范围内,平台只负责提交微调任务并查询状态
|
|
||||||
- 前端应用已独立开发,本规格仅覆盖后端 API 能力
|
|
||||||
- 所有文件二进制内容存储在兼容 S3 协议的对象存储服务中,不存入关系型数据库
|
|
||||||
- 生产环境使用容器化部署,后端服务、数据库、缓存、对象存储均为独立容器
|
|
||||||
- AI 服务通过 HTTP 提供结构化的提取和问答生成能力,后端不内嵌 AI 模型
|
|
||||||
- 标注流水线中一条资料同一时间只有一个活跃的提取任务或问答生成任务,不支持并行多版本标注
|
|
||||||
- 审计日志的长期归档(超过月分区范围)由数据库运维团队负责,不在本系统范围内
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
# 任务清单:label_backend 知识图谱智能标注平台
|
|
||||||
|
|
||||||
**输入**: `/specs/001-label-backend-spec/` 全部设计文档
|
|
||||||
**前置条件**: plan.md ✅ | spec.md ✅ | research.md ✅ | data-model.md ✅ | contracts/ ✅ | quickstart.md ✅
|
|
||||||
|
|
||||||
## 格式说明
|
|
||||||
|
|
||||||
- **[P]**: 可并行执行(不同文件,无未完成任务的依赖)
|
|
||||||
- **[USn]**: 对应 spec.md 中的用户故事编号
|
|
||||||
- 每条任务包含精确的文件路径
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 1: 项目初始化
|
|
||||||
|
|
||||||
**目标**: 创建 Maven 项目骨架、基础配置和 Docker 环境
|
|
||||||
|
|
||||||
- [ ] T001 创建 Maven 项目骨架(`com.label` GroupId,`label-backend` ArtifactId,Java 17 编译目标)
|
|
||||||
- [ ] T002 配置 `pom.xml`(Spring Boot 3、Apache Shiro 1.13.x、MyBatis Plus 3.5.x、Spring Data Redis、AWS S3 SDK v2、Testcontainers、Lombok)
|
|
||||||
- [ ] T003 [P] 创建 `sql/init.sql`(按依赖顺序建全部 11 张表:sys_company → sys_user → source_data → annotation_task → annotation_result → training_dataset → export_batch → sys_config → sys_operation_log → annotation_task_history → video_process_job;含所有索引和初始配置数据)
|
|
||||||
- [ ] T004 [P] 创建 `docker-compose.yml`(postgres、redis、rustfs、backend、ai-service、frontend 六个服务,含健康检查)和后端 `Dockerfile`(eclipse-temurin:17-jre-alpine)
|
|
||||||
- [ ] T005 创建 `src/main/resources/application.yml`(数据源、Redis、RustFS、AI 服务 base-url、Shiro 相关配置项)
|
|
||||||
|
|
||||||
**检查点**: Maven 编译通过(`mvn compile`),Docker Compose `up -d` 全部服务健康
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 2: 公共基础设施(阻塞性前置条件)
|
|
||||||
|
|
||||||
**目标**: 所有业务模块依赖的公共组件。**必须全部完成后用户故事阶段才能开始**
|
|
||||||
|
|
||||||
**⚠️ 重要**: 此阶段未完成前任何用户故事均不可开始实现
|
|
||||||
|
|
||||||
- [ ] T006 创建 `Result<T>`、`ResultCode`、`PageResult<T>` — `src/main/java/com/label/common/result/`(统一响应格式:`{"code":"SUCCESS","data":{...}}`)
|
|
||||||
- [ ] T007 [P] 创建 `BusinessException`(含 `code`、`message`、`httpStatus`)和 `GlobalExceptionHandler`(`@RestControllerAdvice`)— `src/main/java/com/label/common/exception/`
|
|
||||||
- [ ] T008 [P] 创建 `CompanyContext`(ThreadLocal,含 `set/get/clear` 三个方法,clear 必须在 finally 块调用)— `src/main/java/com/label/common/context/CompanyContext.java`
|
|
||||||
- [ ] T009 创建 `RedisKeyManager`(三个静态方法:`tokenKey`、`userPermKey`、`taskClaimKey`)和 `RedisService` — `src/main/java/com/label/common/redis/`
|
|
||||||
- [ ] T010 创建 MyBatis Plus 配置类 `MybatisPlusConfig`,注册 `TenantLineInnerInterceptor`(从 `CompanyContext` 获取 `companyId` 自动注入 WHERE 子句;`sys_company`、`sys_config` 加入忽略表列表)— `src/main/java/com/label/common/config/MybatisPlusConfig.java`
|
|
||||||
- [ ] T011 创建 `StateValidator`(`assertTransition` 泛型方法,违规时抛出 `BusinessException("INVALID_STATE_TRANSITION",...)`)— `src/main/java/com/label/common/statemachine/StateValidator.java`
|
|
||||||
- [ ] T012 [P] 创建 `SourceStatus` 枚举(PENDING/PREPROCESSING/EXTRACTING/QA_REVIEW/APPROVED,含 TRANSITIONS Map)— `src/main/java/com/label/common/statemachine/SourceStatus.java`
|
|
||||||
- [ ] T013 [P] 创建 `TaskStatus` 枚举(UNCLAIMED/IN_PROGRESS/SUBMITTED/APPROVED/REJECTED,含 TRANSITIONS Map,含 IN_PROGRESS→IN_PROGRESS 用于 ADMIN 强制转移)— `src/main/java/com/label/common/statemachine/TaskStatus.java`
|
|
||||||
- [ ] T014 [P] 创建 `DatasetStatus` 枚举(PENDING_REVIEW/APPROVED/REJECTED,含 TRANSITIONS Map)— `src/main/java/com/label/common/statemachine/DatasetStatus.java`
|
|
||||||
- [ ] T015 [P] 创建 `VideoJobStatus` 枚举(PENDING/RUNNING/SUCCESS/FAILED/RETRYING,含 TRANSITIONS Map,注释说明 FAILED→PENDING 由 ADMIN 手动触发)— `src/main/java/com/label/common/statemachine/VideoJobStatus.java`
|
|
||||||
- [ ] T016 创建 `@OperationLog` 注解(`type` 和 `targetType` 两个属性,`@Around` 级别)— `src/main/java/com/label/common/aop/OperationLog.java`
|
|
||||||
- [ ] T017 创建 `AuditAspect`(`@Around("@annotation(operationLog)")`,在 finally 块以独立操作写入 `sys_operation_log`;审计写入失败只记录 error 日志,禁止抛出异常回滚业务)— `src/main/java/com/label/common/aop/AuditAspect.java`
|
|
||||||
- [ ] T018 [P] 创建 `RustFsClient`(AWS S3 SDK v2 封装,endpoint 指向 RustFS;实现 `upload`、`download`、`delete`、`getPresignedUrl`)— `src/main/java/com/label/common/storage/RustFsClient.java`
|
|
||||||
- [ ] T019 [P] 创建 `AiServiceClient`(`RestClient` 封装,8 个端点:`extractText`、`extractImage`、`extractFrames`、`videoToText`、`genTextQa`、`genImageQa`、`startFinetune`、`getFinetuneStatus`)— `src/main/java/com/label/common/ai/AiServiceClient.java`
|
|
||||||
- [ ] T020 创建 Shiro 三件套:`TokenFilter`(解析 `Authorization: Bearer {uuid}`,查 Redis `token:{uuid}`,注入 `CompanyContext`,请求结束 finally 清理 ThreadLocal)、`UserRealm`(先查 Redis `user:perm:{userId}` TTL 5min,未命中查 PG;含 `addInheritedRoles`)、`ShiroConfig`(过滤器链:`/api/auth/login` → `anon`,`/api/**` → `tokenFilter`)— `src/main/java/com/label/common/shiro/`
|
|
||||||
- [ ] T021 创建 `AbstractIntegrationTest`(Testcontainers,启动真实 PostgreSQL + Redis 容器,执行 sql/init.sql,注入测试用的公司和用户数据)— `src/test/java/com/label/AbstractIntegrationTest.java`
|
|
||||||
- [ ] T022 集成测试:`ShiroFilterIntegrationTest`(无 Token → 401;有效 Token 但角色不足 → 403;有效 Token 且角色满足 → 200)— `src/test/java/com/label/integration/ShiroFilterIntegrationTest.java`
|
|
||||||
- [ ] T023 单元测试:`StateMachineTest`(验证所有枚举的合法转换通过;非法转换抛出 `BusinessException("INVALID_STATE_TRANSITION")`)— `src/test/java/com/label/unit/StateMachineTest.java`
|
|
||||||
|
|
||||||
**检查点**: 基础设施就绪,所有 Phase 3+ 的用户故事可并行开始
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 3: 用户故事 1 — 用户登录与身份认证(优先级: P1)🎯 MVP
|
|
||||||
|
|
||||||
**目标**: 用户可以用用户名和密码登录,获得会话凭证,使用凭证访问受保护接口,退出后凭证立即失效
|
|
||||||
|
|
||||||
**独立测试**: 登录 → 获取 Token → 访问 `/api/auth/me` 返回用户信息 → 退出 → 再次访问返回 401
|
|
||||||
|
|
||||||
- [ ] T024 [P] [US1] 创建 `SysCompany` 实体(MyBatis Plus `@TableName`)和 `SysCompanyMapper` — `src/main/java/com/label/module/user/entity/SysCompany.java` + `mapper/SysCompanyMapper.java`
|
|
||||||
- [ ] T025 [P] [US1] 创建 `SysUser` 实体(`passwordHash` 字段加 `@JsonIgnore`)和 `SysUserMapper`(含 `selectByCompanyAndUsername` 方法)— `src/main/java/com/label/module/user/entity/SysUser.java` + `mapper/SysUserMapper.java`
|
|
||||||
- [ ] T026 [US1] 实现 `AuthService`:`login()`(BCrypt 校验密码 → UUID v4 Token → Redis Hash 存储 userId/role/companyId/username → 设置 TTL = `token_ttl_seconds` 配置值);`logout()`(删除 Redis Token Key)— `src/main/java/com/label/module/user/service/AuthService.java`
|
|
||||||
- [ ] T027 [US1] 实现 `AuthController`:`POST /api/auth/login`(`anon`,调用 `AuthService.login()`)、`POST /api/auth/logout`(已登录)、`GET /api/auth/me`(返回当前用户信息);所有响应用 `Result<T>` 包装 — `src/main/java/com/label/module/user/controller/AuthController.java`
|
|
||||||
- [ ] T028 [US1] 集成测试:正确密码登录返回 Token;Token 有效时 `/api/auth/me` 返回 200;主动退出后再访问返回 401;错误密码登录返回 401 — `src/test/java/com/label/integration/AuthIntegrationTest.java`
|
|
||||||
|
|
||||||
**检查点**: US1 独立可测试 — 登录/退出流程完整可用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 4: 用户故事 2 — 原始资料上传(优先级: P1)
|
|
||||||
|
|
||||||
**目标**: 上传员可以上传文本/图片/视频,查询自己的资料列表;管理员可查看全公司资料
|
|
||||||
|
|
||||||
**独立测试**: 上传文本文件 → 列表查到 → 详情含预签名 URL → 管理员可删除
|
|
||||||
|
|
||||||
- [ ] T029 [P] [US2] 创建 `SourceData` 实体(含 `parentSourceId` 自引用字段)和 `SourceDataMapper`(含 `updateStatus` 方法)— `src/main/java/com/label/module/source/entity/SourceData.java` + `mapper/SourceDataMapper.java`
|
|
||||||
- [ ] T030 [US2] 实现 `SourceService`:`upload()`(先 insert 获取 ID → 构造路径 → 上传 RustFS → 更新 filePath);`list()`(UPLOADER 按 `uploaderId` 过滤,ADMIN 不过滤,强制分页);`findById()`(含 15 分钟预签名 URL);`delete()`(仅 PENDING 状态可删,同步删 RustFS 文件)— `src/main/java/com/label/module/source/service/SourceService.java`
|
|
||||||
- [ ] T031 [US2] 实现 `SourceController`(`POST /api/source/upload`、`GET /api/source/list`、`GET /api/source/{id}`、`DELETE /api/source/{id}`;`@RequiresRoles` 注解声明权限;所有响应 `Result<T>` 包装)— `src/main/java/com/label/module/source/controller/SourceController.java`
|
|
||||||
- [ ] T032 [US2] 集成测试:UPLOADER 上传文本/图片 → 列表仅返回自己的资料;ADMIN 查看列表返回全部;上传视频 → source_data 状态为 PENDING(视频预处理 Phase 9 覆盖);已进入流水线的资料删除返回 409 — `src/test/java/com/label/integration/SourceIntegrationTest.java`
|
|
||||||
|
|
||||||
**检查点**: US2 独立可测试 — 上传/查询/删除流程完整可用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 5: 用户故事 3+4 — 提取阶段标注与审批(优先级: P1)
|
|
||||||
|
|
||||||
**目标**: 标注员可以领取任务(并发安全)、AI 辅助预标注、编辑并提交;审批员可以通过(自动触发 QA 任务)或驳回(标注员可重领)
|
|
||||||
|
|
||||||
**独立测试**: 创建任务 → 标注员领取 → AI 预标注 → 提交 → 审批通过 → QA 任务自动出现在任务池
|
|
||||||
|
|
||||||
### 实体与数据层
|
|
||||||
|
|
||||||
- [ ] T033 [P] [US3] 创建 `AnnotationTask` 实体 + `AnnotationTaskMapper`(含 `claimTask(taskId, userId, companyId)` 方法,SQL:`UPDATE ... SET status='IN_PROGRESS', claimed_by=?, claimed_at=NOW() WHERE id=? AND status='UNCLAIMED' AND company_id=?`,返回影响行数)— `src/main/java/com/label/module/task/entity/AnnotationTask.java` + `mapper/AnnotationTaskMapper.java`
|
|
||||||
- [ ] T034 [P] [US3] 创建 `AnnotationTaskHistory` 实体 + `TaskHistoryMapper` — `src/main/java/com/label/module/task/entity/AnnotationTaskHistory.java` + `mapper/TaskHistoryMapper.java`
|
|
||||||
- [ ] T035 [P] [US3] 创建 `AnnotationResult` 实体 + `AnnotationResultMapper`(含 `updateResultJson` 整体覆盖方法和 `selectByTaskId` 方法)— `src/main/java/com/label/module/annotation/entity/AnnotationResult.java` + `mapper/AnnotationResultMapper.java`
|
|
||||||
|
|
||||||
### 任务管理服务与控制器
|
|
||||||
|
|
||||||
- [ ] T036 [US3] 实现 `TaskClaimService.claim()`(① Redis `SET NX task:claim:{taskId}` TTL 30s,失败抛 `TASK_CLAIMED`;② DB `claimTask()` 影响行数为 0 时抛 `TASK_CLAIMED`;③ `insertHistory(UNCLAIMED→IN_PROGRESS)`)和 `unclaim()`(StateValidator + 清 Redis 锁 + 历史)和 `reclaim()`(校验 REJECTED + claimedBy = 当前用户 + REJECTED→IN_PROGRESS + 历史)— `src/main/java/com/label/module/task/service/TaskClaimService.java`
|
|
||||||
- [ ] T037 [US3] 实现 `TaskService`(`createTask`、`getPool`(按角色过滤:ANNOTATOR→UNCLAIMED/EXTRACTION;REVIEWER→SUBMITTED)、`getMine`(含 IN_PROGRESS/SUBMITTED/REJECTED)、`getPendingReview`(SUBMITTED,分页)、`getById`、`reassign`(ADMIN,仅更新 claimedBy + 历史))— `src/main/java/com/label/module/task/service/TaskService.java`
|
|
||||||
- [ ] T038 [US3] 实现 `TaskController`(10 个端点:`POST /api/tasks`、`GET /api/tasks/pool`、`POST /api/tasks/{id}/claim`、`POST /api/tasks/{id}/unclaim`、`GET /api/tasks/mine`、`POST /api/tasks/{id}/reclaim`、`GET /api/tasks/pending-review`、`GET /api/tasks/{id}`、`GET /api/tasks`、`PUT /api/tasks/{id}/reassign`)— `src/main/java/com/label/module/task/controller/TaskController.java`
|
|
||||||
|
|
||||||
### 提取标注服务与控制器
|
|
||||||
|
|
||||||
- [ ] T039 [US3] 实现 `ExtractionService.aiPreAnnotate()`(调用 `AiServiceClient.extractText/extractImage`,写入 `annotation_result`)和 `updateResult()`(整体覆盖 `result_json`,校验 JSON 格式)— `src/main/java/com/label/module/annotation/service/ExtractionService.java`
|
|
||||||
- [ ] T040 [US3] 实现 `ExtractionService.submit()`(`@Transactional`:IN_PROGRESS→SUBMITTED + `submitted_at` + insertHistory)— `src/main/java/com/label/module/annotation/service/ExtractionService.java`
|
|
||||||
- [ ] T041 [US4] 创建 `ExtractionApprovedEvent`(携带 `taskId`、`sourceId`、`sourceType`、`companyId`)— `src/main/java/com/label/module/annotation/event/ExtractionApprovedEvent.java`
|
|
||||||
- [ ] T042 [US4] 实现 `ExtractionService.approve()`(`@Transactional`:① 自审校验;② `is_final=true`;③ SUBMITTED→APPROVED + `completedAt` + 历史;④ `publishEvent(ExtractionApprovedEvent)`;AI 调用禁止在此事务内执行)— `src/main/java/com/label/module/annotation/service/ExtractionService.java`
|
|
||||||
- [ ] T043 [US4] 实现 `ExtractionApprovedEventListener`(`@TransactionalEventListener(AFTER_COMMIT)` + `@Transactional(REQUIRES_NEW)`:调用 AI 生成候选问答对 → 写 `training_dataset`(PENDING_REVIEW)→ 创建 QA_GENERATION 任务(UNCLAIMED)→ `source_data` 状态→ QA_REVIEW)— `src/main/java/com/label/module/annotation/service/ExtractionApprovedEventListener.java`
|
|
||||||
- [ ] T044 [US4] 实现 `ExtractionService.reject()`(`@Transactional`:① 自审校验;② StateValidator;③ SUBMITTED→REJECTED + 历史)— `src/main/java/com/label/module/annotation/service/ExtractionService.java`
|
|
||||||
- [ ] T045 [US4] 实现 `ExtractionController`(5 个端点:`GET /api/extraction/{taskId}`、`PUT /api/extraction/{taskId}`、`POST /api/extraction/{taskId}/submit`、`POST /api/extraction/{taskId}/approve`、`POST /api/extraction/{taskId}/reject`)— `src/main/java/com/label/module/annotation/controller/ExtractionController.java`
|
|
||||||
|
|
||||||
### 集成测试
|
|
||||||
|
|
||||||
- [ ] T046 [US3] 并发集成测试:10 个线程同时争抢同一 UNCLAIMED 任务,验证恰好 1 人成功、其余均收到 `TASK_CLAIMED` 错误、DB 中 `claimed_by` 唯一 — `src/test/java/com/label/integration/TaskClaimConcurrencyTest.java`
|
|
||||||
- [ ] T047 [US4] 集成测试:审批通过 → QA 任务自动出现在任务池;自审返回 `SELF_REVIEW_FORBIDDEN` 403;驳回后标注员可重领并再次提交 — `src/test/java/com/label/integration/ExtractionApprovalIntegrationTest.java`
|
|
||||||
|
|
||||||
**检查点**: US3+US4 独立可测试 — 完整提取流水线(领取→标注→提交→审批→QA任务自动创建)可用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 6: 用户故事 5 — 问答生成阶段标注与审批(优先级: P2)
|
|
||||||
|
|
||||||
**目标**: 标注员领取 QA 任务、修改候选问答对并提交;审批员通过后训练样本入库,整条流水线完成
|
|
||||||
|
|
||||||
**独立测试**: 领取 QA 任务 → 修改问答对 → 提交 → 审批通过 → training_dataset 状态 APPROVED,source_data 状态 APPROVED
|
|
||||||
|
|
||||||
- [ ] T048 [P] [US5] 创建 `TrainingDataset` 实体 + `TrainingDatasetMapper`(含 `approveByTaskId`、`deleteByTaskId` 方法)— `src/main/java/com/label/module/annotation/entity/TrainingDataset.java` + `mapper/TrainingDatasetMapper.java`
|
|
||||||
- [ ] T049 [US5] 实现 `QaService.updateResult()`(整体覆盖问答对 JSONB)和 `submit()`(`@Transactional`:IN_PROGRESS→SUBMITTED + 历史)— `src/main/java/com/label/module/annotation/service/QaService.java`
|
|
||||||
- [ ] T050 [US5] 实现 `QaService.approve()`(`@Transactional`:① `validateAndGetTask` 先于一切 DB 写入;② 自审校验;③ `training_dataset` → APPROVED;④ `annotation_task` → APPROVED + 历史;⑤ `source_data` → APPROVED)— `src/main/java/com/label/module/annotation/service/QaService.java`
|
|
||||||
- [ ] T051 [US5] 实现 `QaService.reject()`(`@Transactional`:① 自审校验;② `deleteByTaskId` 清除候选问答对;③ SUBMITTED→REJECTED + 历史;④ `source_data` 保持 QA_REVIEW 不变)— `src/main/java/com/label/module/annotation/service/QaService.java`
|
|
||||||
- [ ] T052 [US5] 实现 `QaController`(5 个端点:`GET /api/qa/{taskId}`、`PUT /api/qa/{taskId}`、`POST /api/qa/{taskId}/submit`、`POST /api/qa/{taskId}/approve`、`POST /api/qa/{taskId}/reject`)— `src/main/java/com/label/module/annotation/controller/QaController.java`
|
|
||||||
- [ ] T053 [US5] 集成测试:QA 审批通过 → `training_dataset.status = APPROVED`,`source_data.status = APPROVED`;QA 驳回 → 候选记录被删除,标注员可重领 — `src/test/java/com/label/integration/QaApprovalIntegrationTest.java`
|
|
||||||
|
|
||||||
**检查点**: US5 独立可测试 — 完整 QA 流水线可用,training_dataset 产出验证通过
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 7: 用户故事 6 — 训练数据导出与微调提交(优先级: P2)
|
|
||||||
|
|
||||||
**目标**: 管理员将已审批样本批量导出为 JSONL,并可提交 GLM 微调任务
|
|
||||||
|
|
||||||
**独立测试**: 选取已审批样本 → 创建批次 → RustFS 中存在 JSONL 文件 → 提交微调 → 可查询状态
|
|
||||||
|
|
||||||
- [ ] T054 [P] [US6] 创建 `ExportBatch` 实体 + `ExportBatchMapper` — `src/main/java/com/label/module/export/entity/ExportBatch.java` + `mapper/ExportBatchMapper.java`
|
|
||||||
- [ ] T055 [US6] 实现 `ExportService.createBatch()`(`@Transactional`:① 校验全部样本为 APPROVED;② 生成 JSONL(每行一个 `glm_format_json`);③ 上传 RustFS `finetune-export/export/{batchUuid}.jsonl`;④ 批量更新 `export_batch_id`/`exported_at`;⑤ 插入 `export_batch` 记录)— `src/main/java/com/label/module/export/service/ExportService.java`
|
|
||||||
- [ ] T056 [US6] 实现 `FinetuneService`:`trigger()`(调用 `AiServiceClient.startFinetune()`,更新 `glm_job_id` 和 `finetune_status = RUNNING`)和 `getStatus()`(调用 `AiServiceClient.getFinetuneStatus()`)— `src/main/java/com/label/module/export/service/FinetuneService.java`
|
|
||||||
- [ ] T057 [US6] 实现 `ExportController`(`GET /api/training/samples`、`POST /api/export/batch`、`POST /api/export/{batchId}/finetune`、`GET /api/export/{batchId}/status`、`GET /api/export/list`;全部 `@RequiresRoles("ADMIN")`)— `src/main/java/com/label/module/export/controller/ExportController.java`
|
|
||||||
- [ ] T058 [US6] 集成测试:成功创建批次后 JSONL 文件存在于 RustFS;包含非 APPROVED 样本时返回 `INVALID_SAMPLES` 400 — `src/test/java/com/label/integration/ExportIntegrationTest.java`
|
|
||||||
|
|
||||||
**检查点**: US6 独立可测试 — 导出批次创建和微调提交流程可用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 8: 用户故事 7 — 用户与权限管理(优先级: P2)
|
|
||||||
|
|
||||||
**目标**: 管理员可以创建用户、变更角色(立即生效)、禁用账号(立即失效)
|
|
||||||
|
|
||||||
**独立测试**: 创建标注员用户 → 验证其能领取任务 → 升为审批员 → 验证立即可以审批 → 禁用账号 → 已有 Token 立即失效
|
|
||||||
|
|
||||||
- [ ] T059 [US7] 实现 `UserService`:`createUser()`(BCrypt 哈希密码,强度因子 ≥ 10);`updateUser()`;`updateRole()`(DB 写入后立即 `redisTemplate.delete(userPermKey(userId))`);`updateStatus()`(禁用时删 Redis Token + 权限缓存)— `src/main/java/com/label/module/user/service/UserService.java`
|
|
||||||
- [ ] T060 [US7] 实现 `UserController`(`GET /api/users`、`POST /api/users`、`PUT /api/users/{id}`、`PUT /api/users/{id}/status`、`PUT /api/users/{id}/role`;全部 `@RequiresRoles("ADMIN")`)— `src/main/java/com/label/module/user/controller/UserController.java`
|
|
||||||
- [ ] T061 [US7] 集成测试:变更角色后权限下一次请求立即生效(无需重新登录);禁用账号后现有 Token 下一次请求立即返回 401 — `src/test/java/com/label/integration/UserManagementIntegrationTest.java`
|
|
||||||
|
|
||||||
**检查点**: US7 独立可测试 — 用户管理和即时权限变更可用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 9: 用户故事 8 — 视频处理与系统配置(优先级: P3)
|
|
||||||
|
|
||||||
**目标**: 上传视频后触发异步预处理(帧提取/转文字);AI 回调幂等处理;管理员可配置 Prompt 模板等系统参数
|
|
||||||
|
|
||||||
**独立测试(视频)**: 上传视频 → 创建处理任务 → 模拟成功回调 → annotation_task 出现在任务池;重复成功回调 → 任务数量不增加
|
|
||||||
**独立测试(配置)**: 为公司设置专属 Prompt → 验证该公司使用新值;其他公司使用全局默认
|
|
||||||
|
|
||||||
- [ ] T062 [P] [US8] 创建 `VideoProcessJob` 实体 + `VideoProcessJobMapper` — `src/main/java/com/label/module/video/entity/VideoProcessJob.java` + `mapper/VideoProcessJobMapper.java`
|
|
||||||
- [ ] T063 [P] [US8] 创建 `SysConfig` 实体 + `SysConfigMapper`(含 `selectByCompanyAndKey(companyId, configKey)` 方法,支持 `companyId IS NULL` 查询)— `src/main/java/com/label/module/config/entity/SysConfig.java` + `mapper/SysConfigMapper.java`
|
|
||||||
- [ ] T064 [US8] 实现 `VideoProcessService`:`createJob()`(`@Transactional`:`source_data.status → PREPROCESSING` + 插入 job + 触发 AI 异步调用);`handleCallback()`(`@Transactional`:幂等检查 status==SUCCESS 则 return;成功 → SUCCESS + `source_data.status → PENDING`;失败 → 按 retry_count 决定 RETRYING 或 FAILED);`reset()`(FAILED → PENDING)— `src/main/java/com/label/module/video/service/VideoProcessService.java`
|
|
||||||
- [ ] T065 [US8] 实现 `VideoController`(`POST /api/video/process`、`GET /api/video/jobs/{jobId}`、`POST /api/video/jobs/{jobId}/reset`、`POST /api/video/callback`(内部接口,IP 白名单或服务密钥保护))— `src/main/java/com/label/module/video/controller/VideoController.java`
|
|
||||||
- [ ] T066 [US8] 实现 `SysConfigService.get(configKey)`(先按 `(companyId, key)` 查;未命中按 `(NULL, key)` 查全局默认)和 `update(key, value)`(UPSERT:公司级配置不存在则创建,存在则覆盖)— `src/main/java/com/label/module/config/service/SysConfigService.java`
|
|
||||||
- [ ] T067 [US8] 实现 `SysConfigController`(`GET /api/config`(合并公司级 + 全局,标注 scope)、`PUT /api/config/{key}`;均 `@RequiresRoles("ADMIN")`)— `src/main/java/com/label/module/config/controller/SysConfigController.java`
|
|
||||||
- [ ] T068 [US8] 集成测试:同一 jobId 两次成功回调,`annotation_task` 记录数为 1(幂等);达最大重试次数后 status = FAILED — `src/test/java/com/label/integration/VideoCallbackIdempotencyTest.java`
|
|
||||||
- [ ] T069 [US8] 集成测试:公司级配置覆盖同 Key 的全局默认;其他公司读取全局默认 — `src/test/java/com/label/integration/SysConfigIntegrationTest.java`
|
|
||||||
|
|
||||||
**检查点**: US8 独立可测试 — 视频处理幂等和配置管理可用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Phase 10: 收尾与横切关注点
|
|
||||||
|
|
||||||
**目标**: 多租户隔离验证、整体合规检查、快速启动验证
|
|
||||||
|
|
||||||
- [ ] T070 集成测试:`MultiTenantIsolationTest`(公司 A 身份查询公司 B 的资料/任务 → 返回空列表或 404,不泄露数据)— `src/test/java/com/label/integration/MultiTenantIsolationTest.java`
|
|
||||||
- [ ] T071 [P] 代码审查:检查所有 Controller 方法返回值均为 `Result<T>` 或 `Result<PageResult<T>>`,无裸 POJO 或裸 List 返回
|
|
||||||
- [ ] T072 [P] 代码审查:检查所有列表查询方法均含分页参数(`page`/`pageSize`),无 `selectAll()` 或不分页的查询
|
|
||||||
- [ ] T073 [P] 代码审查:检查 `sys_operation_log` 相关代码,确认应用层零处 UPDATE 或 DELETE
|
|
||||||
- [ ] T074 [P] 代码审查:检查所有 `@Transactional` 方法内无 `AiServiceClient` 的同步 HTTP 调用(审批触发 AI 必须通过 `@TransactionalEventListener`)
|
|
||||||
- [ ] T075 运行 `quickstart.md` 端到端验证:`docker compose up -d` → 登录 → 上传文件 → 创建任务 → 领取 → 提交 → 审批通过 → 确认 QA 任务出现
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 依赖关系与执行顺序
|
|
||||||
|
|
||||||
### 阶段依赖
|
|
||||||
|
|
||||||
```
|
|
||||||
Phase 1(初始化)
|
|
||||||
↓
|
|
||||||
Phase 2(基础设施)[全部完成后解锁所有用户故事]
|
|
||||||
↓
|
|
||||||
Phase 3(US1 认证) ← 可与 Phase 4/5/6/7/8/9 并行
|
|
||||||
Phase 4(US2 上传) ← 依赖 Phase 2,独立于其他用户故事
|
|
||||||
Phase 5(US3+4 提取) ← 依赖 Phase 2(上传已有资料的集成测试依赖 US2)
|
|
||||||
Phase 6(US5 QA) ← 依赖 Phase 5 完成(QA 任务由提取审批自动创建)
|
|
||||||
Phase 7(US6 导出) ← 依赖 Phase 6 完成(需要 APPROVED 的 training_dataset)
|
|
||||||
Phase 8(US7 用户管理) ← 依赖 Phase 3(UserService 在 AuthService 基础上扩展)
|
|
||||||
Phase 9(US8 视频+配置) ← 依赖 Phase 2,其余独立
|
|
||||||
↓
|
|
||||||
Phase 10(收尾)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 用户故事间依赖
|
|
||||||
|
|
||||||
- **US1(认证)**: 仅依赖 Phase 2,完全独立
|
|
||||||
- **US2(上传)**: 仅依赖 Phase 2,完全独立
|
|
||||||
- **US3+4(提取)**: 依赖 Phase 2;集成测试中使用已上传资料需 US2
|
|
||||||
- **US5(QA)**: 依赖 US3+4(QA 任务来源于提取阶段审批通过的级联触发)
|
|
||||||
- **US6(导出)**: 依赖 US5(需要 APPROVED 状态的 training_dataset)
|
|
||||||
- **US7(用户管理)**: 依赖 US1(UserService 扩展 AuthService 的用户实体)
|
|
||||||
- **US8(视频+配置)**: 仅依赖 Phase 2
|
|
||||||
|
|
||||||
### 阶段内并行机会
|
|
||||||
|
|
||||||
- Phase 2:T007-T010、T012-T015、T018-T019 均可并行(独立文件)
|
|
||||||
- Phase 3:T024、T025 可并行(独立文件)
|
|
||||||
- Phase 5:T033、T034、T035 可并行(独立文件)
|
|
||||||
- Phase 9:T062、T063 可并行(独立文件)
|
|
||||||
- Phase 10:T071-T074 全部可并行(仅代码审查,无文件修改)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 并行执行示例
|
|
||||||
|
|
||||||
### Phase 2 基础设施并行
|
|
||||||
|
|
||||||
```
|
|
||||||
同时启动:
|
|
||||||
任务: "创建 BusinessException、GlobalExceptionHandler — common/exception/" [T007]
|
|
||||||
任务: "创建 CompanyContext(ThreadLocal)— common/context/" [T008]
|
|
||||||
任务: "创建 RustFsClient — common/storage/" [T018]
|
|
||||||
任务: "创建 AiServiceClient — common/ai/" [T019]
|
|
||||||
任务: "创建 SourceStatus 枚举" [T012]
|
|
||||||
任务: "创建 TaskStatus 枚举" [T013]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Phase 5 提取阶段并行
|
|
||||||
|
|
||||||
```
|
|
||||||
同时启动(实体/Mapper):
|
|
||||||
任务: "创建 AnnotationTask 实体 + Mapper" [T033]
|
|
||||||
任务: "创建 AnnotationTaskHistory 实体 + Mapper" [T034]
|
|
||||||
任务: "创建 AnnotationResult 实体 + Mapper" [T035]
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 实施策略
|
|
||||||
|
|
||||||
### MVP 优先(仅用户故事 1)
|
|
||||||
|
|
||||||
1. 完成 Phase 1(初始化)
|
|
||||||
2. 完成 Phase 2(基础设施)— **关键,阻塞所有故事**
|
|
||||||
3. 完成 Phase 3(US1 认证)
|
|
||||||
4. **停止并验证**: 登录/退出/权限校验全流程可用
|
|
||||||
5. 可以独立部署演示认证功能
|
|
||||||
|
|
||||||
### 增量交付
|
|
||||||
|
|
||||||
1. Phase 1 + Phase 2 → 基础就绪
|
|
||||||
2. Phase 3(US1)→ 验证 → 演示(MVP)
|
|
||||||
3. Phase 4(US2)→ 验证 → 演示(上传功能)
|
|
||||||
4. Phase 5(US3+4)→ 验证 → 演示(标注流程)
|
|
||||||
5. Phase 6(US5)→ 验证 → 演示(完整双阶段流水线)
|
|
||||||
6. Phase 7(US6)→ 验证 → 演示(训练数据产出)
|
|
||||||
7. Phase 8+9 → 验证 → 演示(完整平台)
|
|
||||||
8. Phase 10 → 收尾
|
|
||||||
|
|
||||||
### 多人协作策略
|
|
||||||
|
|
||||||
Phase 2 完成后:
|
|
||||||
- 开发者 A:Phase 3(US1 认证)+ Phase 8(US7 用户管理)
|
|
||||||
- 开发者 B:Phase 4(US2 上传)+ Phase 5(US3+4 提取)
|
|
||||||
- 开发者 C:Phase 9(US8 视频+配置)
|
|
||||||
|
|
||||||
Phase 5 完成后:
|
|
||||||
- 开发者 A/B 合力:Phase 6(US5 QA)→ Phase 7(US6 导出)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 说明
|
|
||||||
|
|
||||||
- `[P]` 任务 = 不同文件,无依赖,可并行
|
|
||||||
- `[USn]` 标签将任务映射到具体用户故事,便于追踪
|
|
||||||
- 每个用户故事应独立可完成和可测试
|
|
||||||
- 每完成一个阶段后提交 git commit
|
|
||||||
- 在每个检查点停下来独立验证该用户故事
|
|
||||||
- 避免:模糊任务、同文件并发冲突、破坏独立性的跨故事依赖
|
|
||||||
Reference in New Issue
Block a user