当前位置:   article > 正文

SpringBoot集成knife4j实现Swagger接口文档

springboot集成knife4j实现swagger接口文档

前言:如果你是后台开发,提供restful接口给前端,建议你使用Swagger3提供restful的接口文档自动生成和在线接口调试。knife4j是对Swagger进一步封装,其优化了API文档的UI界面,是本人最推荐的方式。


一、Swagger简介

1.1、Swagger

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。Swagger拥有接口文档自动生成和在线调试接口的两大功能。Swagger拥有众多不同语言和平台的开源实现与工具,他有很多实现方式,非常方便,并且支持语言特别多。

(1) Swagger是一组开源项目,其中主要项目如下:

  • Swagger-tools:提供各种与Swagger进行集成和交互的工具。例如模式检验、Swagger 1.2文档转换成Swagger 2.0文档等功能。

  • Swagger-core: 用于Java/Scala的的Swagger实现。与JAX-RS(Jersey、Resteasy、CXF...)、Servlets和Play框架进行集成。

  • Swagger-js: 用于JavaScript的Swagger实现。

  • Swagger-node-express: Swagger模块,用于node.js的Express web应用框架。

  • Swagger-ui:一个无依赖的HTML、JS和CSS集合,可以为Swagger兼容API动态生成优雅文档。

  • Swagger-codegen:一个模板驱动引擎,通过分析用户Swagger资源声明以各种语言生成客户端代码。

(2) springfox-swagger

在Spring中集成Swagger会使用到springfox-swagger,它对Spring和Swagger的使用进行了整合

springfox是Java对swagger的一个具体实现。springfox的前身是swagger-springmvc,用于springmvc与swagger的整合。它内部会自动解析Spring容器中Controller暴露出的接口,并且也提供了一个界面用于展示或调用这些API。

Springfox其实是一个通过扫描代码提取代码中的信息,生成API文档的工具。在Swagger的教程中,都会提到@Api@ApiModel@ApiOperation这些注解,这些注解其实不是Springfox的,而是Swagger的。springfox-swagger2这个包依赖了swagger-core这个包,而这些注解正是在这里面。但是,swagger-core这个包是只支持JAX-RS2的,并不支持常用的Spring MVC。这就是springfox-swagger的作用了,它将上面那些用于JAX-RS2的注解适配到了Spring MVC上。

Spring项目引入依赖:

  1. <dependency>
  2. <groupId>io.springfox</groupId>
  3. <artifactId>springfox-swagger2</artifactId>
  4. <version>${springfox.swagger.version}</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>io.springfox</groupId>
  8. <artifactId>springfox-swagger-ui</artifactId>
  9. <version>${springfox.swagger.version}</version>
  10. </dependency>

SpingBoot项目引入依赖:

  1. <dependency>
  2. <groupId>io.springfox</groupId>
  3. <artifactId>springfox-boot-starter</artifactId>
  4. <version>3.0.0</version>
  5. </dependency>

(3)Swagger 3 的使用

Swagger2(基于openApi2)已经在17年停止维护了,取而代之的是 Swagger3(基于openApi3),而国内 Swagger3使用的文档较少,百度搜出来的都是过时的Swagger2(17年停止维护并更名为swagger3)的使用。

  • Open API:OpenApi是业界真正的 api 文档标准,其是由 Swagger 来维护的,并被linux列为api标准,从而成为行业标准。

  • Swagger组织:swagger 是一个 api 文档维护组织,后来成为了 Open API 标准的主要定义者,现在最新的版本为17年发布的 Swagger3(Open Api3)。

  • swagger2的包名为 io.swagger,而swagger3的包名为 io.swagger.core.v3。

Swagger 3 相关特性:

  • 支持 Spring 5,Webflux(仅请求映射支持,尚不支持功能端点)、Spring Integration

  • 补充官方在 spring boot 的自动装配 springfox-boot-starter 以后可以直接依赖一个 dependency

  • 与2.0更好的规范兼容性

  • 支持OpenApi 3.0.3

  • 轻依赖 spring-plugin,swagger-core

  • 现有的swagger2批注将继续有效并丰富开放式API 3.0规范

1.2、springfox-swagger-ui

如果使用springfox-swagger-ui,启动项目后的api文档访问路径:http://localhost:8080/swagger-ui.html

 1.3、swagger-bootstrap-ui

swagger-bootstrap-ui是springfox-swagger的增强UI实现,api文档结构更加清晰,在线调试也很方便

  1. <dependency>
  2. <groupId>com.github.xiaoymin</groupId>
  3. <artifactId>swagger-bootstrap-ui</artifactId>
  4. <version>${swagger.bootstrap.ui.version}</version>
  5. </dependency>

访问的url为:http:// http://localhost:8080/doc.html

1.4、swagger-bootstrap-ui的升级版Knife4j

Knife4j在更名之前,原来的名称是叫swagger-bootstrap-ui,这是两种不一样风格的ui显示,将原来的蓝色变成炫酷的黑色模式,比传统的springfox-swagger-ui和swagger-bootstrap-ui更好看Knife4j是本人最推荐的方式

  • Knifej是使用knife4j-spring-boot-starter的风格来编写的,可以将配置项写在配置文件中,这些配置项提供了许多增强功能,可以更好的整合springboot、springcloud;

  • Knifej执行更新,为了更平滑的演进,而swagger-bootstrap-ui已停更;

 1.5、SpringDoc(可选)

SpringDoc也是 Spring 社区维护的一个项目(非官方),帮助使用者将 swagger3 集成到 Spring 中。也是用来在 Spring 中帮助开发者生成文档,并可以轻松的在SpringBoot中使用。SpringDoc基于swagger,并做了更多的对Spring系列各框架的兼容,用法上与Swagger3基本相同,并多了一些自己的配置,相较于Swagger3来说更好用,支持也更好一点。

从 spring-fox 迁移到 springdoc,需要依赖变更:pom.xml 里去掉 springfox 或者 swagger 的依赖,并添加springdoc-openapi-ui

  1.    <dependency>
  2.       <groupId>org.springdoc</groupId>
  3.       <artifactId>springdoc-openapi-ui</artifactId>
  4.       <version>1.3.1</version>
  5.    </dependency>

SpringDoc使用 swagger3 注解代替 swagger2 的:

这一步是可选的,因为改动太大,故 springfox对旧版的 swagger做了兼容处理。但不知道未来会不会不兼容,这里列出如何用 swagger 3 的注解代替 swagger 2 的,注意修改 swagger 3 注解的包路径为io.swagger.v3.oas.annotations。

Swagger2 的注解命名以易用性切入,全是 Api 开头,在培养出使用者依赖注解的习惯后,Swagger 3将注解名称规范化和工程化:

如果感兴趣,可以参考:SpringBoot结合SpringDo


二、Knife4j代替springfox-boot-starter实现Swagger3

Swagger用来自动生成API接口文档,还可以在线调试;而knife4j是对Swagger进一步封装,其优化了API文档的UI界面。Knife4j的前身是swagger-bootstrap-ui取名knife4j是希望她能像一把匕首一样小巧轻量并且功能强悍,希望把她做成一个为Swagger接口文档服务的通用性解决方案,不仅仅只是专注于UI前端。

2.1、导入Knife4j依赖

  •  SpringFox是对Swagger的SpringMVC的支持融合,Knife4是对Swagger进一步封装,其优化了api文档的界面

  •  Knife4j在更名之前,原来的名称是叫swagger-bootstrap-ui。Knife4j底层依赖springfox,因此无需再单独引入springfox的具体版本

  1. <dependency>
  2. <groupId>com.github.xiaoymin</groupId>
  3. <artifactId>knife4j-spring-boot-starter</artifactId>
  4. <version>3.0.3</version>
  5. </dependency>

2.2、创建Knife4jConfig配置类

  1. import com.hs.notice.entity.EmailNotice;
  2. import com.hs.notice.entity.SmsNotice;
  3. import com.hs.notice.entity.WeChatNotice;
  4. import com.fasterxml.classmate.TypeResolver;
  5. import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.context.annotation.Bean;
  8. import org.springframework.context.annotation.Configuration;
  9. import org.springframework.core.env.Environment;
  10. import org.springframework.core.env.Profiles;
  11. import springfox.documentation.builders.ApiInfoBuilder;
  12. import springfox.documentation.builders.PathSelectors;
  13. import springfox.documentation.builders.RequestHandlerSelectors;
  14. import springfox.documentation.service.ApiInfo;
  15. import springfox.documentation.service.Contact;
  16. import springfox.documentation.spi.DocumentationType;
  17. import springfox.documentation.spring.web.plugins.Docket;
  18. import springfox.documentation.swagger2.annotations.EnableSwagger2;
  19. /**
  20. * 作用: 自动生成API文档和在线接口调试工具
  21. */
  22. @Configuration
  23. //该注解是Springfox-swagger框架提供的使用Swagger注解,该注解必须加
  24. @EnableSwagger2
  25. //knife4j提供的增强扫描注解,Ui提供了例如动态参数、参数过滤、接口排序等增强功能
  26. @EnableKnife4j
  27. public class Knife4jConfig {
  28. /**
  29. * 创建一个Docket的对象,相当于是swagger的一个实例 : 配置开发和测试环境下开启Swagger,生产发布时关闭
  30. *
  31. * RequestHandlerSelectors,配置要扫描接口的方式
  32. * basePackage:指定扫描的包路径
  33. * any:扫描全部
  34. * none:全部不扫描
  35. * withClassAnnotation:扫描类上的注解,如RestController
  36. * withMethodAnnotation:扫描方法上的注解,如GetMapping
  37. *
  38. * @return
  39. */
  40. @Autowired
  41. TypeResolver typeResolver;
  42. @Bean
  43. public Docket createRestApi(Environment environment)
  44. {
  45. //设置显示的swagger环境信息,判断是否处在自己设定的环境当中,为了安全生产环境不开放Swagger
  46. Profiles profiles=Profiles.of("dev","test");
  47. boolean flag=environment.acceptsProfiles(profiles);
  48. //创建一个Docket的对象,相当于是swagger的一个实例
  49. return new Docket(DocumentationType.SWAGGER_2)
  50. .useDefaultResponseMessages(false)
  51. .groupName("1.x版本")
  52. .apiInfo(apiInfo())
  53. //只有当springboot配置文件为dev或test环境时,才开启swaggerAPI文档功能
  54. .enable(flag)
  55. .select()
  56. // 这里指定Controller扫描包路径:设置要扫描的接口类,一般是Controller类
  57. .apis(RequestHandlerSelectors.basePackage("com.hs.notice.controller")) //这里采用包扫描的方式来确定要显示的接口
  58. // .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class)) //这里采用包含注解的方式来确定要显示的接口
  59. // 配置过滤哪些,设置对应的路径才获取
  60. .paths(PathSelectors.any())
  61. .build()
  62. //防止Controller中参数中没有实体类或者返回值不是实体类导致Swagger Models页面扫描不到的情况
  63. .additionalModels(typeResolver.resolve(EmailNotice.class))
  64. .additionalModels(typeResolver.resolve(SmsNotice.class))
  65. .additionalModels(typeResolver.resolve(WeChatNotice.class));
  66. }
  67. ///配置相关的api信息
  68. private ApiInfo apiInfo()
  69. {
  70. return new ApiInfoBuilder()
  71. .description("API调试文档")
  72. //作者信息
  73. .contact(new Contact("何哥", "http://ip地址:8086/doc.html", "110@qq.com"))
  74. .version("v1.0")
  75. .title("消息通知服务API文档")
  76. //服务Url
  77. .termsOfServiceUrl("")
  78. .build();
  79. }
  80. }

如上代码所示,通过@Configuration注解,让Spring来加载该类配置。

  • @ EnableSwagger2支持Swagger 2的SpringFox支持。
  • DocumentationType.SWAGGER_2告诉Docketbean我们正在使用Swagger规范的版本2。

  • apiInfo()用来创建该Api的基本信息(这些基本信息会展现在文档页面中)。

  • select()创建一个构建器,用于定义哪些控制器及其生成的文档中应包含哪些方法。

  • apis()定义要包含的类(控制器和模型类)。这里我们包括所有这些,但您可以通过基础包,类注释等来限制它们。SpringFox会将其检测为文档生成源。Controller和Model类。您可以在Docket配置中轻松配置它。我们可以使用.apis(RequestHandlerSelectors.any(来包含所有类;当然我们也可以缩小到我们的基础包。

  • paths()允许您根据路径映射定义应包含哪个控制器的方法。我们现在包括所有这些,但您可以使用正则表达式等限制它。上面的代码.paths(PathSelectors.any())是代表匹配所有URL。

2.3、创建User实体类

  1. import io.swagger.annotations.ApiModel;
  2. import io.swagger.annotations.ApiModelProperty;
  3. import lombok.Data;
  4. @Data
  5. @ApiModel(value = "用户实体")
  6. public class User {
  7.     @ApiModelProperty(value = "id")
  8.     private Integer id;
  9.     @ApiModelProperty(value = "用户名")
  10.     private  String username;
  11.     @ApiModelProperty(value = "性别,0男,1女")
  12.     private Integer sex;
  13. }

 2.4、创建UserController接口

  1. package com.example.demo.controller;
  2. import com.example.demo.User;
  3. import io.swagger.annotations.*;
  4. import org.springframework.web.bind.annotation.*;
  5. import springfox.documentation.annotations.ApiIgnore;
  6. @RestController
  7. @Api(tags = "用户接口")//描述UserController的信息
  8. public class UserController {
  9. @ApiOperation(value = "查询用户",notes = "根据id查询用户")
  10. @ApiImplicitParam(paramType = "path",name="id",value = "用户id",required = true)
  11. @GetMapping("/user/query/{id}")
  12. public String getUserById(@PathVariable Integer id) {
  13. return "/user/"+id;
  14. }
  15. @ApiResponses({
  16. @ApiResponse(code=200,message="删除成功"),
  17. @ApiResponse(code=500,message="删除失败")})
  18. @ApiOperation(value = "删除用户",notes = "根据id删除用户")
  19. @DeleteMapping("/user/delete/{id}")
  20. public Integer deleteUserById(@PathVariable Integer id) {
  21. return id;
  22. }
  23. @ApiOperation(value = "添加用户",notes = "添加一个用户,传入用户名和性别")
  24. @ApiImplicitParams({
  25. @ApiImplicitParam(paramType = "query",name="username",value = "用户名",required = true,defaultValue = "张三"),
  26. @ApiImplicitParam(paramType = "query",name="sex",value = "性别",required = true,defaultValue = "女")
  27. })
  28. @PostMapping("/user")
  29. public String addUser(@RequestParam String username,@RequestParam String sex){
  30. return username+","+sex;
  31. }
  32. @ApiOperation(value="修改用户",notes = "根据传入的用户信息修改用户")
  33. @PutMapping("/user")
  34. public String updateUser(@RequestBody User user){
  35. return user.toString();
  36. }
  37. @GetMapping("/ignore")
  38. @ApiIgnore
  39. public void ignoreMethod(){}
  40. }

2.5、启动项目,访问Swagger文档

启动项目,在浏览器输入http://localhost:8080/doc.html就可以看到接口的信息,展开接口,就能看到所有的接口详细信息。

展开后可以对各个请求进行测试。选择接口后点击调试,输入相关的参数点击发送按钮即可。 

 页面简单清爽 ,可以导出接口文档非常方便,赶紧来试试吧


三、Swagger常用注解

@Api:用在请求的类上,表示对类的说明
    tags="说明该类的作用,可以在UI界面上看到的注解"
    value="该参数没什么意义,在UI界面上也看到,所以不需要配置"
 
@ApiOperation:用在请求的方法上,说明方法的用途、作用
    value="说明方法的用途、作用"
    notes="方法的备注说明"
 
@ApiImplicitParams:用在请求的方法上,表示一组参数说明
    @ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
        name:参数名
        value:参数的汉字说明、解释
        required:参数是否必须传
        paramType:参数放在哪个地方
            · header --> 请求参数的获取:@RequestHeader
            · query --> 请求参数的获取:@RequestParam
            · path(用于restful接口)--> 请求参数的获取:@PathVariable
            · body(不常用)
            · form(不常用)    
        dataType:参数类型,默认String,其它值dataType="Integer"       
        defaultValue:参数的默认值
 
@ApiResponses:用在请求的方法上,表示一组响应
    @ApiResponse:用在@ApiResponses中,一般用于表达一个错误的响应信息
        code:数字,例如400
        message:信息,例如"请求参数没填好"
        response:抛出异常的类
 
@ApiModel:用于响应类上,表示一个返回响应数据的信息
            (这种一般用在post创建的时候,使用@RequestBody这样的场景,
            请求参数无法使用@ApiImplicitParam注解进行描述的时候)
    @ApiModelProperty:用在属性上,描述响应类的属性

3.1、Model类使用

  1. @Data
  2. @ApiModel(value="BizComponent对象", description="组件")
  3. public class BizComponent implements Serializable {
  4. private static final long serialVersionUID = 1L;
  5. @TableId(value = "id", type = IdType.AUTO)
  6. private Long id;
  7. @ApiModelProperty(value = "分类")
  8. private Long categoryId;
  9. @ApiModelProperty(value = "组件名称")
  10. private String name;
  11. @ApiModelProperty(value = "组件描述")
  12. private String description;
  13. @ApiModelProperty(value = "日期字段")
  14. private LocalDateTime componentTime;
  15. @ApiModelProperty(value = "创建时间")
  16. @TableField(fill = FieldFill.INSERT)
  17. private LocalDateTime createTime;
  18. @ApiModelProperty(value = "修改时间")
  19. @TableField(fill = FieldFill.INSERT_UPDATE)
  20. private LocalDateTime modifiedTime;
  21. }

3.2、Control类和接口方法使用

  1. @RestController
  2. @RequestMapping("/api/category")
  3. @Api(value = "/category", tags = "组件分类")
  4. public class BizCategoryController {
  5. private IBizCategoryService bizCategoryService;
  6. @GetMapping("/list")
  7. @ApiOperation(value = "列表", notes = "分页列表")
  8. public R<PageModel<BizCategory>> list(PageQuery pageQuery,
  9. @RequestParam @ApiParam("组件分类名称") String name) {
  10. IPage<BizCategory> page = bizCategoryService.page(pageQuery.loadPage(),
  11. new LambdaQueryWrapper<BizCategory>().like(BizCategory::getName, name));
  12. return R.success(page);
  13. }
  14. @GetMapping("/{categoryId}")
  15. @ApiOperation(value = "详情", notes = "组件分类详情")
  16. @ApiResponses({
  17. @ApiResponse(code=200,message="请求成功"),
  18. @ApiResponse(code=100,message="请求失败")})
  19. public R<BizCategory> detail(@PathVariable @ApiParam("分类Id") Long categoryId) {
  20. BizCategory category = bizCategoryService.getById(categoryId);
  21. return R.success(category);
  22. }
  23. @PostMapping("/save")
  24. @ApiOperation(value = "保存", notes = "新增或修改")
  25. @ApiImplicitParams({
  26. @ApiImplicitParam(paramType = "form", name = "categoryId", value = "组件id(修改时为必填)"),
  27. @ApiImplicitParam(paramType = "form", name = "name", value = "组件分类名称", required = true)
  28. })
  29. public R<BizCategory> save(Long categoryId, String name) {
  30. BizCategory category = new BizCategory();
  31. category.setId(categoryId);
  32. category.setName(name);
  33. bizCategoryService.saveOrUpdate(category);
  34. return R.success(category);
  35. }
  36. @DeleteMapping("/{categoryId}")
  37. @ApiOperation(value = "删除", notes = "删除")
  38. public R delete(@PathVariable @ApiParam("分类Id") Long categoryId) {
  39. bizCategoryService.delete(categoryId);
  40. return R.success();
  41. }
  42. }

四、企业项目中比较实用的用法

4.1、忽略不想生成文档的接口

某些Controller 不需要生成API文档的接口,可以通过@ApiIgnore忽略掉

 4.2、开发环境开启Swagger ,生产环境关闭

虽然说swagger是个好东西,但是使用中切不可以忽略的一个安全问题。dev环境中你可以开放swagger给前端或者测试,但如果你的swagger ui不小心放到了生产,那是一件多么可怕的事情,真可以来个一锅端,切记切记。

  1. @Value("${swagger.switch}")
  2. private boolean swaggerSwitch;
  3. @Bean
  4. public Docket api() {
  5. Docket docket = new Docket(DocumentationType.SWAGGER_2);
  6. if (swaggerSwitch) {
  7. docket.enable(true);
  8. } else {
  9. docket.enable(false);
  10. }
  11. docket.apiInfo(apiInfo()).select().paths(PathSelectors.any()).build();
  12. return docket;
  13. }

4.3、Swagger UI中 model实体类不显示解决方案

方式一:只要在接口中,返回值中存在实体类,就会被扫描到swagger中

  1.  @PostMapping("/account")
  2.     public Account user(){
  3.         return new Account();
  4.     }

方式二:在controller 使用 @RequestBody 注解

Swagger的model里面之所以没有你需要的model,是因为你的model没有被swagger发现,我们利用Spring的注解让这个类被发现就行

  1.   @ApiOperation(value = "添加用户", notes = "添加新用户")
  2.     @PostMapping("/add")
  3.     public Map addUser( @RequestBody Account account){
  4.         return userService.createUser(account);
  5.     }

方式三:在Swagger的配置Bean中,手动的添加你想呈现的类,如下最后三行,可以写很多个typeResolver.resolve(XXX.class)作为参数传入

  1. import com.fasterxml.classmate.TypeResolver;
  2. @Autowired
  3. TypeResolver typeResolver;
  4. @Bean
  5. public Docket createRestApi(Environment environment)
  6. {
  7. return new Docket(DocumentationType.SWAGGER_2)
  8. .useDefaultResponseMessages(false)
  9. .groupName("1.x版本")
  10. .apiInfo(apiInfo())
  11. .enable(true)
  12. .select()
  13. .apis(RequestHandlerSelectors.basePackage("com.hs.notice.controller"))
  14. .paths(PathSelectors.any())
  15. .build()
  16. //防止Controller中参数中没有实体类或者返回值不是实体类导致Swagger Models页面扫描不到的情况
  17. .additionalModels(typeResolver.resolve(EmailNotice.class))
  18. .additionalModels(typeResolver.resolve(SmsNotice.class))
  19. .additionalModels(typeResolver.resolve(WeChatNotice.class));
  20. }

4.4、Knife4j文档请求异常

 报错信息如下:

  1. java.lang.NullPointerException: null
  2.     at springfox.documentation.swagger2.mappers.RequestParameterMapper.bodyParameter(RequestParameterMapper.java:264) ~[springfox-swagger2-3.0.0.jar:3.0.0]

错误原因:

极有可能就是,给参数变量重命名了,导致Controller入参和API写的参数对应不上

比如下面代码中的Api提示参数的name = "errorDeviceState",但实际参数写的却是 errorDeviceStateEnum

  1. @ApiOperation("查看单个完整巡检报告")
  2. @GetMapping
  3. @ApiImplicitParams({
  4. @ApiImplicitParam(name = "inspectId", value = "巡检任务ID", dataTypeClass = String.class),
  5. @ApiImplicitParam(name = "errorDeviceState", value = "设备巡检状态(1异常,2离线)", dataTypeClass = ErrorDeviceStateEnum.class)
  6. })
  7. public WebResult<ReportVO> queryDetail(@RequestParam String inspectId, @RequestParam ErrorDeviceStateEnum errorDeviceStateEnum) {
  8. // code ...
  9. }

4.5、Swagger文档中的调试界面请求参数丢失

问题描述:请求参数本来应该是x-www-form-urlencoded方式的,却变成了raw方式,丢失content参数

  解决方案:丢失参数content前面加上个@RequestParam注解就可以了

sendMessage(String openid,@RequestParam @Length(max=1024) String content,String appId,String t_sign,String requestId)

4.6、本地swagger使用localhost/doc.html不能访问

localhost/doc.html换为127.0.0.1/doc.html


参考链接:

knife4j官网

SpringBoot整合knife4j

swagger2 注解说明 ( @ApiImplicitParams )

knife4j及一些api注解说明

SpringBoot项目整合knife4j 2.0总结

SpringBoot集成Swagger3.0

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/656310
推荐阅读
相关标签
  

闽ICP备14008679号