停止追踪specs,docs等目录文件

This commit is contained in:
wh
2026-04-14 21:04:37 +08:00
parent 5839bc2ece
commit 8ba3de17ab
23 changed files with 0 additions and 7547 deletions

View File

@@ -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.xmlINFO 级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 环境(检测 /.dockerenvexec 前台运行,保持容器进程存活
# - 裸机 / VMnohup 后台运行,日志追加至 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
# 裸机 / VMnohup 后台运行
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.shDocker 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.sh0755 可执行) -->
<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.info11 文件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.info11 文件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

View File

@@ -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.

View File

@@ -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

View File

@@ -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 会同时失效。
控制策略:
- 按阶段迁移并同步修复引用
- 每个阶段结束后至少执行一次编译检查
### 风险 2Spring 扫描路径异常
若配置类、切面、监听器或 Mapper 的扫描路径与旧包结构耦合,迁移后可能导致 Bean 缺失。
控制策略:
- 显式检查启动类与配置类中的扫描配置
- 将路径校正作为独立阶段处理
### 风险 3事件监听失效
监听器从 `service` 拆到 `listener` 后,若注解或组件扫描不正确,会导致事件未被消费。
控制策略:
- 迁移时同步校验事件类与监听器依赖关系
- 通过现有集成测试覆盖事件链路
### 风险 4测试回归失败
测试代码同样引用旧包名,若只改主代码,会造成测试集整体失效。
控制策略:
- 测试代码与主代码同步迁移
-`mvn test` 作为最终验收门槛
---
## 8. 验收标准
本次结构调整完成后,应满足以下标准:
1. `src/main/java/com/label/module` 目录被完全移除
2. 主代码包结构符合标准扁平目录规范
3. 项目能够成功编译
4. 现有测试能够通过,至少覆盖当前可运行的启动测试、单元测试和集成测试
5. 调整范围内不存在残留旧包引用
---
## 9. 实现原则
执行本设计时应遵循以下原则:
- 先保证结构归位,再追求风格统一
- 先保证行为不变,再做命名优化
- 所有改动以“目录标准化”为中心,不引入额外架构决策
- 每次修改都要让工程更接近规范,而不是制造新的混合结构
---
## 10. 结论
本次改造将 `label_backend` 从“按业务域分层 + 公共包混合”的现状,统一整理为规范文档定义的扁平标准目录结构。该调整不会改变系统能力边界,但会显著提升代码组织一致性、后续开发可预测性以及新成员理解成本。
实施时采用“目标结构一次确定、操作按阶段推进”的方式,以降低大规模包迁移带来的编译与装配风险。

View File

@@ -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 生命周期
- 任务领取并发
- 提取/问答审批状态流转
- 导出样本校验
- 视频回调幂等与重试