• 【wiki知识库】02.wiki知识库SpringBoot后端的准备


      📝个人主页:哈__

    期待您的关注 

    目录

    一、🔥今日目标

    二、📂打开SpringBoot项目 

    2.1 导入所需依赖

    2.2修改application.yml配置文件

     2.3导入MybatisPlus逆向工程工具

     2.4创建一个公用的返回值

     2.5创建CopyUtil工具类

     2.6创建MybatisPlus的配置类

    三、使用MybatisPlus逆向工程生成Ebook

    四、Ebook查询功能的开发


    一、🔥今日目标

    上一篇文章已经带领大家把前后端的SpringBoot和Vue的架子搭了起来,今天呢我就要带大家开始上手开发我们的wiki知识库了,今天主要是带领大家把后端中一些基本的东西写出来,例如依赖、部分的工具类等,还会带大家实现电子书模块的查询功能,但是通过PostMan进行地测试的。

    二、📂打开SpringBoot项目 

    2.1 导入所需依赖

    这里大家可能使用的不是阿里云服务器创建的SpringBoot项目,所以我把整个的POM文件复制了上来。依赖呢我现在只用到了这么多,如果后边我们缺了什么依赖的话还会在加的。

    1. "1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    4. <modelVersion>4.0.0modelVersion>
    5. <groupId>com.mygroupId>
    6. <artifactId>hawikiartifactId>
    7. <version>0.0.1-SNAPSHOTversion>
    8. <name>hawikiname>
    9. <description>hawikidescription>
    10. <properties>
    11. <java.version>1.8java.version>
    12. <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    13. <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
    14. <spring-boot.version>2.7.16spring-boot.version>
    15. properties>
    16. <dependencies>
    17. <dependency>
    18. <groupId>org.springframework.bootgroupId>
    19. <artifactId>spring-boot-starterartifactId>
    20. dependency>
    21. <dependency>
    22. <groupId>org.springframework.bootgroupId>
    23. <artifactId>spring-boot-starter-testartifactId>
    24. <scope>testscope>
    25. dependency>
    26. <dependency>
    27. <groupId>org.springframework.bootgroupId>
    28. <artifactId>spring-boot-starter-aopartifactId>
    29. dependency>
    30. <dependency>
    31. <groupId>org.springframework.bootgroupId>
    32. <artifactId>spring-boot-starter-data-redisartifactId>
    33. dependency>
    34. <dependency>
    35. <groupId>org.springframework.bootgroupId>
    36. <artifactId>spring-boot-starter-validationartifactId>
    37. dependency>
    38. <dependency>
    39. <groupId>org.springframework.bootgroupId>
    40. <artifactId>spring-boot-starter-websocketartifactId>
    41. dependency>
    42. <dependency>
    43. <groupId>com.baomidougroupId>
    44. <artifactId>mybatis-plus-boot-starterartifactId>
    45. <version>3.4.3.1version>
    46. dependency>
    47. <dependency>
    48. <groupId>com.baomidougroupId>
    49. <artifactId>mybatis-plus-generatorartifactId>
    50. <version>3.3.2version>
    51. dependency>
    52. <dependency>
    53. <groupId>org.apache.velocitygroupId>
    54. <artifactId>velocityartifactId>
    55. <version>1.7version>
    56. dependency>
    57. <dependency>
    58. <groupId>com.mysqlgroupId>
    59. <artifactId>mysql-connector-jartifactId>
    60. dependency>
    61. <dependency>
    62. <groupId>com.alibabagroupId>
    63. <artifactId>fastjsonartifactId>
    64. <version>1.2.70version>
    65. dependency>
    66. <dependency>
    67. <groupId>cn.hutoolgroupId>
    68. <artifactId>hutool-allartifactId>
    69. <version>5.8.18version>
    70. dependency>
    71. <dependency>
    72. <groupId>org.projectlombokgroupId>
    73. <artifactId>lombokartifactId>
    74. dependency>
    75. <dependency>
    76. <groupId>ch.qos.logbackgroupId>
    77. <artifactId>logback-classicartifactId>
    78. dependency>
    79. dependencies>
    80. <dependencyManagement>
    81. <dependencies>
    82. <dependency>
    83. <groupId>org.springframework.bootgroupId>
    84. <artifactId>spring-boot-dependenciesartifactId>
    85. <version>${spring-boot.version}version>
    86. <type>pomtype>
    87. <scope>importscope>
    88. dependency>
    89. dependencies>
    90. dependencyManagement>
    91. <build>
    92. <plugins>
    93. <plugin>
    94. <groupId>org.apache.maven.pluginsgroupId>
    95. <artifactId>maven-compiler-pluginartifactId>
    96. <version>3.8.1version>
    97. <configuration>
    98. <source>1.8source>
    99. <target>1.8target>
    100. <encoding>UTF-8encoding>
    101. configuration>
    102. plugin>
    103. <plugin>
    104. <groupId>org.springframework.bootgroupId>
    105. <artifactId>spring-boot-maven-pluginartifactId>
    106. <version>${spring-boot.version}version>
    107. <configuration>
    108. <mainClass>com.my.hawiki.HawikiApplicationmainClass>
    109. <skip>trueskip>
    110. configuration>
    111. <executions>
    112. <execution>
    113. <id>repackageid>
    114. <goals>
    115. <goal>repackagegoal>
    116. goals>
    117. execution>
    118. executions>
    119. plugin>
    120. plugins>
    121. build>
    122. project>

    2.2修改application.yml配置文件

    在application.yml配置文件中,我们要配置数据库的连接信息,还要配置redis的连接信息

    1. spring:
    2. datasource:
    3. driver-class-name: com.mysql.cj.jdbc.Driver
    4. url: jdbc:mysql://localhost:3306/wiki?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT&allowPublicKeyRetrieval=true
    5. type: com.zaxxer.hikari.HikariDataSource
    6. username: root
    7. password: 你的数据库密码
    8. logging:
    9. config: classpath:logback-spring.xml
    10. level:
    11. com.my.hawiki.mapper: trace

     在上方的配置信息你看到了一个名字叫做logging的配置,这个是用来配置我们的日志输出的,日志信息的配置类使用的是一个xml文件。内容具体如下。首先你要在你的项目的根目录下创建一个名字叫做log的文件夹,这样以后的日志信息就会保存到这个文件夹下的两个文件中,一个叫做error.log,一个叫做trace.log。

    1. "1.0" encoding="UTF-8"?>
    2. <configuration>
    3. <property name="PATH" value="./log">property>
    4. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    5. <encoder>
    6. <Pattern>%d{ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID})
    7. %msg%n
    8. Pattern>
    9. encoder>
    10. appender>
    11. <appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    12. <file>${PATH}/trace.logfile>
    13. <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    14. <FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.logFileNamePattern>
    15. <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
    16. <maxFileSize>10MBmaxFileSize>
    17. timeBasedFileNamingAndTriggeringPolicy>
    18. rollingPolicy>
    19. <layout>
    20. <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%npattern>
    21. layout>
    22. appender>
    23. <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    24. <file>${PATH}/error.logfile>
    25. <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    26. <FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.logFileNamePattern>
    27. <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
    28. <maxFileSize>10MBmaxFileSize>
    29. timeBasedFileNamingAndTriggeringPolicy>
    30. rollingPolicy>
    31. <layout>
    32. <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%npattern>
    33. layout>
    34. <filter class="ch.qos.logback.classic.filter.LevelFilter">
    35. <level>ERRORlevel>
    36. <onMatch>ACCEPTonMatch>
    37. <onMismatch>DENYonMismatch>
    38. filter>
    39. appender>
    40. <root level="ERROR">
    41. <appender-ref ref="ERROR_FILE"/>
    42. root>
    43. <root level="TRACE">
    44. <appender-ref ref="TRACE_FILE"/>
    45. root>
    46. <root level="INFO">
    47. <appender-ref ref="STDOUT"/>
    48. root>
    49. configuration>

     2.3导入MybatisPlus逆向工程工具

    我在项目中创建了一个名字为util的软件包。别的包大家可以先不用管,这些我之后都会说到的,这个util包呢主要就是保存我们的一些工具类。

    MybatisPlus逆向工程的代码如下。我另一篇文章也有些过这个逆向工程【Spring】SpringBoot整合MybatisPlusGernerator,MybatisPlus逆向工程-CSDN博客

    要注意的是你一定要修改其中的数据库连接信息。

    1. public class MybatisGenerator {
    2. public static void main(String[] args) {
    3. AutoGenerator autoGenerator = new AutoGenerator();
    4. DataSourceConfig dataSourceConfig = new DataSourceConfig();
    5. dataSourceConfig.setDbType(DbType.MYSQL);
    6. dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
    7. dataSourceConfig.setUsername("root");
    8. dataSourceConfig.setPassword("你的数据库课密码");
    9. dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/wiki?useUnicode=true&characterEncoding=UTF-8");
    10. autoGenerator.setDataSource(dataSourceConfig);
    11. GlobalConfig globalConfig = new GlobalConfig();
    12. globalConfig.setOpen(false);
    13. globalConfig.setOutputDir(System.getProperty("user.dir")+"/src/main/java");
    14. globalConfig.setAuthor("CSDN__哈");
    15. globalConfig.setServiceName("%sService");
    16. autoGenerator.setGlobalConfig(globalConfig);
    17. PackageConfig packageConfig = new PackageConfig();
    18. packageConfig.setParent("com.my.hawiki");
    19. packageConfig.setEntity("domain");
    20. packageConfig.setMapper("mapper");
    21. //packageConfig.setController("controller");
    22. packageConfig.setService("service");
    23. packageConfig.setServiceImpl("service.impl");
    24. autoGenerator.setPackageInfo(packageConfig);
    25. StrategyConfig strategyConfig = new StrategyConfig();
    26. //是否需要lombok
    27. strategyConfig.setEntityLombokModel(true);
    28. //设置命名格式
    29. strategyConfig.setNaming(NamingStrategy.underline_to_camel);
    30. strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel);
    31. //设置我们的表名 你到底要生成哪张表的框架
    32. strategyConfig.setInclude("ebook");
    33. autoGenerator.setStrategy(strategyConfig);
    34. autoGenerator.execute();
    35. }
    36. }

     2.4创建一个公用的返回值

    你可能不太理解这句话是什么意思,在SpringBoot项目当中,我们的每一个请求可能都会返回给前端数据,返回的数据一定是一个字符串。但这种方式并不适合我们的需求,比如我想返回的信息不仅仅是数据,还有一些其他的状态信息。例如当我们程序没出错的时候,我想在返回一句话,查询成功,如果出错了我就返回查询失败,这样返回多个参数信息,我们就不能使用字符串返回了。

    我们需要的是这样的返回值。这是一个JSON类型的数据,也可以说是一个对象类型的数据,我们直接返回一个对象的话也可以达到这种效果,前提是导入了fastjson依赖。

    创建的CommonResp如下。这个类在util包下创建的我。

    1. @Data
    2. @AllArgsConstructor
    3. public class CommonResp {
    4. /**
    5. * 业务上的成功或失败
    6. */
    7. private boolean success = true;
    8. /**
    9. * 返回信息
    10. */
    11. private String message;
    12. /**
    13. * 返回泛型数据,自定义类型
    14. */
    15. private T content;
    16. @Override
    17. public String toString() {
    18. final StringBuffer sb = new StringBuffer("ResponseDto{");
    19. sb.append("success=").append(success);
    20. sb.append(", message='").append(message).append('\'');
    21. sb.append(", content=").append(content);
    22. sb.append('}');
    23. return sb.toString();
    24. }
    25. }

     2.5创建CopyUtil工具类

    这个工具类的作用就是把一个类转换为另一个类的形式。也是在util包下。

    1. import org.springframework.beans.BeanUtils;
    2. import org.springframework.util.CollectionUtils;
    3. import java.util.ArrayList;
    4. import java.util.List;
    5. public class CopyUtil {
    6. /**
    7. * 单体复制
    8. */
    9. public static T copy(Object source, Class clazz) {
    10. if (source == null) {
    11. return null;
    12. }
    13. T obj = null;
    14. try {
    15. obj = clazz.newInstance();
    16. } catch (Exception e) {
    17. e.printStackTrace();
    18. return null;
    19. }
    20. BeanUtils.copyProperties(source, obj);
    21. return obj;
    22. }
    23. /**
    24. * 列表复制
    25. */
    26. public static List copyList(List source, Class clazz) {
    27. List target = new ArrayList<>();
    28. if (!CollectionUtils.isEmpty(source)) {
    29. for (Object c : source) {
    30. T obj = copy(c, clazz);
    31. target.add(obj);
    32. }
    33. }
    34. return target;
    35. }
    36. }

     2.6创建MybatisPlus的配置类

    这个类写到了我创建出来的config包下

    1. @Configuration
    2. public class MyBatisPlusConfig {
    3. @Bean
    4. public PaginationInterceptor paginationInterceptor() {
    5. PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
    6. paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(false));
    7. return paginationInterceptor;
    8. }
    9. }

     到了这一步,我们今天的目标就完成的快差不多了,后端的工具类我们今天用到的也差不多就是这些。接下来我就带大家实现电子书接口的查询功能。

     这是我们的ebook的数据库,接下来我就要写接口去查询这些内容了。

    三、🌼使用MybatisPlus逆向工程生成Ebook

     打开我给大家的逆向工程的工具类,把你要生成的表的名称填写到对应的位置,上边我已经给了注释了,这里不再展示到底写在哪里了,把表名改成ebook,然后右键运行工具类,等一段时间后就会生成以下几个结构。

    • controller
    • service
      • impl
    • domain
    • mapper
      • xml

    四、🤖Ebook查询功能的开发

    我们需要的都已经写完了,接下来就去实现具体的功能,在这之前呢,我还需要写两个Param类,这两个类的作用就是接收前端给后端传输的一些参数。


    PageParam

    1. @Data
    2. public class PageParam {
    3. @NotNull(message = "【页码】不能为空")
    4. private Integer page;
    5. @NotNull(message = "【每页条数】不能为空")
    6. @Max(value = 1000, message = "【每页条数】不能超过1000")
    7. private Integer size;
    8. }

    EbookQueryParam

    1. @EqualsAndHashCode(callSuper = true)
    2. @Data
    3. public class EbookQueryParam extends PageParam {
    4. private Long id;
    5. private String name;
    6. private Long categoryId2;
    7. }

    除了这两个Param外还需要两个VO,VO的作用就是把我们的查询结果进行一个二次封装,然后在传给前端,比如说你开发登录功能,用户成功登陆后,你不能把用户的密码直接返回给前端,而是要把这个密码的字段删掉在返回给前端。


    PageVo

    1. @Data
    2. public class PageVo {
    3. private long total;
    4. private List list;
    5. }

    EbookQueryVo 

    1. @Data
    2. public class EbookQueryVo {
    3. private Long id;
    4. private String name;
    5. private Long category1Id;
    6. private Long category2Id;
    7. private String description;
    8. private String cover;
    9. private Integer docCount;
    10. private Integer viewCount;
    11. private Integer voteCount;
    12. }

    打开我们的EbookController,写入以下代码。

    1. @RestController
    2. @RequestMapping("/ebook")
    3. public class EbookController {
    4. @Resource
    5. EbookService ebookService;
    6. /**
    7. * 查询电子书 带有模糊查询
    8. * @param ebookQueryParam 带有数据校验
    9. * @return
    10. */
    11. @RequestMapping("/list")
    12. public CommonResp list(@Validated EbookQueryParam ebookQueryParam){
    13. PageVo list = ebookService.list(ebookQueryParam);
    14. return new CommonResp<>(true,"查找成功",list);
    15. }
    16. }

    EbookService代码如下。

    1. public interface EbookService extends IService {
    2. PageVo list(EbookQueryParam ebookQueryParam);
    3. }

     EbookMapper代码如下。

    1. /**
    2. *

    3. * 电子书 Mapper 接口
    4. *

    5. *
    6. * @author CSDN__哈
    7. * @since 2024-05-26
    8. */
    9. public interface EbookMapper extends BaseMapper {
    10. }

     EbookServiceImpl代码如下。

    1. package com.my.hawiki.service.impl;
    2. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    3. import com.baomidou.mybatisplus.core.toolkit.StringUtils;
    4. import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    5. import com.my.hawiki.domain.Ebook;
    6. import com.my.hawiki.mapper.EbookMapper;
    7. import com.my.hawiki.param.EbookQueryParam;
    8. import com.my.hawiki.service.EbookService;
    9. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    10. import com.my.hawiki.utils.CopyUtil;
    11. import com.my.hawiki.vo.EbookQueryVo;
    12. import com.my.hawiki.vo.PageVo;
    13. import org.springframework.stereotype.Service;
    14. import javax.annotation.Resource;
    15. import java.util.ArrayList;
    16. import java.util.List;
    17. /**
    18. *

    19. * 电子书 服务实现类
    20. *

    21. *
    22. * @author CSDN__哈
    23. * @since 2024-05-26
    24. */
    25. @Service
    26. public class EbookServiceImpl extends ServiceImpl implements EbookService {
    27. @Resource
    28. EbookMapper ebookMapper;
    29. @Override
    30. public PageVo list(EbookQueryParam ebookQueryParam) {
    31. // 这里创建了一个wrapper用于sql语句条件的拼接
    32. LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
    33. // 拼接我们传过来的id 就相当于 where id = ??
    34. lambdaQueryWrapper.eq(ebookQueryParam.getId()!=null,Ebook::getId,ebookQueryParam.getId())
    35. // 相当于 where categoryid2 = ??
    36. .eq(ebookQueryParam.getCategoryId2()!=null,Ebook::getCategory2Id,ebookQueryParam.getCategoryId2())
    37. // 相当于 where name like %???%
    38. .like(StringUtils.isNotBlank(ebookQueryParam.getName()),Ebook::getName,ebookQueryParam.getName());
    39. // 这里使用MybataisPlus的page类,接收一下前端传来了页号和页的大小
    40. Page page = new Page<>(ebookQueryParam.getPage(),ebookQueryParam.getSize());
    41. // 这里我们将从数据库中查询的结果封装到一个Page类下
    42. Page resultPage = ebookMapper.selectPage(page, lambdaQueryWrapper);
    43. // 这里我创建了一个PageVo,用于返回给前端信息
    44. PageVo pageVo = new PageVo<>();
    45. List list = new ArrayList<>();
    46. // 从resultPage中获取从数据库中去除的结果,然后把数据插入到list中
    47. for (Ebook record : resultPage.getRecords()) {
    48. EbookQueryVo ebookQueryVo = CopyUtil.copy(record, EbookQueryVo.class);
    49. list.add(ebookQueryVo);
    50. }
    51. pageVo.setList(list);
    52. pageVo.setTotal(resultPage.getSize());
    53. return pageVo;
    54. }
    55. }

    好了这里就写的差不多了,我们可以使用PostMan工具测试一下,这里大家需要自己安装一下。

    启动我们的启动类,并且在启动类上加上一个mapper扫描。之后启动项目

    @MapperScan("com.my.hawiki.mapper")

     可以看到我传入的page是1,size是5,查询出来的结果没问题。

    但大家别忘了,我还有一个数据校验呢,page和size不能为空。那我们什么也不传入试一试。现在直接查不出来了。

    我们看一下后台日志。这里给我们报了个错,说明我们的数据校验是有作用的,那我不想让他报错,这我该怎么做呢?我还想使用CommomResp返回一个信息,告诉前端错误是什么。

    1. 54.346 WARN o.s.w.s.m.s.DefaultHandlerExceptionResolver:208 http-nio-8080-exec-7
    2. Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errorsField error in object 'ebookQueryParam' on field 'page': rejected value [null]; codes [NotNull.ebookQueryParam.page,NotNull.page,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [ebookQueryParam.page,page]; arguments []; default message [page]]; default message [【页码】不能为空]Field error in object 'ebookQueryParam' on field 'size': rejected value [null]; codes [NotNull.ebookQueryParam.size,NotNull.size,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [ebookQueryParam.size,size]; arguments []; default message [size]]; default message [【每页条数】不能为空]]

     这里我就要创建一个handler包,包下创建一个GlobalExceptionHandler。

    GlobalExceptionHandler代码如下。这个类的作用就是捕获我们程序中的异常,然后作出处理。

    1. import com.my.hawiki.utils.CommonResp;
    2. import org.springframework.validation.BindException;
    3. import org.springframework.validation.FieldError;
    4. import org.springframework.web.bind.annotation.ExceptionHandler;
    5. import org.springframework.web.bind.annotation.RestControllerAdvice;
    6. @RestControllerAdvice
    7. public class GlobalExceptionHandler {
    8. /**
    9. * 校验异常
    10. */
    11. @ExceptionHandler(BindException.class)
    12. public CommonResp handleBindException(BindException e) {
    13. StringBuilder errorMessage = new StringBuilder();
    14. for (FieldError fieldError : e.getFieldErrors()) {
    15. // 这里可以自定义错误信息的格式
    16. errorMessage.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append("; ");
    17. }
    18. // 去掉最后一个分号和空格
    19. if (errorMessage.length() > 0) {
    20. errorMessage.setLength(errorMessage.length() - 2);
    21. }
    22. return new CommonResp<>(false, errorMessage.toString(), null);
    23. }
    24. }

    接下来在重新启动项目试试看。

    大功告成, 电子书查询接口的功能已经测试成功了。

  • 相关阅读:
    WRFDA资料同化实践技术应用
    无胁科技-TVD每日漏洞情报-2022-11-22
    TCP和UDP、TCP三次握手、TCP为什么要进行三次握手不是两次、TCP四次挥手、TCP和UDP的区别、TCP抓包分析、TCP什么时候三次挥手
    127. 单词接龙
    Spring Boot基础
    terraform简单的开始-安装和一些配置
    卷王杯 easy unserialize
    1-丁基-3-甲基咪唑氯化锌([BMIM][Zn2Cl5])离子液体
    Xcode14创建github远程仓库Token
    深入了解 Axios 的 put 请求:使用技巧与最佳实践
  • 原文地址:https://blog.csdn.net/qq_61024956/article/details/139224794