From c3308e069dbe0060bcdb39cc5dcf1b95f892e9f0 Mon Sep 17 00:00:00 2001 From: wh Date: Fri, 10 Apr 2026 10:47:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E6=B7=BB=E5=8A=A0swagger?= =?UTF-8?q?=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plans/2026-04-09-swagger-shiro-toggle.md | 1411 +++++++++++++++++ .../label/common/config/OpenApiConfig.java | 33 + 2 files changed, 1444 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-09-swagger-shiro-toggle.md create mode 100644 src/main/java/com/label/common/config/OpenApiConfig.java diff --git a/docs/superpowers/plans/2026-04-09-swagger-shiro-toggle.md b/docs/superpowers/plans/2026-04-09-swagger-shiro-toggle.md new file mode 100644 index 0000000..1cb3f13 --- /dev/null +++ b/docs/superpowers/plans/2026-04-09-swagger-shiro-toggle.md @@ -0,0 +1,1411 @@ +# Swagger + Shiro 认证开关 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:** 为所有 REST 接口添加 springdoc-openapi Swagger 文档注解,并在 application.yml 中提供 Shiro 认证鉴权开关以方便测试。 + +**Architecture:** 通过在 `pom.xml` 引入 `springdoc-openapi-starter-webmvc-ui`,新建 `OpenApiConfig` 配置 Bearer Token 安全方案;修改 `TokenFilter` 支持 Swagger 路径白名单和 `shiro.auth.enabled=false` 时注入 mock Principal;在所有 Controller 上添加 `@Tag`/`@Operation`,核心 DTO 添加 `@Schema`。 + +**Tech Stack:** Spring Boot 3.2.5、Apache Shiro 1.13、springdoc-openapi-starter-webmvc-ui 2.5.0、Lombok、io.swagger.v3.oas.annotations + +--- + +## 文件变更清单 + +| 操作 | 文件路径 | +|------|----------| +| 修改 | `pom.xml` | +| 修改 | `src/main/resources/application.yml` | +| 修改 | `src/main/java/com/label/common/shiro/TokenFilter.java` | +| 新建 | `src/main/java/com/label/common/config/OpenApiConfig.java` | +| 修改 | `src/main/java/com/label/module/user/dto/LoginRequest.java` | +| 修改 | `src/main/java/com/label/module/user/dto/LoginResponse.java` | +| 修改 | `src/main/java/com/label/module/user/dto/UserInfoResponse.java` | +| 修改 | `src/main/java/com/label/module/task/dto/TaskResponse.java` | +| 修改 | `src/main/java/com/label/module/source/dto/SourceResponse.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/source/controller/SourceController.java` | +| 修改 | `src/main/java/com/label/module/task/controller/TaskController.java` | +| 修改 | `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/export/controller/ExportController.java` | +| 修改 | `src/main/java/com/label/module/config/controller/SysConfigController.java` | +| 修改 | `src/main/java/com/label/module/video/controller/VideoController.java` | + +--- + +## Task 1: 依赖与配置(pom.xml + application.yml) + +**Files:** +- Modify: `pom.xml` +- Modify: `src/main/resources/application.yml` + +- [ ] **Step 1: 在 pom.xml `` 末尾添加 springdoc 依赖** + +在 `` 依赖之前插入: + +```xml + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + +``` + +- [ ] **Step 2: 在 application.yml 末尾追加 springdoc 和 shiro.auth 配置** + +在 `logging:` 块之前插入以下内容(保留原 `logging:` 块): + +```yaml +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui.html + +shiro: + auth: + enabled: true + mock-company-id: 1 + mock-user-id: 1 + mock-role: ADMIN + mock-username: mock +``` + +- [ ] **Step 3: 编译验证** + +```bash +cd label_backend +mvn compile -q +``` + +预期:`BUILD SUCCESS`,无编译错误。 + +- [ ] **Step 4: Commit** + +```bash +cd label_backend +git add pom.xml src/main/resources/application.yml +git commit -m "feat: add springdoc-openapi dependency and swagger/shiro-auth config" +``` + +--- + +## Task 2: 修改 TokenFilter(Swagger 白名单 + 认证开关) + +**Files:** +- Modify: `src/main/java/com/label/common/shiro/TokenFilter.java` + +- [ ] **Step 1: 用以下完整内容替换 TokenFilter.java** + +```java +package com.label.common.shiro; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.label.common.context.CompanyContext; +import com.label.common.redis.RedisKeyManager; +import com.label.common.redis.RedisService; +import com.label.common.result.Result; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.util.ThreadContext; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.Map; + +/** + * JWT-style Bearer Token 过滤器。 + * 继承 Spring 的 OncePerRequestFilter(jakarta.servlet),避免与 Shiro 1.x + * 的 PathMatchingFilter(javax.servlet)产生命名空间冲突。 + * + * 过滤逻辑: + * - 跳过非 /api/ 路径、/api/auth/login、Swagger UI 路径(公开端点) + * - shiro.auth.enabled=false 时:注入 mock Principal,跳过 Redis 校验(测试模式) + * - 解析 "Authorization: Bearer {uuid}",查询 Redis Hash token:{uuid} + * - Token 存在 → 注入 CompanyContext,登录 Shiro Subject,继续请求链路 + * - Token 缺失或过期 → 直接返回 401 + * - finally 块中清除 CompanyContext 和 ThreadContext Subject,防止线程池串漏 + */ +@Slf4j +@RequiredArgsConstructor +public class TokenFilter extends OncePerRequestFilter { + + private final RedisService redisService; + private final ObjectMapper objectMapper; + + @Value("${shiro.auth.enabled:true}") + private boolean authEnabled; + + @Value("${shiro.auth.mock-company-id:1}") + private Long mockCompanyId; + + @Value("${shiro.auth.mock-user-id:1}") + private Long mockUserId; + + @Value("${shiro.auth.mock-role:ADMIN}") + private String mockRole; + + @Value("${shiro.auth.mock-username:mock}") + private String mockUsername; + + /** + * 公开端点跳过过滤: + * - 非 /api/ 前缀路径 + * - 登录接口 + * - AI 服务内部回调 + * - Swagger UI / OpenAPI 文档路径 + */ + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getServletPath(); + return !path.startsWith("/api/") + || path.equals("/api/auth/login") + || path.equals("/api/video/callback") + || path.startsWith("/swagger-ui") + || path.startsWith("/v3/api-docs"); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + // 认证开关关闭时:注入固定测试上下文,跳过 Redis 校验 + if (!authEnabled) { + TokenPrincipal mockPrincipal = new TokenPrincipal( + mockUserId, mockRole, mockCompanyId, mockUsername, "mock-token"); + CompanyContext.set(mockCompanyId); + SecurityUtils.getSubject().login(new BearerToken("mock-token", mockPrincipal)); + request.setAttribute("__token_principal__", mockPrincipal); + filterChain.doFilter(request, response); + return; + } + + String authHeader = request.getHeader("Authorization"); + if (authHeader == null || !authHeader.startsWith("Bearer ")) { + writeUnauthorized(response, "缺少或无效的认证令牌"); + return; + } + + String token = authHeader.substring(7).trim(); + Map tokenData = redisService.hGetAll(RedisKeyManager.tokenKey(token)); + + if (tokenData == null || tokenData.isEmpty()) { + writeUnauthorized(response, "令牌已过期或不存在"); + return; + } + + Long userId = Long.parseLong(tokenData.get("userId").toString()); + String role = tokenData.get("role").toString(); + Long companyId = Long.parseLong(tokenData.get("companyId").toString()); + String username = tokenData.get("username").toString(); + + // 注入多租户上下文(finally 中清除,防止线程池串漏) + CompanyContext.set(companyId); + + // 创建 TokenPrincipal 并登录 Shiro Subject,使 @RequiresRoles 等注解生效 + TokenPrincipal principal = new TokenPrincipal(userId, role, companyId, username, token); + SecurityUtils.getSubject().login(new BearerToken(token, principal)); + request.setAttribute("__token_principal__", principal); + + filterChain.doFilter(request, response); + } catch (Exception e) { + log.error("解析 Token 数据失败: {}", e.getMessage()); + writeUnauthorized(response, "令牌数据格式错误"); + } finally { + // 关键:必须清除 ThreadLocal,防止线程池复用时数据串漏 + CompanyContext.clear(); + ThreadContext.unbindSubject(); + } + } + + private void writeUnauthorized(HttpServletResponse resp, String message) throws IOException { + resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + resp.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8"); + resp.getWriter().write(objectMapper.writeValueAsString(Result.failure("UNAUTHORIZED", message))); + } +} +``` + +- [ ] **Step 2: 编译验证** + +```bash +cd label_backend +mvn compile -q +``` + +预期:`BUILD SUCCESS` + +- [ ] **Step 3: Commit** + +```bash +cd label_backend +git add src/main/java/com/label/common/shiro/TokenFilter.java +git commit -m "feat: add swagger path whitelist and shiro.auth.enabled toggle to TokenFilter" +``` + +--- + +## Task 3: 新建 OpenApiConfig + +**Files:** +- Create: `src/main/java/com/label/common/config/OpenApiConfig.java` + +- [ ] **Step 1: 创建 OpenApiConfig.java** + +```java +package com.label.common.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * SpringDoc OpenAPI 全局配置:API 基本信息 + Bearer Token 安全方案。 + */ +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Label Backend API") + .version("1.0.0") + .description("知识图谱智能标注平台后端接口文档")) + .addSecurityItem(new SecurityRequirement().addList("BearerAuth")) + .components(new Components() + .addSecuritySchemes("BearerAuth", + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("UUID") + .description("登录后返回的 Token,格式:Bearer {uuid}"))); + } +} +``` + +- [ ] **Step 2: 编译验证** + +```bash +cd label_backend +mvn compile -q +``` + +预期:`BUILD SUCCESS` + +- [ ] **Step 3: Commit** + +```bash +cd label_backend +git add src/main/java/com/label/common/config/OpenApiConfig.java +git commit -m "feat: add OpenApiConfig with Bearer Token security scheme" +``` + +--- + +## Task 4: 核心 DTO 添加 @Schema 注解 + +**Files:** +- Modify: `src/main/java/com/label/module/user/dto/LoginRequest.java` +- Modify: `src/main/java/com/label/module/user/dto/LoginResponse.java` +- Modify: `src/main/java/com/label/module/user/dto/UserInfoResponse.java` +- Modify: `src/main/java/com/label/module/task/dto/TaskResponse.java` +- Modify: `src/main/java/com/label/module/source/dto/SourceResponse.java` + +- [ ] **Step 1: 替换 LoginRequest.java** + +```java +package com.label.module.user.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 登录请求体。 + */ +@Data +@Schema(description = "登录请求") +public class LoginRequest { + @Schema(description = "公司代码(英文简写),用于确定租户", example = "acme") + private String companyCode; + @Schema(description = "登录用户名", example = "admin") + private String username; + @Schema(description = "明文密码(传输层应使用 HTTPS 保护)", example = "password123") + private String password; +} +``` + +- [ ] **Step 2: 替换 LoginResponse.java** + +```java +package com.label.module.user.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * 登录成功响应体。 + */ +@Data +@AllArgsConstructor +@Schema(description = "登录响应") +public class LoginResponse { + @Schema(description = "Bearer Token(UUID v4),后续请求放入 Authorization 头") + private String token; + @Schema(description = "用户主键") + private Long userId; + @Schema(description = "登录用户名") + private String username; + @Schema(description = "角色:UPLOADER / ANNOTATOR / REVIEWER / ADMIN") + private String role; + @Schema(description = "Token 有效期(秒)") + private Long expiresIn; +} +``` + +- [ ] **Step 3: 替换 UserInfoResponse.java** + +```java +package com.label.module.user.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * GET /api/auth/me 响应体,包含当前登录用户的详细信息。 + */ +@Data +@AllArgsConstructor +@Schema(description = "当前登录用户信息") +public class UserInfoResponse { + @Schema(description = "用户主键") + private Long id; + @Schema(description = "用户名") + private String username; + @Schema(description = "真实姓名") + private String realName; + @Schema(description = "角色:UPLOADER / ANNOTATOR / REVIEWER / ADMIN") + private String role; + @Schema(description = "所属公司 ID") + private Long companyId; + @Schema(description = "所属公司名称") + private String companyName; +} +``` + +- [ ] **Step 4: 替换 TaskResponse.java** + +```java +package com.label.module.task.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 任务接口统一响应体(任务池、我的任务、任务详情均复用)。 + */ +@Data +@Builder +@Schema(description = "标注任务响应") +public class TaskResponse { + @Schema(description = "任务主键") + private Long id; + @Schema(description = "关联资料 ID") + private Long sourceId; + @Schema(description = "任务阶段:EXTRACTION / QA_GENERATION") + private String taskType; + @Schema(description = "任务状态:UNCLAIMED / IN_PROGRESS / SUBMITTED / APPROVED / REJECTED") + private String status; + @Schema(description = "领取人用户 ID") + private Long claimedBy; + @Schema(description = "领取时间") + private LocalDateTime claimedAt; + @Schema(description = "提交时间") + private LocalDateTime submittedAt; + @Schema(description = "完成时间") + private LocalDateTime completedAt; + @Schema(description = "驳回原因(REJECTED 状态时非空)") + private String rejectReason; + @Schema(description = "创建时间") + private LocalDateTime createdAt; +} +``` + +- [ ] **Step 5: 替换 SourceResponse.java** + +```java +package com.label.module.source.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 资料接口统一响应体(上传、列表、详情均复用此类)。 + * 各端点按需填充字段,未填充字段序列化时因 jackson non_null 配置自动省略。 + */ +@Data +@Builder +@Schema(description = "原始资料响应") +public class SourceResponse { + @Schema(description = "资料主键") + private Long id; + @Schema(description = "原始文件名") + private String fileName; + @Schema(description = "资料类型:TEXT / IMAGE / VIDEO") + private String dataType; + @Schema(description = "文件大小(字节)") + private Long fileSize; + @Schema(description = "状态:PENDING / PREPROCESSING / EXTRACTING / QA_REVIEW / APPROVED") + private String status; + @Schema(description = "上传用户 ID(列表端点返回)") + private Long uploaderId; + @Schema(description = "15 分钟预签名下载链接(详情端点返回)") + private String presignedUrl; + @Schema(description = "父资料 ID(视频帧/文本片段;详情端点返回)") + private Long parentSourceId; + @Schema(description = "创建时间") + private LocalDateTime createdAt; +} +``` + +- [ ] **Step 6: 编译验证** + +```bash +cd label_backend +mvn compile -q +``` + +预期:`BUILD SUCCESS` + +- [ ] **Step 7: Commit** + +```bash +cd label_backend +git add src/main/java/com/label/module/user/dto/ \ + src/main/java/com/label/module/task/dto/ \ + src/main/java/com/label/module/source/dto/ +git commit -m "feat: add @Schema annotations to core DTOs" +``` + +--- + +## Task 5: AuthController + UserController 添加 Swagger 注解 + +**Files:** +- Modify: `src/main/java/com/label/module/user/controller/AuthController.java` +- Modify: `src/main/java/com/label/module/user/controller/UserController.java` + +- [ ] **Step 1: 替换 AuthController.java** + +```java +package com.label.module.user.controller; + +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.user.dto.LoginRequest; +import com.label.module.user.dto.LoginResponse; +import com.label.module.user.dto.UserInfoResponse; +import com.label.module.user.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 认证接口:登录、退出、获取当前用户。 + */ +@Tag(name = "认证管理", description = "登录、退出、获取当前用户信息") +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @Operation(summary = "用户登录,返回 Bearer Token") + @PostMapping("/login") + public Result login(@RequestBody LoginRequest request) { + return Result.success(authService.login(request)); + } + + @Operation(summary = "退出登录,立即删除 Redis Token") + @PostMapping("/logout") + public Result logout(HttpServletRequest request) { + String token = extractToken(request); + authService.logout(token); + return Result.success(null); + } + + @Operation(summary = "获取当前登录用户信息") + @GetMapping("/me") + public Result me(HttpServletRequest request) { + TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__"); + return Result.success(authService.me(principal)); + } + + private String extractToken(HttpServletRequest request) { + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + return authHeader.substring(7).trim(); + } + return null; + } +} +``` + +- [ ] **Step 2: 替换 UserController.java** + +```java +package com.label.module.user.controller; + +import com.label.common.result.PageResult; +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.user.entity.SysUser; +import com.label.module.user.service.UserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 用户管理接口(5 个端点,全部 ADMIN 权限)。 + */ +@Tag(name = "用户管理", description = "用户 CRUD、状态与角色变更(ADMIN 专属)") +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + @Operation(summary = "分页查询用户列表") + @GetMapping + @RequiresRoles("ADMIN") + public Result> listUsers( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + HttpServletRequest request) { + return Result.success(userService.listUsers(page, pageSize, principal(request))); + } + + @Operation(summary = "创建用户") + @PostMapping + @RequiresRoles("ADMIN") + public Result createUser(@RequestBody Map body, + HttpServletRequest request) { + return Result.success(userService.createUser( + body.get("username"), + body.get("password"), + body.get("realName"), + body.get("role"), + principal(request))); + } + + @Operation(summary = "更新用户基本信息(realName / password)") + @PutMapping("/{id}") + @RequiresRoles("ADMIN") + public Result updateUser(@PathVariable Long id, + @RequestBody Map body, + HttpServletRequest request) { + return Result.success(userService.updateUser( + id, + body.get("realName"), + body.get("password"), + principal(request))); + } + + @Operation(summary = "变更用户状态(ACTIVE / DISABLED)") + @PutMapping("/{id}/status") + @RequiresRoles("ADMIN") + public Result updateStatus(@PathVariable Long id, + @RequestBody Map body, + HttpServletRequest request) { + userService.updateStatus(id, body.get("status"), principal(request)); + return Result.success(null); + } + + @Operation(summary = "变更用户角色,立即驱逐权限缓存") + @PutMapping("/{id}/role") + @RequiresRoles("ADMIN") + public Result updateRole(@PathVariable Long id, + @RequestBody Map body, + HttpServletRequest request) { + userService.updateRole(id, body.get("role"), principal(request)); + return Result.success(null); + } + + private TokenPrincipal principal(HttpServletRequest request) { + return (TokenPrincipal) request.getAttribute("__token_principal__"); + } +} +``` + +- [ ] **Step 3: 编译验证** + +```bash +cd label_backend +mvn compile -q +``` + +预期:`BUILD SUCCESS` + +- [ ] **Step 4: Commit** + +```bash +cd label_backend +git add src/main/java/com/label/module/user/controller/ +git commit -m "feat: add @Tag/@Operation to AuthController and UserController" +``` + +--- + +## Task 6: SourceController + TaskController 添加 Swagger 注解 + +**Files:** +- Modify: `src/main/java/com/label/module/source/controller/SourceController.java` +- Modify: `src/main/java/com/label/module/task/controller/TaskController.java` + +- [ ] **Step 1: 替换 SourceController.java** + +```java +package com.label.module.source.controller; + +import com.label.common.result.PageResult; +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.source.dto.SourceResponse; +import com.label.module.source.service.SourceService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +/** + * 原始资料管理接口。 + */ +@Tag(name = "资料管理", description = "文件上传至 RustFS、资料元数据管理") +@RestController +@RequestMapping("/api/source") +@RequiredArgsConstructor +public class SourceController { + + private final SourceService sourceService; + + @Operation(summary = "上传文件(multipart/form-data),返回资料摘要") + @PostMapping("/upload") + @RequiresRoles("UPLOADER") + @ResponseStatus(HttpStatus.CREATED) + public Result upload( + @RequestParam("file") MultipartFile file, + @RequestParam("dataType") String dataType, + HttpServletRequest request) { + TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__"); + return Result.success(sourceService.upload(file, dataType, principal)); + } + + @Operation(summary = "分页查询资料列表(UPLOADER 只见自己,ADMIN 见全部)") + @GetMapping("/list") + @RequiresRoles("UPLOADER") + public Result> list( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + @RequestParam(required = false) String dataType, + @RequestParam(required = false) String status, + HttpServletRequest request) { + TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__"); + return Result.success(sourceService.list(page, pageSize, dataType, status, principal)); + } + + @Operation(summary = "查询资料详情(含 15 分钟预签名下载链接)") + @GetMapping("/{id}") + @RequiresRoles("UPLOADER") + public Result findById(@PathVariable Long id) { + return Result.success(sourceService.findById(id)); + } + + @Operation(summary = "删除资料(仅 PENDING 状态可删),同步删除 RustFS 文件") + @DeleteMapping("/{id}") + @RequiresRoles("ADMIN") + public Result delete(@PathVariable Long id, HttpServletRequest request) { + TokenPrincipal principal = (TokenPrincipal) request.getAttribute("__token_principal__"); + sourceService.delete(id, principal.getCompanyId()); + return Result.success(null); + } +} +``` + +- [ ] **Step 2: 替换 TaskController.java** + +```java +package com.label.module.task.controller; + +import com.label.common.result.PageResult; +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.task.dto.TaskResponse; +import com.label.module.task.service.TaskClaimService; +import com.label.module.task.service.TaskService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 任务管理接口(10 个端点)。 + */ +@Tag(name = "任务管理", description = "任务创建、领取、放弃、审批、强制转移") +@RestController +@RequestMapping("/api/tasks") +@RequiredArgsConstructor +public class TaskController { + + private final TaskService taskService; + private final TaskClaimService taskClaimService; + + @Operation(summary = "查询可领取任务池(UNCLAIMED,ANNOTATOR 看 EXTRACTION,REVIEWER 看 SUBMITTED)") + @GetMapping("/pool") + @RequiresRoles("ANNOTATOR") + public Result> getPool( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + HttpServletRequest request) { + return Result.success(taskService.getPool(page, pageSize, principal(request))); + } + + @Operation(summary = "查询我的任务(IN_PROGRESS / SUBMITTED / REJECTED)") + @GetMapping("/mine") + @RequiresRoles("ANNOTATOR") + public Result> getMine( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + @RequestParam(required = false) String status, + HttpServletRequest request) { + return Result.success(taskService.getMine(page, pageSize, status, principal(request))); + } + + @Operation(summary = "查询待审批队列(REVIEWER 专属,status=SUBMITTED)") + @GetMapping("/pending-review") + @RequiresRoles("REVIEWER") + public Result> getPendingReview( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + @RequestParam(required = false) String taskType) { + return Result.success(taskService.getPendingReview(page, pageSize, taskType)); + } + + @Operation(summary = "查询全部任务(ADMIN,支持状态/类型过滤)") + @GetMapping + @RequiresRoles("ADMIN") + public Result> getAll( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + @RequestParam(required = false) String status, + @RequestParam(required = false) String taskType) { + return Result.success(taskService.getAll(page, pageSize, status, taskType)); + } + + @Operation(summary = "为指定资料创建标注任务(ADMIN)") + @PostMapping + @RequiresRoles("ADMIN") + public Result createTask(@RequestBody Map body, + HttpServletRequest request) { + Long sourceId = Long.parseLong(body.get("sourceId").toString()); + String taskType = body.get("taskType").toString(); + TokenPrincipal principal = principal(request); + return Result.success(taskService.toPublicResponse( + taskService.createTask(sourceId, taskType, principal.getCompanyId()))); + } + + @Operation(summary = "查询任务详情") + @GetMapping("/{id}") + @RequiresRoles("ANNOTATOR") + public Result getById(@PathVariable Long id) { + return Result.success(taskService.toPublicResponse(taskService.getById(id))); + } + + @Operation(summary = "领取任务(Redis SET NX + DB 乐观锁双重保障)") + @PostMapping("/{id}/claim") + @RequiresRoles("ANNOTATOR") + public Result claim(@PathVariable Long id, HttpServletRequest request) { + taskClaimService.claim(id, principal(request)); + return Result.success(null); + } + + @Operation(summary = "放弃任务,退回任务池") + @PostMapping("/{id}/unclaim") + @RequiresRoles("ANNOTATOR") + public Result unclaim(@PathVariable Long id, HttpServletRequest request) { + taskClaimService.unclaim(id, principal(request)); + return Result.success(null); + } + + @Operation(summary = "重领被驳回的任务(task.status=REJECTED 且 claimedBy=当前用户)") + @PostMapping("/{id}/reclaim") + @RequiresRoles("ANNOTATOR") + public Result reclaim(@PathVariable Long id, HttpServletRequest request) { + taskClaimService.reclaim(id, principal(request)); + return Result.success(null); + } + + @Operation(summary = "ADMIN 强制转移任务归属") + @PutMapping("/{id}/reassign") + @RequiresRoles("ADMIN") + public Result reassign(@PathVariable Long id, + @RequestBody Map body, + HttpServletRequest request) { + Long targetUserId = Long.parseLong(body.get("userId").toString()); + taskService.reassign(id, targetUserId, principal(request)); + return Result.success(null); + } + + private TokenPrincipal principal(HttpServletRequest request) { + return (TokenPrincipal) request.getAttribute("__token_principal__"); + } +} +``` + +- [ ] **Step 3: 编译验证** + +```bash +cd label_backend +mvn compile -q +``` + +预期:`BUILD SUCCESS` + +- [ ] **Step 4: Commit** + +```bash +cd label_backend +git add src/main/java/com/label/module/source/controller/ \ + src/main/java/com/label/module/task/controller/ +git commit -m "feat: add @Tag/@Operation to SourceController and TaskController" +``` + +--- + +## Task 7: ExtractionController + QaController 添加 Swagger 注解 + +**Files:** +- Modify: `src/main/java/com/label/module/annotation/controller/ExtractionController.java` +- Modify: `src/main/java/com/label/module/annotation/controller/QaController.java` + +- [ ] **Step 1: 替换 ExtractionController.java** + +```java +package com.label.module.annotation.controller; + +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.annotation.service.ExtractionService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 提取阶段标注工作台接口(5 个端点)。 + */ +@Tag(name = "标注工作台", description = "EXTRACTION 阶段:AI 预标注、结果编辑、提交、审批") +@RestController +@RequestMapping("/api/extraction") +@RequiredArgsConstructor +public class ExtractionController { + + private final ExtractionService extractionService; + + @Operation(summary = "获取当前提取结果(含 AI 预标注数据)") + @GetMapping("/{taskId}") + @RequiresRoles("ANNOTATOR") + public Result> getResult(@PathVariable Long taskId, + HttpServletRequest request) { + return Result.success(extractionService.getResult(taskId, principal(request))); + } + + @Operation(summary = "更新提取结果(整体 JSONB 覆盖,PUT 语义)") + @PutMapping("/{taskId}") + @RequiresRoles("ANNOTATOR") + public Result updateResult(@PathVariable Long taskId, + @RequestBody String resultJson, + HttpServletRequest request) { + extractionService.updateResult(taskId, resultJson, principal(request)); + return Result.success(null); + } + + @Operation(summary = "提交提取结果,进入审批队列") + @PostMapping("/{taskId}/submit") + @RequiresRoles("ANNOTATOR") + public Result submit(@PathVariable Long taskId, + HttpServletRequest request) { + extractionService.submit(taskId, principal(request)); + return Result.success(null); + } + + @Operation(summary = "审批通过(REVIEWER),自动触发 QA 任务创建") + @PostMapping("/{taskId}/approve") + @RequiresRoles("REVIEWER") + public Result approve(@PathVariable Long taskId, + HttpServletRequest request) { + extractionService.approve(taskId, principal(request)); + return Result.success(null); + } + + @Operation(summary = "驳回提取结果(REVIEWER),标注员可重领后修改") + @PostMapping("/{taskId}/reject") + @RequiresRoles("REVIEWER") + public Result reject(@PathVariable Long taskId, + @RequestBody Map body, + HttpServletRequest request) { + String reason = body != null ? body.get("reason") : null; + extractionService.reject(taskId, reason, principal(request)); + return Result.success(null); + } + + private TokenPrincipal principal(HttpServletRequest request) { + return (TokenPrincipal) request.getAttribute("__token_principal__"); + } +} +``` + +- [ ] **Step 2: 替换 QaController.java** + +```java +package com.label.module.annotation.controller; + +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.annotation.service.QaService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 问答生成阶段标注工作台接口(5 个端点)。 + */ +@Tag(name = "问答生成", description = "QA_GENERATION 阶段:查看候选问答对、修改、提交、审批") +@RestController +@RequestMapping("/api/qa") +@RequiredArgsConstructor +public class QaController { + + private final QaService qaService; + + @Operation(summary = "获取候选问答对列表") + @GetMapping("/{taskId}") + @RequiresRoles("ANNOTATOR") + public Result> getResult(@PathVariable Long taskId, + HttpServletRequest request) { + return Result.success(qaService.getResult(taskId, principal(request))); + } + + @Operation(summary = "修改问答对(整体覆盖,PUT 语义)") + @PutMapping("/{taskId}") + @RequiresRoles("ANNOTATOR") + public Result updateResult(@PathVariable Long taskId, + @RequestBody String body, + HttpServletRequest request) { + qaService.updateResult(taskId, body, principal(request)); + return Result.success(null); + } + + @Operation(summary = "提交问答对,进入审批队列") + @PostMapping("/{taskId}/submit") + @RequiresRoles("ANNOTATOR") + public Result submit(@PathVariable Long taskId, + HttpServletRequest request) { + qaService.submit(taskId, principal(request)); + return Result.success(null); + } + + @Operation(summary = "审批通过(REVIEWER),写入 training_dataset,流水线完成") + @PostMapping("/{taskId}/approve") + @RequiresRoles("REVIEWER") + public Result approve(@PathVariable Long taskId, + HttpServletRequest request) { + qaService.approve(taskId, principal(request)); + return Result.success(null); + } + + @Operation(summary = "驳回问答对(REVIEWER),删除候选记录,标注员重新生成") + @PostMapping("/{taskId}/reject") + @RequiresRoles("REVIEWER") + public Result reject(@PathVariable Long taskId, + @RequestBody Map body, + HttpServletRequest request) { + String reason = body != null ? body.get("reason") : null; + qaService.reject(taskId, reason, principal(request)); + return Result.success(null); + } + + private TokenPrincipal principal(HttpServletRequest request) { + return (TokenPrincipal) request.getAttribute("__token_principal__"); + } +} +``` + +- [ ] **Step 3: 编译验证** + +```bash +cd label_backend +mvn compile -q +``` + +预期:`BUILD SUCCESS` + +- [ ] **Step 4: Commit** + +```bash +cd label_backend +git add src/main/java/com/label/module/annotation/controller/ +git commit -m "feat: add @Tag/@Operation to ExtractionController and QaController" +``` + +--- + +## Task 8: ExportController + SysConfigController + VideoController 添加 Swagger 注解 + +**Files:** +- Modify: `src/main/java/com/label/module/export/controller/ExportController.java` +- Modify: `src/main/java/com/label/module/config/controller/SysConfigController.java` +- Modify: `src/main/java/com/label/module/video/controller/VideoController.java` + +- [ ] **Step 1: 替换 ExportController.java** + +```java +package com.label.module.export.controller; + +import com.label.common.result.PageResult; +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.annotation.entity.TrainingDataset; +import com.label.module.export.entity.ExportBatch; +import com.label.module.export.service.ExportService; +import com.label.module.export.service.FinetuneService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 训练数据导出与微调接口(5 个端点,全部 ADMIN 权限)。 + */ +@Tag(name = "导出管理", description = "训练样本查询、JSONL 批次导出、GLM 微调任务管理") +@RestController +@RequiredArgsConstructor +public class ExportController { + + private final ExportService exportService; + private final FinetuneService finetuneService; + + @Operation(summary = "分页查询已审批可导出的训练样本") + @GetMapping("/api/training/samples") + @RequiresRoles("ADMIN") + public Result> listSamples( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + @RequestParam(required = false) String sampleType, + @RequestParam(required = false) Boolean exported, + HttpServletRequest request) { + return Result.success(exportService.listSamples(page, pageSize, sampleType, exported, principal(request))); + } + + @Operation(summary = "创建导出批次,合并样本为 JSONL 并上传 RustFS") + @PostMapping("/api/export/batch") + @RequiresRoles("ADMIN") + @ResponseStatus(HttpStatus.CREATED) + public Result createBatch(@RequestBody Map body, + HttpServletRequest request) { + @SuppressWarnings("unchecked") + List rawIds = (List) body.get("sampleIds"); + List sampleIds = rawIds.stream() + .map(id -> Long.parseLong(id.toString())) + .toList(); + return Result.success(exportService.createBatch(sampleIds, principal(request))); + } + + @Operation(summary = "向 GLM 工厂提交微调任务") + @PostMapping("/api/export/{batchId}/finetune") + @RequiresRoles("ADMIN") + public Result> triggerFinetune(@PathVariable Long batchId, + HttpServletRequest request) { + return Result.success(finetuneService.trigger(batchId, principal(request))); + } + + @Operation(summary = "查询微调任务状态") + @GetMapping("/api/export/{batchId}/status") + @RequiresRoles("ADMIN") + public Result> getFinetuneStatus(@PathVariable Long batchId, + HttpServletRequest request) { + return Result.success(finetuneService.getStatus(batchId, principal(request))); + } + + @Operation(summary = "分页查询所有导出批次") + @GetMapping("/api/export/list") + @RequiresRoles("ADMIN") + public Result> listBatches( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "20") int pageSize, + HttpServletRequest request) { + return Result.success(exportService.listBatches(page, pageSize, principal(request))); + } + + private TokenPrincipal principal(HttpServletRequest request) { + return (TokenPrincipal) request.getAttribute("__token_principal__"); + } +} +``` + +- [ ] **Step 2: 替换 SysConfigController.java** + +```java +package com.label.module.config.controller; + +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.config.entity.SysConfig; +import com.label.module.config.service.SysConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 系统配置接口(2 个端点,均需 ADMIN 权限)。 + */ +@Tag(name = "系统配置", description = "Prompt 模板、模型参数等全局/公司级配置管理") +@RestController +@RequiredArgsConstructor +public class SysConfigController { + + private final SysConfigService sysConfigService; + + @Operation(summary = "查询合并后的配置列表(公司专属 + 全局默认)") + @GetMapping("/api/config") + @RequiresRoles("ADMIN") + public Result>> listConfig(HttpServletRequest request) { + TokenPrincipal principal = principal(request); + return Result.success(sysConfigService.list(principal.getCompanyId())); + } + + @Operation(summary = "UPSERT 公司专属配置项") + @PutMapping("/api/config/{key}") + @RequiresRoles("ADMIN") + public Result updateConfig(@PathVariable String key, + @RequestBody Map body, + HttpServletRequest request) { + String value = body.get("value"); + String description = body.get("description"); + TokenPrincipal principal = principal(request); + return Result.success( + sysConfigService.update(key, value, description, principal.getCompanyId())); + } + + private TokenPrincipal principal(HttpServletRequest request) { + return (TokenPrincipal) request.getAttribute("__token_principal__"); + } +} +``` + +- [ ] **Step 3: 替换 VideoController.java** + +```java +package com.label.module.video.controller; + +import com.label.common.result.Result; +import com.label.common.shiro.TokenPrincipal; +import com.label.module.video.entity.VideoProcessJob; +import com.label.module.video.service.VideoProcessService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.authz.annotation.RequiresRoles; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * 视频处理接口(4 个端点)。 + */ +@Tag(name = "视频处理", description = "触发视频抽帧/转文本、查询任务状态、AI 回调接收") +@Slf4j +@RestController +@RequiredArgsConstructor +public class VideoController { + + private final VideoProcessService videoProcessService; + + @Value("${video.callback-secret:}") + private String callbackSecret; + + @Operation(summary = "触发视频处理任务(FRAME_EXTRACT 或 VIDEO_TO_TEXT)") + @PostMapping("/api/video/process") + @RequiresRoles("ADMIN") + public Result createJob(@RequestBody Map body, + HttpServletRequest request) { + Object sourceIdVal = body.get("sourceId"); + Object jobTypeVal = body.get("jobType"); + if (sourceIdVal == null || jobTypeVal == null) { + return Result.failure("INVALID_PARAMS", "sourceId 和 jobType 不能为空"); + } + Long sourceId = Long.parseLong(sourceIdVal.toString()); + String jobType = jobTypeVal.toString(); + String params = body.containsKey("params") ? body.get("params").toString() : null; + + TokenPrincipal principal = principal(request); + return Result.success( + videoProcessService.createJob(sourceId, jobType, params, principal.getCompanyId())); + } + + @Operation(summary = "查询视频处理任务状态") + @GetMapping("/api/video/jobs/{jobId}") + @RequiresRoles("ADMIN") + public Result getJob(@PathVariable Long jobId, + HttpServletRequest request) { + return Result.success(videoProcessService.getJob(jobId, principal(request).getCompanyId())); + } + + @Operation(summary = "管理员重置失败的视频处理任务(FAILED → PENDING)") + @PostMapping("/api/video/jobs/{jobId}/reset") + @RequiresRoles("ADMIN") + public Result resetJob(@PathVariable Long jobId, + HttpServletRequest request) { + return Result.success(videoProcessService.reset(jobId, principal(request).getCompanyId())); + } + + @Operation(summary = "AI 服务回调接口(无需 Bearer Token,内部调用)") + @PostMapping("/api/video/callback") + public Result handleCallback(@RequestBody Map body, + HttpServletRequest request) { + if (callbackSecret != null && !callbackSecret.isBlank()) { + String provided = request.getHeader("X-Callback-Secret"); + if (!callbackSecret.equals(provided)) { + return Result.failure("UNAUTHORIZED", "回调密钥无效"); + } + } + + Long jobId = Long.parseLong(body.get("jobId").toString()); + String status = (String) body.get("status"); + String outputPath = body.containsKey("outputPath") ? (String) body.get("outputPath") : null; + String errorMessage = body.containsKey("errorMessage") ? (String) body.get("errorMessage") : null; + + log.info("视频处理回调:jobId={}, status={}", jobId, status); + videoProcessService.handleCallback(jobId, status, outputPath, errorMessage); + return Result.success(null); + } + + private TokenPrincipal principal(HttpServletRequest request) { + return (TokenPrincipal) request.getAttribute("__token_principal__"); + } +} +``` + +- [ ] **Step 4: 最终编译验证** + +```bash +cd label_backend +mvn compile -q +``` + +预期:`BUILD SUCCESS` + +- [ ] **Step 5: 最终打包验证** + +```bash +cd label_backend +mvn package -DskipTests -q +``` + +预期:`BUILD SUCCESS`,`target/` 下生成 `.zip` 和 `.tar.gz` + +- [ ] **Step 6: Commit** + +```bash +cd label_backend +git add src/main/java/com/label/module/export/controller/ \ + src/main/java/com/label/module/config/controller/ \ + src/main/java/com/label/module/video/controller/ +git commit -m "feat: add @Tag/@Operation to ExportController, SysConfigController, VideoController" +``` + +--- + +## Self-Review + +**Spec coverage 核对:** + +| 规格要求 | 对应 Task | +|----------|-----------| +| pom.xml 新增 springdoc 依赖 | Task 1 | +| application.yml 配置 springdoc | Task 1 | +| application.yml 配置 shiro.auth 开关 | Task 1 | +| TokenFilter swagger 路径白名单 | Task 2 | +| TokenFilter shiro.auth.enabled=false 分支 | Task 2 | +| OpenApiConfig Bearer SecurityScheme | Task 3 | +| 5 个核心 DTO @Schema | Task 4 | +| AuthController + UserController @Tag/@Operation | Task 5 | +| SourceController + TaskController @Tag/@Operation | Task 6 | +| ExtractionController + QaController @Tag/@Operation | Task 7 | +| ExportController + SysConfigController + VideoController @Tag/@Operation | Task 8 | + +**全部覆盖,无遗漏。** diff --git a/src/main/java/com/label/common/config/OpenApiConfig.java b/src/main/java/com/label/common/config/OpenApiConfig.java new file mode 100644 index 0000000..8b695cf --- /dev/null +++ b/src/main/java/com/label/common/config/OpenApiConfig.java @@ -0,0 +1,33 @@ +package com.label.common.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * SpringDoc OpenAPI 全局配置:API 基本信息 + Bearer Token 安全方案。 + */ +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("Label Backend API") + .version("1.0.0") + .description("知识图谱智能标注平台后端接口文档")) + .addSecurityItem(new SecurityRequirement().addList("BearerAuth")) + .components(new Components() + .addSecuritySchemes("BearerAuth", + new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("UUID") + .description("登录后返回的 Token,格式:Bearer {uuid}"))); + } +}