Przeglądaj źródła

docs(swagger): 完善 Swagger 泛型返回类型文档说明

- 添加了常见 HTTP 响应状态码的说明和使用建议
- 为所有接口补充了 400、500 等错误状态码的 @ApiResponse 注解
- 创建 UserBindingPageResult 类用于分页结果的文档说明
- 在分页查询接口中使用专门的文档说明类替代泛型 Page 类
- 补充了复杂泛型类型在 Swagger 中的处理方案
- 增加了为每个字段添加 @Schema(description) 的最佳实践
- 更新了验证方法,强调检查所有可能的响应状态码声明
- 明确了适用范围包括分页查询等复杂泛型类型场景
mcbaiyun 1 miesiąc temu
rodzic
commit
beaf3395b2

+ 91 - 3
docs/Swagger泛型返回类型字段信息不显示问题解决方案.md

@@ -4,7 +4,13 @@
 
 在使用 Swagger/OpenAPI 生成 API 文档时,当接口返回的是泛型类型(如项目中的 `R<T>`)时,Swagger 无法自动推断出泛型参数的具体类型信息,导致在文档中无法正确显示实际返回数据的结构。
 
-具体表现为:接口实际返回 `R<CheckUserBindingResponse>`,但在 Swagger UI 中只显示了 `R` 类的基本结构(code、message、data),而没有显示 data 字段中 `CheckUserBindingResponse` 的具体字段(exists 和 bindingType)。
+具体表现为以下几种情况:
+
+1. 接口实际返回 `R<CheckUserBindingResponse>`,但在 Swagger UI 中只显示了 `R` 类的基本结构(code、message、data),而没有显示 data 字段中 `CheckUserBindingResponse` 的具体字段(exists 和 bindingType)。
+
+2. 分页查询接口返回 `R<Page<UserBindingResponse>>`,但 Swagger UI 中 Page 类的 records 字段说明为空,没有显示其实际包含的是 `UserBindingResponse` 对象列表。
+
+3. 各种HTTP响应状态码未在文档中明确声明,导致 API 使用者不清楚接口可能返回的所有情况。
 
 ## 问题原因
 
@@ -18,6 +24,26 @@ Swagger 在处理 Java 泛型时存在局限性,无法在运行时获取泛型
 
 1. 在需要明确指定返回类型的 Controller 方法上添加 `@ApiResponse` 注解
 2. 在 `@ApiResponse` 中通过 `content` 属性指定具体的媒体类型和 schema 实现类
+3. 为所有可能的响应状态码添加 `@ApiResponse` 注解
+4. 对于复杂的泛型类型(如分页结果),创建专门用于文档说明的 VO 类
+
+### 常见HTTP响应状态码说明
+
+在设计 RESTful API 时,应考虑以下常见的HTTP状态码:
+
+- **200 OK** - 请求成功,返回正常数据
+- **201 Created** - 创建资源成功
+- **204 No Content** - 请求成功,但无返回内容
+- **400 Bad Request** - 客户端请求参数错误
+- **401 Unauthorized** - 未认证或认证失败
+- **403 Forbidden** - 无权限访问
+- **404 Not Found** - 请求的资源不存在
+- **409 Conflict** - 请求冲突,如重复创建
+- **500 Internal Server Error** - 服务器内部错误
+- **502 Bad Gateway** - 网关错误
+- **503 Service Unavailable** - 服务不可用
+
+在实际项目中,应根据接口的具体业务逻辑和可能的出错场景,选择合适的状态码并添加对应的 `@ApiResponse` 注解。
 
 ### 示例代码
 
@@ -27,6 +53,12 @@ Swagger 在处理 Java 泛型时存在局限性,无法在运行时获取泛型
 @ApiResponse(responseCode = "200", description = "OK",
              content = @Content(mediaType = "application/json",
                                schema = @Schema(implementation = CheckUserBindingResponse.class)))
+@ApiResponse(responseCode = "400", description = "Bad Request",
+             content = @Content(mediaType = "application/json",
+                               schema = @Schema(implementation = Void.class)))
+@ApiResponse(responseCode = "500", description = "Internal Server Error",
+             content = @Content(mediaType = "application/json",
+                               schema = @Schema(implementation = Void.class)))
 public R<?> check(@RequestBody CheckUserBindingRequest req) {
     try {
         CheckUserBindingResponse response = userBindingService.checkUserBinding(req);
@@ -37,11 +69,62 @@ public R<?> check(@RequestBody CheckUserBindingRequest req) {
 }
 ```
 
+对于分页查询接口,需要创建专门的文档说明类:
+
+```java
+// UserBindingPageResult.java
+@Schema(description = "用户绑定关系分页查询结果")
+@Data
+public class UserBindingPageResult {
+    @Schema(description = "用户绑定关系列表")
+    private List<UserBindingResponse> records;
+    
+    @Schema(description = "总记录数")
+    private Long total;
+    
+    @Schema(description = "每页大小")
+    private Long size;
+    
+    @Schema(description = "当前页码")
+    private Long current;
+    
+    @Schema(description = "总页数")
+    private Long pages;
+}
+```
+
+然后在 Controller 中使用:
+
+```java
+@Operation(summary = "分页查询患者的绑定关系列表", description = "根据患者ID和绑定类型查询绑定关系列表")
+@PostMapping(path = "/list-by-patient", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+@ApiResponse(responseCode = "200", description = "OK",
+             content = @Content(mediaType = "application/json",
+                               schema = @Schema(implementation = UserBindingPageResult.class)))
+@ApiResponse(responseCode = "400", description = "Bad Request",
+             content = @Content(mediaType = "application/json",
+                               schema = @Schema(implementation = Void.class)))
+@ApiResponse(responseCode = "500", description = "Internal Server Error",
+             content = @Content(mediaType = "application/json",
+                               schema = @Schema(implementation = Void.class)))
+public R<?> listByPatient(Long patientUserId, String bindingType, @RequestBody BaseQueryRequest req) {
+    try {
+        Page<UserBindingResponse> page = userBindingService.listBindingsByPatient(patientUserId, bindingType, req);
+        return R.success(200, "ok", page);
+    } catch (Exception e) {
+        return R.fail(500, e.getMessage());
+    }
+}
+```
+
 ## 注意事项
 
 1. 保持项目中其他接口的一致性,如果其他接口返回类型使用的是 `R<?>`,则此接口也应保持一致
 2. `@ApiResponse` 注解中的 `implementation` 属性应指向实际的数据载体类,而非泛型包装类
-3. 此方法适用于所有需要在 Swagger 文档中明确显示返回数据结构的场景
+3. 为所有可能的响应状态码添加 `@ApiResponse` 注解,不仅限于成功和错误情况
+4. 对于复杂的泛型类型(如分页结果),创建专门用于文档说明的 VO 类,避免直接使用框架类(如 Page)
+5. 确保为每个字段添加 `@Schema(description = "...")` 注解,提供清晰的字段说明
+6. 根据接口的实际业务逻辑选择合适的HTTP状态码,不要随意使用
 
 ## 验证方法
 
@@ -54,13 +137,18 @@ public R<?> check(@RequestBody CheckUserBindingRequest req) {
 
 3. 找到对应的接口,查看其响应模型是否正确显示了所有字段信息
 
+4. 确认所有可能的响应状态码都已在文档中声明
+
 ## 适用范围
 
 此解决方案适用于:
 - 所有使用泛型包装类(如 `R<T>`、`ResponseEntity<T>` 等)作为返回类型的接口
 - 需要在 Swagger 文档中明确展示实际返回数据结构的场景
 - 前后端分离项目中为前端开发人员提供准确 API 文档的需求
+- 分页查询接口等复杂泛型类型场景
 
 ## 总结
 
-通过显式声明泛型返回类型的实际结构,我们可以有效解决 Swagger 无法自动解析泛型参数类型的问题,为 API 使用者提供更准确、更详细的接口文档信息。这种方法在不改变代码逻辑的前提下,仅通过添加注解的方式优化了 API 文档的可读性和可用性。
+通过显式声明泛型返回类型的实际结构,我们可以有效解决 Swagger 无法自动解析泛型参数类型的问题,为 API 使用者提供更准确、更详细的接口文档信息。这种方法在不改变代码逻辑的前提下,仅通过添加注解的方式优化了 API 文档的可读性和可用性。
+
+特别需要注意的是,不仅要处理成功响应(200)的情况,还要为各种可能的错误响应(如400、401、403、404、500等)添加文档说明,并且对于复杂的泛型类型(如分页结果),需要创建专门的文档说明类来确保字段信息能正确显示。合理的状态码使用能够帮助API使用者更好地理解和处理各种响应情况。

+ 34 - 0
src/main/java/work/baiyun/chronicdiseaseapp/controller/UserBindingController.java

@@ -2,6 +2,9 @@ package work.baiyun.chronicdiseaseapp.controller;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
@@ -16,6 +19,7 @@ import work.baiyun.chronicdiseaseapp.model.vo.CreateUserBindingRequest;
 import work.baiyun.chronicdiseaseapp.model.vo.DeleteUserBindingRequest;
 import work.baiyun.chronicdiseaseapp.model.vo.CheckUserBindingResponse;
 import work.baiyun.chronicdiseaseapp.model.vo.CheckUserBindingRequest;
+import work.baiyun.chronicdiseaseapp.model.vo.UserBindingPageResult;
 import work.baiyun.chronicdiseaseapp.service.UserBindingService;
 
 @RestController
@@ -28,6 +32,12 @@ public class UserBindingController {
 
     @Operation(summary = "创建用户绑定关系", description = "创建患者与医生或家属的绑定关系")
     @PostMapping(path = "/create", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiResponse(responseCode = "200", description = "OK",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = Void.class)))
+    @ApiResponse(responseCode = "500", description = "Internal Server Error",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = Void.class)))
     public R<?> create(@RequestBody CreateUserBindingRequest req) {
         try {
             userBindingService.createUserBinding(req);
@@ -39,6 +49,12 @@ public class UserBindingController {
 
     @Operation(summary = "删除用户绑定关系", description = "解除患者与医生或家属的绑定关系")
     @PostMapping(path = "/delete", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiResponse(responseCode = "200", description = "OK",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = Void.class)))
+    @ApiResponse(responseCode = "500", description = "Internal Server Error",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = Void.class)))
     public R<?> delete(@RequestBody DeleteUserBindingRequest req) {
         try {
             userBindingService.deleteUserBinding(req.getPatientUserId(), req.getBoundUserId());
@@ -50,6 +66,12 @@ public class UserBindingController {
 
     @Operation(summary = "分页查询患者的绑定关系列表", description = "根据患者ID和绑定类型查询绑定关系列表")
     @PostMapping(path = "/list-by-patient", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiResponse(responseCode = "200", description = "OK",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = UserBindingPageResult.class)))
+    @ApiResponse(responseCode = "500", description = "Internal Server Error",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = Void.class)))
     public R<?> listByPatient(Long patientUserId, String bindingType, @RequestBody BaseQueryRequest req) {
         try {
             Page<UserBindingResponse> page = userBindingService.listBindingsByPatient(patientUserId, bindingType, req);
@@ -61,6 +83,12 @@ public class UserBindingController {
 
     @Operation(summary = "分页查询用户被绑定的关系列表", description = "根据被绑定用户ID和绑定类型查询被绑定关系列表")
     @PostMapping(path = "/list-by-bound-user", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiResponse(responseCode = "200", description = "OK",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = UserBindingPageResult.class)))
+    @ApiResponse(responseCode = "500", description = "Internal Server Error",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = Void.class)))
     public R<?> listByBoundUser(Long boundUserId, String bindingType, @RequestBody BaseQueryRequest req) {
         try {
             Page<UserBindingResponse> page = userBindingService.listBindingsByBoundUser(boundUserId, bindingType, req);
@@ -72,6 +100,12 @@ public class UserBindingController {
 
     @Operation(summary = "检查用户绑定关系", description = "检查患者与医生或家属是否存在绑定关系")
     @PostMapping(path = "/check", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    @ApiResponse(responseCode = "200", description = "OK",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = CheckUserBindingResponse.class)))
+    @ApiResponse(responseCode = "500", description = "Internal Server Error",
+                 content = @Content(mediaType = "application/json",
+                                   schema = @Schema(implementation = Void.class)))
     public R<?> check(@RequestBody CheckUserBindingRequest req) {
         try {
             CheckUserBindingResponse response = userBindingService.checkUserBinding(req);

+ 27 - 0
src/main/java/work/baiyun/chronicdiseaseapp/model/vo/UserBindingPageResult.java

@@ -0,0 +1,27 @@
+package work.baiyun.chronicdiseaseapp.model.vo;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+@Schema(description = "用户绑定关系分页查询结果")
+@Data
+public class UserBindingPageResult {
+
+    @Schema(description = "用户绑定关系列表")
+    private List<UserBindingResponse> records;
+
+    @Schema(description = "总记录数")
+    private Long total;
+
+    @Schema(description = "每页大小")
+    private Long size;
+
+    @Schema(description = "当前页码")
+    private Long current;
+
+    @Schema(description = "总页数")
+    private Long pages;
+}