Files
label_backend/docs/superpowers/plans/2026-04-15-label-backend-swagger-dto-annotations.md
2026-04-15 15:28:11 +08:00

809 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# label_backend Swagger DTO Annotation 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:** Make every public `label_backend` API parameter visible in Swagger with its name, type, requiredness, and meaning by replacing fixed `Map` request bodies with DTOs and adding OpenAPI annotations.
**Architecture:** Keep existing URLs, HTTP methods, JSON field names, service calls, and auth behavior unchanged. Add DTOs in the flat `com.label.dto` package, annotate Controller parameters and public schema models, and extend OpenAPI reflection tests so future endpoints cannot regress to undocumented parameters.
**Tech Stack:** Java 21, Spring Boot 3.1.5, springdoc-openapi 2.3.0, JUnit 5, AssertJ, Lombok.
---
## File Structure
### Create
- `src/main/java/com/label/dto/CompanyCreateRequest.java`
- `src/main/java/com/label/dto/CompanyUpdateRequest.java`
- `src/main/java/com/label/dto/CompanyStatusUpdateRequest.java`
- `src/main/java/com/label/dto/UserCreateRequest.java`
- `src/main/java/com/label/dto/UserUpdateRequest.java`
- `src/main/java/com/label/dto/UserStatusUpdateRequest.java`
- `src/main/java/com/label/dto/UserRoleUpdateRequest.java`
- `src/main/java/com/label/dto/CreateTaskRequest.java`
- `src/main/java/com/label/dto/TaskReassignRequest.java`
- `src/main/java/com/label/dto/RejectRequest.java`
- `src/main/java/com/label/dto/ExportBatchCreateRequest.java`
- `src/main/java/com/label/dto/VideoProcessCreateRequest.java`
- `src/main/java/com/label/dto/VideoProcessCallbackRequest.java`
- `src/main/java/com/label/dto/SysConfigUpdateRequest.java`
- `src/main/java/com/label/dto/SysConfigItemResponse.java`
- `src/main/java/com/label/dto/DynamicJsonResponse.java`
- `src/main/java/com/label/dto/FinetuneJobResponse.java`
### Modify
- `src/test/java/com/label/unit/OpenApiAnnotationTest.java`
- `src/main/java/com/label/common/result/Result.java`
- `src/main/java/com/label/common/result/PageResult.java`
- `src/main/java/com/label/dto/LoginRequest.java`
- `src/main/java/com/label/dto/LoginResponse.java`
- `src/main/java/com/label/dto/SourceResponse.java`
- `src/main/java/com/label/dto/TaskResponse.java`
- `src/main/java/com/label/dto/UserInfoResponse.java`
- `src/main/java/com/label/controller/AuthController.java`
- `src/main/java/com/label/controller/CompanyController.java`
- `src/main/java/com/label/controller/UserController.java`
- `src/main/java/com/label/controller/SourceController.java`
- `src/main/java/com/label/controller/TaskController.java`
- `src/main/java/com/label/controller/ExtractionController.java`
- `src/main/java/com/label/controller/QaController.java`
- `src/main/java/com/label/controller/ExportController.java`
- `src/main/java/com/label/controller/SysConfigController.java`
- `src/main/java/com/label/controller/VideoController.java`
- `src/main/java/com/label/entity/SysCompany.java`
- `src/main/java/com/label/entity/SysUser.java`
- `src/main/java/com/label/entity/SysConfig.java`
- `src/main/java/com/label/entity/TrainingDataset.java`
- `src/main/java/com/label/entity/ExportBatch.java`
- `src/main/java/com/label/entity/VideoProcessJob.java`
---
### Task 1: Add Failing OpenAPI Contract Tests
**Files:**
- Modify: `src/test/java/com/label/unit/OpenApiAnnotationTest.java`
- [ ] **Step 1: Add DTO and parameter coverage expectations**
Add tests that fail before implementation because new DTO classes do not exist, some endpoint parameters lack `@Parameter`, and request body parameters still use `Map`.
```java
@Test
@DisplayName("固定结构请求体不能继续使用 Map")
void fixedRequestBodiesDoNotUseRawMap() {
for (Class<?> controller : CONTROLLERS) {
Arrays.stream(controller.getDeclaredMethods())
.filter(OpenApiAnnotationTest::isEndpointMethod)
.flatMap(method -> Arrays.stream(method.getParameters()))
.filter(parameter -> parameter.isAnnotationPresent(org.springframework.web.bind.annotation.RequestBody.class))
.forEach(parameter -> assertThat(parameter.getType())
.as(controller.getSimpleName() + " request body should use an explicit DTO")
.isNotEqualTo(java.util.Map.class));
}
}
@Test
@DisplayName("所有 endpoint 公开参数都声明 @Parameter 或请求体说明")
void endpointParametersHaveOpenApiDescriptions() {
for (Class<?> controller : CONTROLLERS) {
Arrays.stream(controller.getDeclaredMethods())
.filter(OpenApiAnnotationTest::isEndpointMethod)
.forEach(method -> Arrays.stream(method.getParameters())
.filter(OpenApiAnnotationTest::isPublicApiParameter)
.forEach(parameter -> assertThat(
parameter.isAnnotationPresent(io.swagger.v3.oas.annotations.Parameter.class)
|| parameter.isAnnotationPresent(io.swagger.v3.oas.annotations.parameters.RequestBody.class))
.as(controller.getSimpleName() + "." + method.getName()
+ " parameter " + parameter.getName()
+ " should have OpenAPI description")
.isTrue()));
}
}
```
Add helper:
```java
private static boolean isPublicApiParameter(java.lang.reflect.Parameter parameter) {
return parameter.isAnnotationPresent(org.springframework.web.bind.annotation.PathVariable.class)
|| parameter.isAnnotationPresent(org.springframework.web.bind.annotation.RequestParam.class)
|| parameter.isAnnotationPresent(org.springframework.web.bind.annotation.RequestBody.class);
}
```
- [ ] **Step 2: Expand DTO and entity schema coverage**
Update DTO coverage lists so the test will fail until new DTOs and exposed entities are annotated.
```java
private static final List<Class<?>> DTOS = List.of(
LoginRequest.class,
LoginResponse.class,
UserInfoResponse.class,
TaskResponse.class,
SourceResponse.class,
CompanyCreateRequest.class,
CompanyUpdateRequest.class,
CompanyStatusUpdateRequest.class,
UserCreateRequest.class,
UserUpdateRequest.class,
UserStatusUpdateRequest.class,
UserRoleUpdateRequest.class,
CreateTaskRequest.class,
TaskReassignRequest.class,
RejectRequest.class,
ExportBatchCreateRequest.class,
VideoProcessCreateRequest.class,
VideoProcessCallbackRequest.class,
SysConfigUpdateRequest.class,
SysConfigItemResponse.class,
DynamicJsonResponse.class,
FinetuneJobResponse.class
);
private static final List<Class<?>> EXPOSED_SCHEMAS = List.of(
Result.class,
PageResult.class,
SysCompany.class,
SysUser.class,
SysConfig.class,
TrainingDataset.class,
ExportBatch.class,
VideoProcessJob.class
);
```
Add test:
```java
@Test
@DisplayName("公开响应模型都声明 @Schema")
void exposedModelsHaveSchema() {
assertThat(EXPOSED_SCHEMAS)
.allSatisfy(model ->
assertThat(model.getAnnotation(Schema.class))
.as(model.getSimpleName() + " should have @Schema")
.isNotNull());
}
```
- [ ] **Step 3: Run test and verify RED**
Run:
```bash
mvn -Dtest=OpenApiAnnotationTest test
```
Expected: FAIL because the new DTO classes are missing and existing Controller parameters are not fully annotated.
### Task 2: Create Explicit Request and Response DTOs
**Files:**
- Create all DTO files listed in File Structure
- [ ] **Step 1: Add company request DTOs**
Create `CompanyCreateRequest`, `CompanyUpdateRequest`, and `CompanyStatusUpdateRequest` with these fields:
```java
@Data
@Schema(description = "创建公司请求")
public class CompanyCreateRequest {
@Schema(description = "公司名称", example = "示例公司")
private String companyName;
@Schema(description = "公司代码,英文或数字短标识", example = "DEMO")
private String companyCode;
}
```
`CompanyUpdateRequest` uses the same fields with description "更新公司信息请求".
`CompanyStatusUpdateRequest`:
```java
@Data
@Schema(description = "更新公司状态请求")
public class CompanyStatusUpdateRequest {
@Schema(description = "公司状态可选值ACTIVE、DISABLED", example = "ACTIVE")
private String status;
}
```
- [ ] **Step 2: Add user request DTOs**
Create:
```java
@Data
@Schema(description = "创建用户请求")
public class UserCreateRequest {
@Schema(description = "登录用户名", example = "annotator01")
private String username;
@Schema(description = "明文初始密码", example = "Passw0rd!")
private String password;
@Schema(description = "真实姓名", example = "张三")
private String realName;
@Schema(description = "用户角色可选值ADMIN、REVIEWER、ANNOTATOR、UPLOADER", example = "ANNOTATOR")
private String role;
}
```
`UserUpdateRequest` fields:
```java
@Schema(description = "真实姓名", example = "张三")
private String realName;
@Schema(description = "新密码;为空时不修改密码", example = "NewPassw0rd!")
private String password;
```
`UserStatusUpdateRequest`:
```java
@Schema(description = "用户状态可选值ACTIVE、DISABLED", example = "ACTIVE")
private String status;
```
`UserRoleUpdateRequest`:
```java
@Schema(description = "用户角色可选值ADMIN、REVIEWER、ANNOTATOR、UPLOADER", example = "REVIEWER")
private String role;
```
- [ ] **Step 3: Add task request DTOs**
Create:
```java
@Data
@Schema(description = "创建标注任务请求")
public class CreateTaskRequest {
@Schema(description = "关联资料 ID", example = "1001")
private Long sourceId;
@Schema(description = "任务类型可选值EXTRACTION、QA_GENERATION", example = "EXTRACTION")
private String taskType;
}
```
```java
@Data
@Schema(description = "管理员强制指派任务请求")
public class TaskReassignRequest {
@Schema(description = "目标用户 ID", example = "2001")
private Long userId;
}
```
- [ ] **Step 4: Add common moderation and dynamic JSON DTOs**
Create:
```java
@Data
@Schema(description = "审批驳回请求")
public class RejectRequest {
@Schema(description = "驳回原因", example = "证据不足,请补充来源片段")
private String reason;
}
```
```java
@Data
@Data
@Schema(description = "动态业务 JSON 响应")
public class DynamicJsonResponse {
@Schema(description = "动态业务数据;提取阶段通常为三元组/四元组结构,问答阶段通常为问答对结构")
private Map<String, Object> content;
}
```
- [ ] **Step 5: Add export and finetune DTOs**
Create:
```java
@Data
@Schema(description = "创建训练数据导出批次请求")
public class ExportBatchCreateRequest {
@Schema(description = "训练样本 ID 列表", example = "[1,2,3]")
private List<Long> sampleIds;
}
```
```java
@Data
@Schema(description = "微调任务响应")
public class FinetuneJobResponse {
@Schema(description = "导出批次 ID", example = "10")
private Long batchId;
@Schema(description = "微调任务 ID", example = "glm-ft-abc123")
private String finetuneJobId;
@Schema(description = "微调任务状态可选值PENDING、RUNNING、SUCCESS、FAILED", example = "RUNNING")
private String status;
@Schema(description = "错误信息;任务失败时返回", example = "training file not found")
private String errorMessage;
}
```
- [ ] **Step 6: Add video and config DTOs**
Create:
```java
@Data
@Schema(description = "创建视频处理任务请求")
public class VideoProcessCreateRequest {
@Schema(description = "视频资料 ID", example = "1001")
private Long sourceId;
@Schema(description = "任务类型可选值EXTRACT_FRAMES、VIDEO_TO_TEXT", example = "EXTRACT_FRAMES")
private String jobType;
@Schema(description = "任务参数 JSON 字符串,例如抽帧模式、帧间隔或起止时间", example = "{\"mode\":\"interval\",\"frameInterval\":30}")
private String params;
}
```
```java
@Data
@Schema(description = "AI 服务视频处理回调请求")
public class VideoProcessCallbackRequest {
@Schema(description = "视频处理任务 ID", example = "123")
private Long jobId;
@Schema(description = "处理状态可选值SUCCESS、FAILED", example = "SUCCESS")
private String status;
@Schema(description = "输出文件路径;成功时返回", example = "frames/1001/0.jpg")
private String outputPath;
@Schema(description = "错误信息;失败时返回", example = "video decode failed")
private String errorMessage;
}
```
```java
@Data
@Schema(description = "系统配置更新请求")
public class SysConfigUpdateRequest {
@Schema(description = "配置值", example = "glm-4-flash")
private String value;
@Schema(description = "配置说明", example = "默认文本模型")
private String description;
}
```
```java
@Data
@Schema(description = "系统配置项响应")
public class SysConfigItemResponse {
@Schema(description = "配置键", example = "ai.defaultTextModel")
private String configKey;
@Schema(description = "配置值", example = "glm-4-flash")
private String configValue;
@Schema(description = "配置说明", example = "默认文本模型")
private String description;
@Schema(description = "配置作用域可选值GLOBAL、COMPANY", example = "COMPANY")
private String scope;
}
```
- [ ] **Step 7: Run test and verify partial GREEN**
Run:
```bash
mvn -Dtest=OpenApiAnnotationTest test
```
Expected: FAIL remains, but missing class errors are gone. Failures should now point to missing Controller parameter annotations and raw `Map` request bodies.
### Task 3: Annotate Common Models, Existing DTOs, and Exposed Entities
**Files:**
- Modify common result classes, existing DTOs, and exposed entity files listed above
- [ ] **Step 1: Add `@Schema` to `Result<T>` and `PageResult<T>`**
Update `Result<T>`:
```java
@Data
@Schema(description = "统一接口响应包装")
public class Result<T> {
@Schema(description = "业务状态码;成功为 SUCCESS失败为具体错误码", example = "SUCCESS")
private String code;
@Schema(description = "接口返回主体;不同接口类型不同")
private T data;
@Schema(description = "响应消息;失败时说明错误原因", example = "参数不合法")
private String message;
}
```
Update `PageResult<T>` similarly for `items`, `total`, `page`, `pageSize`.
- [ ] **Step 2: Complete existing DTO field descriptions**
Ensure every field in `LoginRequest`, `LoginResponse`, `UserInfoResponse`, `TaskResponse`, and `SourceResponse` has `@Schema(description = ..., example = ...)`.
- [ ] **Step 3: Add class and field schemas to exposed entities**
For each exposed entity, add class-level `@Schema(description = "...")` and field-level `@Schema`.
Use these class descriptions:
- `SysCompany`: "租户公司"
- `SysUser`: "系统用户"
- `SysConfig`: "系统配置"
- `TrainingDataset`: "训练样本数据"
- `ExportBatch`: "训练数据导出批次"
- `VideoProcessJob`: "视频处理任务"
- [ ] **Step 4: Run schema coverage test**
Run:
```bash
mvn -Dtest=OpenApiAnnotationTest test
```
Expected: raw `Map` body and parameter annotation tests still fail, schema coverage should pass.
### Task 4: Refactor Controller Request Bodies to DTOs
**Files:**
- Modify Controller files listed in File Structure
- [ ] **Step 1: Replace company `Map` request bodies**
In `CompanyController`, replace:
```java
public Result<SysCompany> create(@RequestBody Map<String, String> body)
```
with:
```java
public Result<SysCompany> create(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "创建公司请求体",
required = true)
@RequestBody CompanyCreateRequest body)
```
Call service with `body.getCompanyName()` and `body.getCompanyCode()`.
Apply the same pattern to `update` and `updateStatus`.
- [ ] **Step 2: Replace user `Map` request bodies**
In `UserController`, replace create/update/status/role request bodies with `UserCreateRequest`, `UserUpdateRequest`, `UserStatusUpdateRequest`, and `UserRoleUpdateRequest`.
Keep service calls unchanged except changing `body.get("field")` to DTO getters.
- [ ] **Step 3: Replace task request bodies**
In `TaskController`, use `CreateTaskRequest` and `TaskReassignRequest`.
`createTask` service call becomes:
```java
taskService.createTask(body.getSourceId(), body.getTaskType(), principal.getCompanyId())
```
`reassign` target becomes:
```java
Long targetUserId = body.getUserId();
```
- [ ] **Step 4: Replace export batch request body**
In `ExportController#createBatch`, use `ExportBatchCreateRequest`.
Service call becomes:
```java
return Result.success(exportService.createBatch(body.getSampleIds(), principal(request)));
```
- [ ] **Step 5: Replace config update request body**
In `SysConfigController#updateConfig`, use `SysConfigUpdateRequest`.
Service call uses `body.getValue()` and `body.getDescription()`.
- [ ] **Step 6: Replace video request bodies**
In `VideoController#createJob`, use `VideoProcessCreateRequest`.
In `VideoController#handleCallback`, use `VideoProcessCallbackRequest`.
Keep callback secret header logic unchanged.
- [ ] **Step 7: Replace reject request bodies**
In `ExtractionController#reject` and `QaController#reject`, use `RejectRequest`.
Set:
```java
String reason = body != null ? body.getReason() : null;
```
- [ ] **Step 8: Run raw Map body test**
Run:
```bash
mvn -Dtest=OpenApiAnnotationTest test
```
Expected: raw `Map` request-body test passes. Parameter description test may still fail until Task 5.
### Task 5: Add Controller Parameter and Request Body OpenAPI Descriptions
**Files:**
- Modify all Controller files
- [ ] **Step 1: Add imports**
Use:
```java
import io.swagger.v3.oas.annotations.Parameter;
```
Use fully qualified `io.swagger.v3.oas.annotations.parameters.RequestBody` for Swagger request body annotations to avoid conflict with Spring `@RequestBody`.
- [ ] **Step 2: Annotate pagination query parameters**
For every `page` parameter:
```java
@Parameter(description = "页码,从 1 开始", example = "1")
@RequestParam(defaultValue = "1") int page
```
For every `pageSize` parameter:
```java
@Parameter(description = "每页条数", example = "20")
@RequestParam(defaultValue = "20") int pageSize
```
- [ ] **Step 3: Annotate status and type filters**
Examples:
```java
@Parameter(description = "任务状态过滤可选值UNCLAIMED、IN_PROGRESS、SUBMITTED、APPROVED、REJECTED", example = "SUBMITTED")
@RequestParam(required = false) String status
```
```java
@Parameter(description = "任务类型过滤可选值EXTRACTION、QA_GENERATION", example = "EXTRACTION")
@RequestParam(required = false) String taskType
```
- [ ] **Step 4: Annotate path variables**
Examples:
```java
@Parameter(description = "任务 ID", example = "1001")
@PathVariable Long id
```
```java
@Parameter(description = "视频处理任务 ID", example = "123")
@PathVariable Long jobId
```
```java
@Parameter(description = "系统配置键", example = "ai.defaultTextModel")
@PathVariable String key
```
- [ ] **Step 5: Annotate multipart upload parameters**
In `SourceController#upload`:
```java
@Parameter(description = "上传文件,支持文本、图片、视频", required = true)
@RequestParam("file") MultipartFile file
```
```java
@Parameter(description = "资料类型可选值text、image、video", example = "text", required = true)
@RequestParam("dataType") String dataType
```
- [ ] **Step 6: Annotate request bodies**
Every Spring `@RequestBody` parameter should also have Swagger request-body annotation:
```java
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "创建任务请求体",
required = true)
@RequestBody CreateTaskRequest body
```
- [ ] **Step 7: Run parameter coverage test**
Run:
```bash
mvn -Dtest=OpenApiAnnotationTest test
```
Expected: PASS for `OpenApiAnnotationTest`.
### Task 6: Make Fixed Map Responses Swagger-Readable
**Files:**
- Modify `ExportController.java`
- Modify `SysConfigController.java`
- Modify `ExtractionController.java`
- Modify `QaController.java`
- Modify `FinetuneService.java` only if needed to map stable fields cleanly
- [ ] **Step 1: Convert config list response**
Change `SysConfigController#listConfig` return type from:
```java
Result<List<Map<String, Object>>>
```
to:
```java
Result<List<SysConfigItemResponse>>
```
Map each item from service map using keys currently returned by `SysConfigService.list`.
- [ ] **Step 2: Wrap dynamic extraction and QA responses**
Change `ExtractionController#getResult` and `QaController#getResult` return types from `Result<Map<String, Object>>` to `Result<DynamicJsonResponse>`.
Implementation:
```java
DynamicJsonResponse response = new DynamicJsonResponse();
response.setContent(extractionService.getResult(taskId, principal(request)));
return Result.success(response);
```
Use the same pattern for QA.
- [ ] **Step 3: Keep raw JSON update request bodies as raw strings**
Do not change `PUT /api/extraction/{taskId}` or `PUT /api/qa/{taskId}` from raw string request body unless compatibility is explicitly approved.
Instead, annotate the raw string body with a Swagger request-body description explaining it is a complete JSON string.
- [ ] **Step 4: Convert finetune responses only if fields are stable**
Inspect `FinetuneService#trigger` and `FinetuneService#getStatus`.
If returned maps have stable keys, convert Controller responses to `FinetuneJobResponse` by mapping the known keys.
If keys are not stable, keep `Map<String,Object>` response but document `@Operation(description = "...")` and do not convert response type.
- [ ] **Step 5: Run compile test**
Run:
```bash
mvn -DskipTests compile
```
Expected: PASS.
### Task 7: Run Focused Compatibility Tests
**Files:**
- Modify integration tests only if DTO request binding requires updating test payload helpers
- [ ] **Step 1: Run OpenAPI annotation test**
Run:
```bash
mvn -Dtest=OpenApiAnnotationTest test
```
Expected: PASS.
- [ ] **Step 2: Run API flows affected by DTO request body replacement**
Run:
```bash
mvn -Dtest=UserManagementIntegrationTest,SysConfigIntegrationTest,ExportIntegrationTest,VideoCallbackIdempotencyTest test
```
Expected: PASS. If Docker/Testcontainers is unavailable, record the failure reason and run `mvn -DskipTests compile` plus `OpenApiAnnotationTest`.
- [ ] **Step 3: Run full test suite if environment supports Docker**
Run:
```bash
mvn test
```
Expected: PASS, or Docker/Testcontainers-specific skip/failure documented.
### Task 8: Final Documentation and Review
**Files:**
- Modify: `README.md` only if implementation reveals stricter constraints than already documented
- Modify: `docs/superpowers/specs/2026-04-15-label-backend-swagger-annotations-design.md` only if a design decision changes during implementation
- [ ] **Step 1: Check for remaining raw request body maps**
Run:
```bash
rg -n "@RequestBody Map<|Map<String, Object> body|Map<String, String> body" src/main/java/com/label/controller
```
Expected: no fixed-structure request-body maps remain. Dynamic non-request-body maps may remain only where intentionally documented.
- [ ] **Step 2: Check for missing `@Parameter` on public endpoint params**
Run:
```bash
mvn -Dtest=OpenApiAnnotationTest test
```
Expected: PASS.
- [ ] **Step 3: Review git diff**
Run:
```bash
git diff -- src/main/java src/test/java README.md docs/superpowers
```
Expected: diff only contains Swagger annotations, DTO additions, DTO request binding updates, and matching tests.
- [ ] **Step 4: Keep implementation uncommitted for review**
Do not commit unless the user explicitly asks for it.
Run:
```bash
git status --short
```
Expected: implementation remains available in the worktree for review.