Files
label_backend/docs/superpowers/plans/2026-04-15-label-backend-swagger-dto-annotations.md

24 KiB
Raw Blame History

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/RawJsonUpdateRequest.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.

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

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.

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,
        RawJsonUpdateRequest.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:

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

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:

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

@Data
@Schema(description = "更新公司状态请求")
public class CompanyStatusUpdateRequest {
    @Schema(description = "公司状态可选值ACTIVE、DISABLED", example = "ACTIVE")
    private String status;
}
  • Step 2: Add user request DTOs

Create:

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

@Schema(description = "真实姓名", example = "张三")
private String realName;

@Schema(description = "新密码;为空时不修改密码", example = "NewPassw0rd!")
private String password;

UserStatusUpdateRequest:

@Schema(description = "用户状态可选值ACTIVE、DISABLED", example = "ACTIVE")
private String status;

UserRoleUpdateRequest:

@Schema(description = "用户角色可选值ADMIN、REVIEWER、ANNOTATOR、UPLOADER", example = "REVIEWER")
private String role;
  • Step 3: Add task request DTOs

Create:

@Data
@Schema(description = "创建标注任务请求")
public class CreateTaskRequest {
    @Schema(description = "关联资料 ID", example = "1001")
    private Long sourceId;

    @Schema(description = "任务类型可选值EXTRACTION、QA_GENERATION", example = "EXTRACTION")
    private String taskType;
}
@Data
@Schema(description = "管理员强制指派任务请求")
public class TaskReassignRequest {
    @Schema(description = "目标用户 ID", example = "2001")
    private Long userId;
}
  • Step 4: Add common moderation and dynamic JSON DTOs

Create:

@Data
@Schema(description = "审批驳回请求")
public class RejectRequest {
    @Schema(description = "驳回原因", example = "证据不足,请补充来源片段")
    private String reason;
}
@Data
@Schema(description = "原始 JSON 更新请求")
public class RawJsonUpdateRequest {
    @Schema(description = "完整 JSON 字符串;用于整体覆盖提取或问答标注结果", example = "{\"items\":[]}")
    private String body;
}
@Data
@Schema(description = "动态业务 JSON 响应")
public class DynamicJsonResponse {
    @Schema(description = "动态业务数据;提取阶段通常为三元组/四元组结构,问答阶段通常为问答对结构")
    private Map<String, Object> content;
}
  • Step 5: Add export and finetune DTOs

Create:

@Data
@Schema(description = "创建训练数据导出批次请求")
public class ExportBatchCreateRequest {
    @Schema(description = "训练样本 ID 列表", example = "[1,2,3]")
    private List<Long> sampleIds;
}
@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:

@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;
}
@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;
}
@Data
@Schema(description = "系统配置更新请求")
public class SysConfigUpdateRequest {
    @Schema(description = "配置值", example = "glm-4-flash")
    private String value;

    @Schema(description = "配置说明", example = "默认文本模型")
    private String description;
}
@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:

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

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

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:

public Result<SysCompany> create(@RequestBody Map<String, String> body)

with:

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:

taskService.createTask(body.getSourceId(), body.getTaskType(), principal.getCompanyId())

reassign target becomes:

Long targetUserId = body.getUserId();
  • Step 4: Replace export batch request body

In ExportController#createBatch, use ExportBatchCreateRequest.

Service call becomes:

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:

String reason = body != null ? body.getReason() : null;
  • Step 8: Run raw Map body test

Run:

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:

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:

@Parameter(description = "页码,从 1 开始", example = "1")
@RequestParam(defaultValue = "1") int page

For every pageSize parameter:

@Parameter(description = "每页条数", example = "20")
@RequestParam(defaultValue = "20") int pageSize
  • Step 3: Annotate status and type filters

Examples:

@Parameter(description = "任务状态过滤可选值UNCLAIMED、IN_PROGRESS、SUBMITTED、APPROVED、REJECTED", example = "SUBMITTED")
@RequestParam(required = false) String status
@Parameter(description = "任务类型过滤可选值EXTRACTION、QA_GENERATION", example = "EXTRACTION")
@RequestParam(required = false) String taskType
  • Step 4: Annotate path variables

Examples:

@Parameter(description = "任务 ID", example = "1001")
@PathVariable Long id
@Parameter(description = "视频处理任务 ID", example = "123")
@PathVariable Long jobId
@Parameter(description = "系统配置键", example = "ai.defaultTextModel")
@PathVariable String key
  • Step 5: Annotate multipart upload parameters

In SourceController#upload:

@Parameter(description = "上传文件,支持文本、图片、视频", required = true)
@RequestParam("file") MultipartFile file
@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:

@io.swagger.v3.oas.annotations.parameters.RequestBody(
        description = "创建任务请求体",
        required = true)
@RequestBody CreateTaskRequest body
  • Step 7: Run parameter coverage test

Run:

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:

Result<List<Map<String, Object>>>

to:

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:

DynamicJsonResponse response = new DynamicJsonResponse();
response.setContent(extractionService.getResult(taskId, principal(request)));
return Result.success(response);

Use the same pattern for QA.

  • Step 3: Use RawJsonUpdateRequest for raw JSON update bodies only if existing clients can send {"body":"..."}

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:

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:

mvn -Dtest=OpenApiAnnotationTest test

Expected: PASS.

  • Step 2: Run API flows affected by DTO request body replacement

Run:

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:

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:

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:

mvn -Dtest=OpenApiAnnotationTest test

Expected: PASS.

  • Step 3: Review git diff

Run:

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: Commit implementation

Run:

git add src/main/java src/test/java README.md docs/superpowers
git commit -m "docs: document backend api parameters in swagger"

Expected: commit succeeds.