《学成在线》微服务实战项目实操笔记系列(P1~P62)【上】

news2024/9/25 7:13:49

《学成在线》项目实操笔记系列【上】,跟视频的每一P对应,全系列12万字,涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳,参考这篇,相信会带给你极大启发。同时也欢迎大家提问与讨论,我会尽力帮大家解答。

一、前期准备

1.1 项目介绍 P2

To C面向个人,B2B2C(第1个B是指商品或服务的供应商,第2个B是指从事电子商务的企业,C是消费者。例子:腾讯课堂,第1个B是腾讯公司,第2个B是入驻授课的企业,C是用户学习课程)

本项目含有3个端:用户端;机构端;运营端

1.2 说自己项目 P3

从以下几个方面进行项目介绍:

1.项目的背景,包括:是自研还是外包,什么业务,服务的客户群是谁,谁去运营等问题。

2.项目的业务流程(核心的业务流程)

3.项目的功能模块(核心模块一定要说)

4.项目的技术架构

5.个人工作职责(说得详细一些)

6.个人负责模块的详细说明,包括模块设计,用到的技术,技术的实现方案等(找最熟悉的模块进行说明)。

项目基本介绍:是公司自研的专门针对成人职业技能教育的网络课堂系统,网站提供了成人职业技能培训的相关课程,如:软件开发培训。基于B2B2C的业务模式。培训机构可以在平台入驻、发布课程,我们公司作为运营方由专门的人员对发布的课程进行审核,审核通过后课程才可以发布成功,课程包括免费和收费两种形式,对于免费课程用户可以直接选课学习,对于收费课程要在选课后支付成功才可以继续学习。

本项目包括3个端:用户端、机构端、运营端。

核心模块:内容管理、媒资管理、课程搜索、订单支付、选课管理、认证授权等。

本项目采用前后端分离架构,后端采用SpringBoot、SpringCloud技术栈开发,数据库使用了Mysql,还使用了Redis、消息队列、分布式文件系统、Elasticsearch等中间件系统(要清楚这些中间件在系统中是如何使用的,在哪里使用的)。

划分的微服务包括:内容管理服务、媒资管理服务、搜索服务、订单支付服务、学习中心服务、系统管理服务、认证授权服务、网关服务、注册中心服务、配置中心服务等。

我在这个项目中负责了内容管理、媒资管理、订单支付模块的设计与开发。

个人负责模块的详细说明:内容管理模块,是对平台上的课程进行管理。设计了课程基本信息表、课程营销表、课程计划、课程师资表。培训机构要发布一门课程需要填写课程基本信息、课程营销信息、课程计划信息、课程师资信息,填写完毕后要提交审核,由运营人员进行课程信息的审核,整个审核过程是程序自动审核加入人工确认的方式,通常24小时完成审核。课程审核通过即可发布课程,课程的相关信息会聚合到课程发布表中,这里不仅要将课程信息写到课程发布表还要将课程信息写到索引库、分布式文件系统中,所以这里存在分布式事务的问题,项目使用本地消息表加任务调度的方式去解决这里的分布式事务,保证数据的最终一致性。

1.3 技术架构 P5

业务:解决了什么问题,为用户提供了什么样的服务。

 

技术栈:

1.4 环境配置 P6

配置环境版本如下:

IDEA基本配置如下:

Maven配置:

先把maven解压,然后把maven仓库解压。

在maven的setting.xml文件中进行配置:

 

<mirror>
   <id>alimaven</id>
   <name>aliyun maven</name>
   <url>http://maven.aliyun.com/nexus/content/groups/public</url>
   <blocked>central</blocked>
</mirror>

在IDEA中进行配置:

虚拟机配置:

解压虚拟机

双击虚拟机

点击虚拟网络编辑器

把VMnet8的子网地址改为:192.168.101.0

虚拟机的用户名:root,密码:centos

虚拟机ip:192.168.101.65

启动docker

systemctl start docker

运行所有的软件: 

sh /data/soft/restart.sh

 查询docker容器:docker ps

docker ps

数据库配置:

我是先用Navicat连接上虚拟机中的mysql:

Git配置:

下载完Git然后配置到IDEA上:

搭建Gogs:

Gogs是一个轻量级的远程仓库,是在虚拟机里面的,本项目使用Gogs作为Git远程仓库。进入Gogs:

http://192.168.101.65:10880

账号:gogs,密码:gogs

关联远程仓库:

我这里项目已经创建好了,我想要关联远程仓库,就不按照视频的方法进行。

点击VCS-Create Git Repositoty,选择当前项目的文件夹作为仓库,全选当前项目中所有文件,输入文字,然后点击commit。就可以把项目上传到本地仓库。

  

然后点击向上的按钮,点击Define remote,会跳出一个弹窗,弹窗的地址填写下面网页中的地址:

我直接使用现成的仓库,复制下面的HTTP地址,粘贴到上面弹窗中,然后会有输入账号密码的弹窗填gogs的账号密码:

然后就会出现项目内容:

如果忘记gogs密码:

可以在用户设置处更改密码;也可以在管理面板,用户管理,编辑处让管理员指定密码:

 

如何还是提示密码错误,可以在IDEA中,把文件名改掉(改成不存在的,会提示输入密码),也可以选不保存忘记密码:

配置.gitignore文件

把下面文件夹中的内容复制到IDEA的.gitignore文件中。

选中.gitignore,先commit然后push到远程仓库。

关于分支:

老师每一天的授课内容都会创建一个分支。如果想看看第1天的代码,就可以切换到第1天的分支,然后该分支只会显示第1天的代码。

分支在Git中相当于一个独立的工作流,每个分支都可以有不同的提交历史和代码改动。

可以通过在Git界面右键分支,点击Checkout来切换分支。

 

1.5 创建工程 P7

父工程职责:把所有依赖的版本确定下来,模块的聚合作用。

基础工程:基础的代码。所有的微服务依赖于基础工程。

首先创建名为xuecheng-plus-project的工程,把src文件删掉,然后创建xuecheng-plus-parent的模块。

项目结构大概如下,然后把第1章里的pom.xml代码复制到父工程的pom.xml文件中:

 

首先用properties标签把所有依赖的版本确定,然后dependencyManagement标签来引入对应的依赖。

下面创建xuecheng-plus-base模块,这个模块和parent模块是并列关系。base模块里除了src和pom.xml文件外其它东西删掉,包括一些启动类和配置项都要删除,只留下基本结构。

 

所有模块都是直接或间接继承父模块,所以要把父工程的坐标(下面红框里)复制,然后粘贴到base模块的<parent>标签下

然后把下面文件中base模块pom.xml中的<dependencies>标签下的内容单独复制,粘贴替换base模块的pom.xml中的<dependencies>下的内容。

1.6 Git面试 P8

可以在Git面板看到所有commit提交到本地仓库的版本。小铅笔所在的就是当前的分支。

面试题1:Git代码冲突怎么处理?
冲突的原因:本地文件的版本浴目标分支中文件的版本不一致时,当存在同一行的内容不同时在进行合并时会出现冲突。

场景:多个分支向主分支合并时(A同事和B同事开发过程中对同一文件的同一行内容进行修改)。同一个分支下pull或push操作时。

在IDEA里commit是提交到本地仓库(是本地机上的一个目录)。push是把本地仓库提交到远程仓库。

可以通过图形界面修改

通过代码行修改方式如下:

然后要add将文件添加到暂存区,commit将文件提交,最后push提交到远程仓库。

面试题2:你是在哪个分支开发?

我们不是直接在主分支开发,由技术经理创建独立的开发分支,我们是在独立的开发分支中进行开发,最后由技术经理将开发分支合并到主分支(技术经理对代码进行审查最终合并到主分支)。

1.7 Maven面试 P9

面试题1:Maven指令的作用

mvn clean 清除target目录中的生成结果

mvn compile 编译源代码(生成target目录,然后会有classes文件)

mvn test 执行单元测试

mvn package 打包(打成的jar包会放在target目录)

mvn install 打包并把打好的包上传到本地仓库

mvn deploy 打包并把打好的包上传到远程仓库

面试题2:Maven依赖版本冲突怎么处理?

maven的依赖版本冲突一般是由于间接依赖导致一个jar包有多个不同的版本。比如:A依赖了B的1.0版本,C依赖了B的2.0版本,项目依赖A和C,从而间接依赖了B的1.0和2.0版本,此时B有两个版本引入到了项目中,可能会出现ClassNotFoundException和NoSuchMethodError等错误。

处理版本冲突可以使用以下方法:

1.使用exclusions排除依赖

比如:我们只依赖B的1.0版本,此时可以在依赖C时排除对B的依赖。

2.使用dependencyManagement锁定版本号

通常在父工程对依赖的版本同一管理

比如:我们只依赖B的1.0版本,此时可以在父工程中限定B的版本为1.0

1.8 数据库环境 P10

数据库用的是虚拟docker容器里的数据库。

数据库用户名:root,密码:mysql。

输入下面启动运行:

systemctl start docker
sh /data/soft/restart.sh

我是在Navicat中对虚拟机中MySQL的连接下新建了一个数据库: 

右键表,然后点击运行sql文件,选择下面的xcplus_content.sql这个文件打开,最后可以看到表都加载到了数据库中:

 

1.9 存储引擎及区别 P11

1.InnoDB(InnoDB用于事务处理,具有ACID事务支持等特性,如果要执行大量insert和update操作,应该选择这个):支持事务。使用的锁颗粒度默认为行级锁,可以支持更高的并发,也可以支持表锁。支持外键约束,外键约束降低了表的查询速度,增加了表之间的耦合度。

2.MyISAM(管理非事务表,提供高速存储和检索以及全文搜索能力):不提供事务支持。只支持表级锁。不支持外键。

3.memory:数据存储在内存中。

1.10 MySQL建表注意 P12

注意选择存储引擎,如果要支持事务需要选择InnoDB。

日期类型如果要记录时分秒选择datetime,只记录年月日使用date;固定长度字符选择char,不固定长度字符varchar(varchar比char节省空间但速度没有char快);对于内容介绍类的长广文本字段使用text或longtext类型;如果存储图片等二进制数据使用blob或longblob类型;对金额字段使用DECIMAL。

如果要存储text、blob字段建议单独建一张表,使用外键关联。

尽量不要定义外键,保证表的独立性,可以存在外键意义的字段。

注意字段的约束,如:非空、唯一、主键等。

1.11 (内容管理)创建工程 P15

 这一节具体可以参考day01/资料中的第2章讲义。

在xuecheng-plus-content下面创建xuecheng-plus-content-api、xuecheng-plus-content-model和xuecheng-plus-content-service这三个模块。

新建的目录结构如下:

 模块之间依赖关系如下:

xuecheng-plus-content-model依赖xuecheng-plus-base。

xuecheng-plus-content-service依赖xuecheng-plus-model。

xuecheng-plus-content-api依赖xuecheng-plus-content-service。因为看图,api依赖service也依赖model,但因为service依赖model,所以api如果依赖了service变相也依赖了model。

xuecheng-plus-content-model的pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xuecheng-plus-content</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>xuecheng-plus-content-model</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xuecheng-plus-base</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

xuecheng-plus-content-service的pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xuecheng-plus-content</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>xuecheng-plus-content-service</artifactId>
        <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xuecheng-plus-content-model</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
     </dependencies>
</project>

xuecheng-plus-content-api的pom.xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>xuecheng-plus-content</artifactId>
        <groupId>com.xuecheng</groupId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <artifactId>xuecheng-plus-content-api</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.xuecheng</groupId>
            <artifactId>xuecheng-plus-content-service</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

二、课程模块开发

2.1 课程查询 需求分析 P16

操作流程(一步一步要怎么操作)就是业务流程,然后弄清在操作流程中需要哪些数据。

2.2 课程查询 生成PO类 P17

具体的搭建步骤可以看资料中下面这个文件里的内容:

course_base文件是课程基本信息文件。

表结构如下。

首先把xuecheng-plus-generator这个代码生成器解压出来,然后放到xuecheng-plus-project这个总工程下面。

此时该模块还是灰色的,右键pom.xml然后点击Add as Maven Project,可以使其变成maven工程。

修改generator下的ContentCodeGenerator类:

修改下面数据的连接配置,像我的表叫content,可以去掉前面xc402:

先把java下面的content删掉,然后运行ContentCodeGenerator的main方法:

main方法执行完后,可以看到生成了content包,model下面有po类。把po下面的所有po类复制粘贴到xuecheng-plus-content-model下的po包中:

发现有一些报错,主要的原因是缺乏了依赖。

可以加入如下的依赖到model模块的pom.xml文件中:

<!--存在mybatisplus注解添加相关注解保证不报错-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-annotation</artifactId>
            <version>${mybatis-plus-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-core</artifactId>
            <version>${mybatis-plus-boot-starter.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
</dependency>

现在就不会报错了: 

2.3 课程查询 设计分析 P18

2.4 课程查询 接口定义 P19

该节内容可以参考下面的文档进行配置,所以我只罗列关键步骤。

第1步:定义分页查询模型类。在xuecheng-plus-base下面的src\main\java\com\xuecheng\base\model下面创建PageParams类,写入代码。

第2步:定义条件模型类。在xuecheng-plus-content/xuecheng-plus-content-model下面src/main/java/com/xuecheng/content/model下创建dto包,创建QueryCourseParamsDto类:

第3步:定义响应模型类。

PageResult分为2部分,1部分是数据,另1部分是分页信息。

第4步:把依赖放到xuecheng-plus-content-api的pom.xml中

详细请见文档:

我个人导入的时候cloud的基础环境包有点问题,然后我修改了<parent>里面的代码:

第5步:在xuecheng-plus-content-api的com/xuecheng下面创建content/api,然后创建一个CourseBaseInfoController类:

@RestController
public class CourseBaseInfoController {
    @RequestMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams,@RequestBody QueryCourseParamsDto queryCourseParamsDto){
        return null;
    }
}

第6步:在xuecheng-plus-content-api的com/xuecheng下面创建ContentApplication启动类,注意这个类一定是要在xucheng下面(不要放到content下)。

@SpringBootApplication
public class ContentApplication {
    public static void main(String[] args){
        SpringApplication.run(ContentApplication.class,args);
    }
}

log4j2-dev.xml在下面的位置,粘贴到下面位置:

 

在xuecheng-plus-content的xuecheng-plus-content-api下面的resources下创建bootstrap.yml文件,然后写入如下代码,注意把url中端口后的数据库名称改成自己的:

点击启动类中下面按钮启动项目:

重点:请求localhost:63040/content/course/list出现的是下面的情况(政策现象):

请求的数据和接口不匹配,加上pageNo和pageSize仍旧不行。

localhost:63040/content/course/list?pageNo=1&pageSize=30

是因为第二个参数要接受json数据转化为Java对象,而现在没有这个json数据。

@RequestBody这个注解对参数的要求为true,因此不行。修改方式如下给@RequestBody加一个required=false。

现在再请求就没有任何问题了:

控制台也显示成功:

前端和controller层间用vo传递数据,controller和service层间用DTO传输数据,service和dao层间用po传输数据。

如果有多个前端,比如手机、PC,传入的参数个数不同,就需要有VO(避免让负责手机前端的工程师误认为有5个参数),比如VO1对应手机是3个参数,VO2对应PC是5个参数。如果没有多个前端,就只有1个前端,那就只需要用DTO即可,不需要VO。

 2.5 课程查询 swagger P20

swagger可以在线生成接口文档。

首先加入swagger依赖(之前已加完),然后要在配置文件bootstrap.yml中进行配置:

swagger:
  title: "学成在线内容管理系统"
  description: "内容系统管理系统对课程相关信息进行管理"
  base-package: com.xuecheng.content
  enabled: true
  version: 1.0.0

在启动类上加@EnableSwagger2Doc注解

然后重启项目

在浏览器中输入:localhost:63040/content/swagger-ui.html,然后可以看到如下的界面:

在类上加@Api注解,在方法上加@ApiOperation接口,把@RequestMapping改成@PostMapping:

@Api(value="课程信息管理接口",tags="课程信息管理接口")
@RestController
public class CourseBaseInfoController {
    @ApiOperation("课程查询接口")
    @PostMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams,@RequestBody(required=false) QueryCourseParamsDto queryCourseParamsDto){
        return null;
    }
}

效果如下: 

@ApiModelProperty可以加在属性上,用来给属性备注名称。

swagger里还可以进行接口测试

但发现日期不太好看

可以直接从下面这个文件中取出LocalDateTimeConfig这个工具类:

然后放到如下的位置:

重启项目,重新测试:

2.6 SpringBoot常用注解 P21

@ResponseBody : 将数据以json的格式响应给前端(侧重返回,定义在类上)

@RequestBody :将json数据转化为java对象(侧重接收,定义在方法上)

@PathVariable : 接收请求路径中占位符的值

@Autowired:是基于类型的注入。

@Resource:基于名称注入。

2.7 项目开发流程 P22

1.产品人员设计产品原型。

2.讨论需求。

3.分模块设计接口。

4.出接口文档。

5.将接口文档给到前端人员,前后端分离开发。

6.开发完毕进行测试。

7.测试完毕发布项目,由运维人员进行部署安装。

2.8 课程查询 DAO接口 P23

前后端分离开发,先定义controller层的接口,生成接口文档,然后再前后端一起开发。

要从底层开始写,从持久层开始写。

在xuecheng-plus-content-service下面的com/xuecheng下面创建content包,在content下创建mapper包。

然后把xuecheng-plus-generator下的com/xuecheng/content/mapper下的所有文件拷贝到上面service的mapper包下。

然后要进行单元测试,首先把xuecheng-plus-content-service的依赖补全,把如下这些依赖复制粘贴到service:

在service的test下创建resources:

把api模块定义的2个配置文件,拷贝到service模块:

配置文件只需要像下面这样:

在service的test/java下创建com/xuecheng包,然后把api的启动类拷贝到com/xuecheng下,删掉生成接口文档的注解:

分页插件会自动加limit语句

分页插件的原理:分页参数会放到ThreadLocal中,mybatis plus有一个拦截器,可以拦截执行的sql,根据数据库类型添加对应的分页语句重写sql,在末尾拼加limit。

在xuecheng-plus-content-service的java/com/xuecheng/content下创建一个config包,然后创建MybatisPlusConfig类,把代码都写入进去:

在service模块的test/java/com/xuecheng/content下创建CourseBaseMapperTests,写入如下内容:

如果左侧为绿√,代表测试通过,也可以打个断点看看:

CourseBaseMapperTests代码如下: 

@SpringBootTest
public class CourseBaseMapperTests {
    @Autowired
    CourseBaseMapper courseBaseMapper;
    @Test
    public void testCourseBaseMapper(){
        CourseBase courseBase = courseBaseMapper.selectById(18);
        Assertions.assertNotNull(courseBase);
        //详细进行分页查询的单元测试
        //查询条件
        QueryCourseParamsDto courseParamsDto = new QueryCourseParamsDto();
        courseParamsDto.setCourseName("java"); //课程名称查询条件
        //拼装查询条件
        LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
        //根据名称模糊查询.在sql中拼接course_base.name like '%值%'
        queryWrapper.like(StringUtils.isNotEmpty(courseParamsDto.getCourseName()),CourseBase::getName,courseParamsDto.getCourseName());
        //根据课程审核状态查询 course_base.audit_status= ?
        queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getAuditStatus()),CourseBase::getAuditStatus,courseParamsDto.getAuditStatus());
        //分页参数对象
        PageParams pageParams = new PageParams();
        pageParams.setPageNo(1L);
        pageParams.setPageSize(2L);
        //创建page分页参数对象,参数:当前页码,每页记录数。
        Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        //开始进行分页查询
        Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper);
        //数据列表
        List<CourseBase> items = pageResult.getRecords();
        //总记录数
        long total = pageResult.getTotal();
        //List<T> items,long counts,long page,long pageSize
        PageResult<CourseBase> courseBasePageResult = new PageResult<CourseBase>(items,total,pageParams.getPageNo(),pageParams.getPageSize());
        System.out.println(courseBasePageResult);
    }
}

在末尾打上断点,进行断点调试: 

看一下是否有LIMIT语句: 

看一下结果数据是否完整:

如果觉得数据有问题可以把SQL语句复制到Navicat中,然后把参数逐一替换问号,进行执行、

2.9 数据字典表 P24

下拉框的文字来源于数据字典表,方便前端进行修改替换展示。

数据字典由code编码和文字组成,通过编码来指代文字。

在Navicat中创建一个新的数据库,xc_system,uff8mb4_general_ci。然后执行.sql文件,全称是xcplus_system.sql。

虚拟机里面的Mysql已经有了这个数据库,名字叫作xcplus-system,所以我们不用创建,只需要知道流程。

2.10 (课程查询)service P25

在xuecheng-plus-base的xuecheng-plus-content-service下的src/main/java/com/xuecheng/content下创建service包,在service包下创建CourseBaseInfoService接口,写入如下代码:

//课程信息管理接口
public interface CourseBaseInfoService {
    /**
     * 课程分页查询
     * @param pageParams 分页查询参数
     * @param queryCourseParamsDto 查询条件
     * @return 查询结果
     */
    public PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto);
}

再在service包下创建impl包,在impl包下创建实现类CourseBaseInfoServiceImpl,写入如下代码(注意一定要把return null改过了):

@Slf4j
@Service
public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {
    @Autowired
    CourseBaseMapper courseBaseMapper;

    @Override
    public PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto courseParamsDto) {
        //拼装查询条件
        LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
        //根据名称模糊查询.在sql中拼接course_base.name like '%值%'
        queryWrapper.like(StringUtils.isNotEmpty(courseParamsDto.getCourseName()),CourseBase::getName,courseParamsDto.getCourseName());
        //根据课程审核状态查询 course_base.audit_status= ?
        queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getAuditStatus()),CourseBase::getAuditStatus,courseParamsDto.getAuditStatus());
        //创建page分页参数对象,参数:当前页码,每页记录数。
        Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        //开始进行分页查询
        Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper);
        //数据列表
        List<CourseBase> items = pageResult.getRecords();
        //总记录数
        long total = pageResult.getTotal();
        //List<T> items,long counts,long page,long pageSize
        PageResult<CourseBase> courseBasePageResult = new PageResult<CourseBase>(items,total,pageParams.getPageNo(),pageParams.getPageSize());
        return courseBasePageResult;
    }
}

 然后要进行单元测试,复制service模块test下原有的CourseBaseMapperTests粘贴到自己的路径下,然后改名为CourseBaseInfoServiceTests,写入如下代码:

@SpringBootTest
public class CourseBaseInfoServiceTests {
    @Autowired
    CourseBaseInfoService courseBaseInfoService;
    @Test
    public void testCourseBaseInfoService(){
        //查询条件
        QueryCourseParamsDto courseParamsDto = new QueryCourseParamsDto();
        courseParamsDto.setCourseName("java"); //课程名称查询条件
        courseParamsDto.setAuditStatus("202004");
        //分页参数对象
        PageParams pageParams = new PageParams();
        pageParams.setPageNo(1L);
        pageParams.setPageSize(2L);
        PageResult<CourseBase> courseBasePageResult = courseBaseInfoService.queryCourseBaseList(pageParams, courseParamsDto);
        System.out.println(courseBasePageResult);
    }
}

看一下控制台输出没太大问题: 

2.11 (课程查询)接口测试 P26

完善xuecheng-plus-content-api下的CourseBaseInfoController的代码:

@Api(value="课程信息管理接口",tags="课程信息管理接口")
@RestController
public class CourseBaseInfoController {
    @Autowired
    CourseBaseInfoService courseBaseInfoService;
    @ApiOperation("课程查询接口")
    @PostMapping("/course/list")
    public PageResult<CourseBase> list(PageParams pageParams,@RequestBody(required=false) QueryCourseParamsDto queryCourseParamsDto){
        PageResult<CourseBase> courseBasePageResult = courseBaseInfoService.queryCourseBaseList(pageParams, queryCourseParamsDto);
        return courseBasePageResult;
    }
}

用swagger来测试没有问题: 

 

点击请求这里的地球,然后点击如下,生成一个Http的请求。 

把swagger上面的参数转移到下面:

###
POST http://localhost:63040/content/course/list?pageNo=1&pageSize=2
Content-Type: application/json

{
  "auditStatus": "202004",
  "courseName": "java",
  "publishStatus": ""
}

同样可以查询出结果: 

现在可以把这些查询的文件,统一放到一个包下方便管理。

在xuecheng-plus-project下面创建一个api-test包,在包下创建一个文件名为:xc-content-api.http用来存放请求测试的语句,然后再创建一个http-client.env.json文件,用来配置相应的环境变量。

 

效果如下:

2.12 部署前端和管理服务 P27

安装完node和npm看看版本(我用的是《苍穹外卖》配置的版本,经测试可以,表明项目可以向下兼容):

用IDEA打开project-xczx2-portal-vue-ts,右键package.json,点击show npm scripts:

右键serve,然后点击Edit serve Settings。然后配置好Node和nmp的版本。最后Run serve:

 4

成功后点击链接访问即可:

打开开发者工具,all报错,这是系统管理服务,请求的是63110端口。

打开第2天的资料,把xuecheng-plus-system放到project里,把pom.xml转化为maven工程,把api模块的配置文件中的数据库修改为我们自己的数据库。

运行Api模块下的SystemApplication启动类。

出现的是跨域问题:

2.13 跨域三种方案 P28

判断是否跨域请求是基于浏览器的同源策略,同源策略是浏览器的一种安全机制,从一个地址请求另一个地址,如果协议、主机、端口三者全部一致则不属于跨域,否则有一个不一致就是跨域。

从http://localhost:8601到http://localhost:8602,因为端口不同,所以是跨域

从http://192.168.101.10:8601到http://192.168.101.11:8601,由于主机不同,是跨域

从http://192.168.101.10:8601到https://192.168.101.10:8601,由于协议不用,是跨域

方法1:JSONP

方法2:添加响应头

Access-Control-Allow-Origin: http://localhost:8601

服务器收到请求判断这个Origin是否允许跨域,如果允许则在响应头中说明允许该来源的跨域请求。

Access-Control-Allow-Origin: *

方法3:通过nginx代理跨域

由于服务器之间没有跨域,浏览器可以通过nginx去访问。 

Nginx和浏览器之间不是跨域的(协议、服务器、端口都一样),通过Nginx代理去访问服务器。

2.14 定义cors过滤器 P29

在xuecheng-plus-system的xuecheng-plus-content-api下的config包下创建一个GlobalCorsConfig类,写入如下代码:

@Configuration
 public class GlobalCorsConfig {
  @Bean
  public CorsFilter corsFilter() {
   CorsConfiguration config = new CorsConfiguration();
   //允许白名单域名进行跨域调用
   config.addAllowedOrigin("*");
   //允许跨越发送cookie
   config.setAllowCredentials(true);
   //放行全部原始头信息
   config.addAllowedHeader("*");
   //允许所有请求方法跨域调用
   config.addAllowedMethod("*");
   UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
   source.registerCorsConfiguration("/**", config);
   return new CorsFilter(source);
  }
 }

现在解决了跨域问题,前端页面能正常显示。 

2.15 课程查询前后端联调 P30

前端.env是重要文件,修改了要重新启动项目。

当前内容管理模块打开:VUE_APP_SERVER_API_URL=http://localhost:63040

引入网关后会打开:VUE_APP_SERVER_API_URL=http://localhost:63010

前后端联调的过程。

测试:输入java,看是否出现课程。点击下拉框,看是否有文字。

2.16 (分类查询)接口定义 P31

下表是树形结构,是三级结构。

在xuecheng-plus-content的xuecheng-plus-content-model下的dto中,定义一个新类CourseCategoryTreeDto,写入如下代码:

@Data
public class CourseCategoryTreeDto extends CourseCategory implements  java.io.Serializable{
    List<CourseCategoryTreeDto> childrenTreeNodes;
}

在xuecheng-plus-content的xuecheng-plus-content-api/src/main/java/com/xuecheng/content/api下新增一个类CourseCategoryController,写入如下代码:

@RestController
public class CourseCategoryController {
    @GetMapping("/course-category/tree-nodes")
    public List<CourseCategoryTreeDto> queryTreeNodes(){
        return null;
    }
}

2.17 (分类查询)树型查询 P32

方法1:表的自连接。

select 
one.id one_id,
one.label one_label,
two.id two_id,
two.label two_label
from course_category one 
inner join course_category two 
on two.parentid = one.id
where one.parentid = '1'
and one.is_show = '1'
and two.is_show = '1'
order by one.orderby,two.orderby

方法2:递归

向下递归,由根节点找子节点。 

with recursive t1 as (
	select * from course_category where id='1'
	union all 
	select t2.* from course_category t2 inner join t1 on t1.id=t2.parentid
)
select * from t1
order by t1.id

向上递归,由子节点找根节点。

with recursive t1 as (
	select * from course_category where id='1-1-1'
	union all 
	select t2.* from course_category t2 inner join t1 on t1.parentid = t2.id
)
select * from t1
order by t1.id

2.18 (分类查询)开发测试 P33

在xuecheng-plus-content的xuecheng-plus-content-service下的mapper中增加CourseCategoryMapper中的代码:

public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {
    //使用递归查询分类
    public List<CourseCategoryTreeDto> selectTreeNodes(String id);
}

在xuecheng-plus-content的xuecheng-plus-content-service下的mapper中增加CourseCategoryMapper.xml中的代码:

<select id="selectTreeNodes" parameterType="string" resultType="com.xuecheng.content.model.dto.CourseCategoryTreeDto">
    with recursive t1 as (
        select * from course_category where id=#{id}
        union all
        select t2.* from course_category t2 inner join t1 on t1.id = t2.parentid
    )
    select * from t1
    order by t1.id
</select>

在xuecheng-plus-content的xuecheng-plus-content-service的test下的java/com/xuecheng/content下,把CourseBaseMapperTests拷贝一份起名CourseCategoryMapperTests:

写入如下代码:

@SpringBootTest
public class CourseCategoryMapperTests {
    @Autowired
    CourseCategoryMapper courseCategoryMapper;
    @Test
    public void testCourseBaseMapper(){
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes("1");
        System.out.println(courseCategoryTreeDtos);
    }
}


在xuecheng-plus-content的xuecheng-plus-content-service下的service下创建CourseCategoryService接口,写入如下代码:

public interface CourseCategoryService {
    public List<CourseCategoryTreeDto> queryTreeNodes(String id);
}

在xuecheng-plus-content的xuecheng-plus-content-service下的service下创建CourseCategoryServiceImpl类,写入如下代码:

@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {
    @Autowired
    CourseCategoryMapper courseCategoryMapper;
    @Override
    public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
        //调用mapper递归查询出分类信息
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);
        //找到每个节点的子节点,最终封装成List<CourseCategoryTreeDto>
        //先将list转成map,key就是结点的id,value就是CourseCategoryTreeDto对象,目的是为了方便从map获取结点。filter(item->!id.equals(item.getId()))把根节点拍出
        Map<String, CourseCategoryTreeDto> mapTemp = courseCategoryTreeDtos.stream().filter(item->!id.equals(item.getId())).collect(Collectors.toMap(key -> key.getId(), value -> value));
        //定义一个list作为最终返回的list
        List<CourseCategoryTreeDto> courseCategoryList = new ArrayList<>();
        //从头遍历List<CourseCategoryTreeDto>,一边遍历一边找子节点放在父节点的childrenTreeNodes
        courseCategoryTreeDtos.stream().filter(item->!id.equals(item.getId())).forEach(item->{
            if(item.getParentid().equals(id)){
                courseCategoryList.add(item);
            }
            //找到节点的父节点
            CourseCategoryTreeDto courseCategoryTreeDto = mapTemp.get(item.getParentid());
            if(courseCategoryTreeDto!=null) {
                if (courseCategoryTreeDto.getChildrenTreeNodes() == null) {
                    //如果该父节点的ChildrenTreeNodes属性为空要new一个集合,因为要向该集合中放它的子节点
                    courseCategoryTreeDto.setChildrenTreeNodes(new ArrayList<CourseCategoryTreeDto>());
                }
                //到每个节点的子节点放在父节点的childrenTreeNodes属性中
                courseCategoryTreeDto.getChildrenTreeNodes().add(item);
            }
        });
        return courseCategoryList;
    }
}

在xuecheng-plus-content的xuecheng-plus-content-service下的test下创建CourseCategoryServiceTests测试类,写入如下代码:

@SpringBootTest
public class CourseCategoryServiceTests {
    @Autowired
    CourseCategoryService courseCategoryService;
    @Test
    public void testCourseBaseInfoService(){
        List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryService.queryTreeNodes("1");
        System.out.println(courseCategoryTreeDtos);
    }
}

单元测试的效果如下: 

在xuecheng-plus-content-api的CourseCategoryController下完善代码如下:

@RestController
public class CourseCategoryController {
    @Autowired
    CourseCategoryService courseCategoryService;
    @GetMapping("/course-category/tree-nodes")
    public List<CourseCategoryTreeDto> queryTreeNodes(){
        return courseCategoryService.queryTreeNodes("1");
    }
}

先把xuecheng-plus-content的xuecheng-plus-content-api下的ContentApplication启动。

然后在api-test包下的xc-content-api.http中添加如下代码:

### 查询课程分类
GET {{content_host}}/content/course-category/tree-nodes

可以看到正常输出结果: 

在前后端联调的时候可以看到课程分类有了结果:

2.19 (新增课程)接口定义 P34

2.20 (新增课程)接口开发 P35

 把下面2个Dto类复制到xuecheng-plus-content的xuecheng-plus-content-api的dto下面:

在xuecheng-plus-content的xuecheng-plus-content-api下的CourseBaseInfoController中写入如下代码:

@ApiOperation("新增课程")
    @PostMapping("/content/course")
    public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto){
        return null;
    }

在xuecheng-plus-content的xuecheng-plus-content-service的service下的CourseBaseInfoService中添加接口:

public CourseBaseInfoDto createCourseBase(Long companyId,AddCourseDto addCourseDto);

 在xuecheng-plus-content的xuecheng-plus-content-service的service/impl下的CourseBaseInfoServiceImpl中添加代码:

@Autowired
    private CourseMarketMapper courseMarketMapper;
    @Autowired
    private CourseCategoryMapper courseCategoryMapper;
    @Transactional
    @Override
    public CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto dto) {
        //参数的合法性校验
        if (StringUtils.isBlank(dto.getName())) {
            throw new RuntimeException("课程名称为空");
        }

        if (StringUtils.isBlank(dto.getMt())) {
            throw new RuntimeException("课程分类为空");
        }

        if (StringUtils.isBlank(dto.getSt())) {
            throw new RuntimeException("课程分类为空");
        }

        if (StringUtils.isBlank(dto.getGrade())) {
            throw new RuntimeException("课程等级为空");
        }

        if (StringUtils.isBlank(dto.getTeachmode())) {
            throw new RuntimeException("教育模式为空");
        }

        if (StringUtils.isBlank(dto.getUsers())) {
            throw new RuntimeException("适应人群为空");
        }

        if (StringUtils.isBlank(dto.getCharge())) {
            throw new RuntimeException("收费规则为空");
        }

        //向课程基本信息表course_base写入数据
        CourseBase courseBaseNew = new CourseBase();
        //将传入的页面的参数放到courseBase对象中
        BeanUtils.copyProperties(dto,courseBaseNew); //只要属性名相同就可以拷贝
        courseBaseNew.setCompanyId(companyId);
        courseBaseNew.setCreateDate(LocalDateTime.now());
        //审核状态默认为未提交
        courseBaseNew.setAuditStatus("202002");
        //发布状态为未发布
        courseBaseNew.setStatus("203001");
        //插入数据库
        int insert = courseBaseMapper.insert(courseBaseNew);
        if(insert<=0){
            throw new RuntimeException("添加课程失败");
        }
        //向课程营销表course_market写入数据
        CourseMarket courseMarketNew = new CourseMarket();
        //将页面输入的数据拷贝到courseMarketNew
        BeanUtils.copyProperties(dto,courseMarketNew);
        //课程的id
        Long courseId = courseBaseNew.getId();
        courseMarketNew.setId(courseId);
        //保存营销信息
        saveCourseMarket(courseMarketNew);
        //从数据库查询课程的详细信息,包括两部分
        CourseBaseInfoDto courseBaseInfo = getCourseBaseInfo(courseId);
        return courseBaseInfo;
    }

    //查询课程信息
    public CourseBaseInfoDto getCourseBaseInfo(long courseId){
        //从课程基本信息表查询
        CourseBase courseBase = courseBaseMapper.selectById(courseId);
        if(courseBase==null){
            return null;
        }
        //从课程营销表查询
        CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
        //组装在一起
        CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
        BeanUtils.copyProperties(courseBase,courseBaseInfoDto);
        BeanUtils.copyProperties(courseMarket,courseBaseInfoDto);
        //通过courseCategoryMapper查询分类信息,将分类名称放在courseBaseInfoDto对象
        return courseBaseInfoDto;
    }

    //单独写一个方法保存营销信息,逻辑:存在则更新,不存在则添加
    private int saveCourseMarket(CourseMarket courseMarketNew){
        //参数的合法性校验
        String charge = courseMarketNew.getCharge();
        if(StringUtils.isEmpty(charge)){
            throw new RuntimeException("收费规则为空");
        }
        //如果课程收费,价格没有填写也需要抛出异常
        if(charge.equals("201001")){
            if(courseMarketNew.getPrice()==null || courseMarketNew.getPrice().floatValue()<=0){
                throw new RuntimeException("课程的价格不能为空并且必须大于0");
            }
        }
        //从数据库查询营销信息,存在则更新,不存在则添加
        Long id = courseMarketNew.getId();
        CourseMarket courseMarket = courseMarketMapper.selectById(id);
        if(courseMarket==null){
            //插入数据库
            int insert = courseMarketMapper.insert(courseMarketNew);
            return insert;
        }else{
            //将courseMarketNew拷贝到courseMarket
            BeanUtils.copyProperties(courseMarketNew,courseMarket);
            courseMarket.setId(courseMarket.getId());
            //更新
            int i = courseMarketMapper.updateById(courseMarket);
            return i;
        }
    }

2.21 (新增课程)接口测试 P36

首先是完善controller的接口。在xuecheng-plus-content的xuecheng-plus-content-api下的CourseBaseInfoController中完善createCourseBase方法:

@ApiOperation("新增课程")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto){
    //获取到用户所述机构的id
    Long companyId = 1232141425L;
    CourseBaseInfoDto courseBase = courseBaseInfoService.createCourseBase(companyId, addCourseDto);
    return courseBase;
}

然后节约时间跳过单元测试,直接倒api-test下的xc-content-api.http中进行接口测试:

### 新增课程
POST {{content_host}}/content/course
Content-Type: application/json
{
  "charge": "201001",
  "description": "adsd",
  "grade": "204001",
  "mt": "1-1",
  "name": "java网络编程高级",
  "originalPrice": 100,
  "phone": "13333333",
  "pic": "fdsf",
  "price": 10,
  "qq": "22333",
  "st": "1-1-1",
  "tags": "sdsdwe",
  "teachmode": "20002",
  "users": "初级人员",
  "validDays": 365,
  "wechat": "223344"
}

 在controller和service的实现类上打上断点,主要是看看断点调试时接受的数据是否完整。

放行之后数据效果如下:

前端可以直接看到之前新增的课程:

数据库中也有如下数据,没问题:

前端还暂时无法提交。

2.22 Mybatis相关问题 P37

1.Mybatis分页插件的实现原理:

首先分页参数会被放到ThreadLocal中,拦截执行的sql语句,根据数据库的类型(比如是Mysql就会在末尾添加LIMIT)添加对应的分页语句重写sql。

计算出total总记录数,pageNum当前是第几页,pageSize每页的大小。是否为首页,是否为尾页,总页数等。

2.树型表的标记字段是什么?如何查询MySQL树型表?

树型表的标记字段是parentid即父结点的id。

3.查询一个树型表的方法:

当层级固定时可以用表的自连接进行查询。

如果想灵活查询每个层级可以使用mysql递归方法,使用with RECURSIVE实现。

4.Result Type和Result Map的区别:

Result Type:当查询到的SQL字段的名字和Result Type中模型类型的属性名字对应上的,可以由MyBatis自动完成映射。

Result Map:当查询到的SQL字段的名字和Result Type中模型类型的属性名字对应不上时,需要通过Result Map手动完成映射。

5. #{}和${}的区别

#{}是标记一个占位符,可以防止sql注入。

${}用于在动态sql中拼接字符串,可能导致sql注入。

2.23 自定义异常类型 P38

如果前端名称为空,会报错500。但仅在控制台输出,前端没展示。

如果写很多try-catch代码,会造成代码冗余。

由增强类来捕获异常,原理是AOP面向切面编程。

用@ControllerAdvice注解来控制器增强,用异常处理注解@ExceptionHandler。

在xuecheng-plus-base的base包下创建exception包。

在exception包下创建RestErrorResponse类,和前端约定返回的异常信息模型:

/**
 * 错误响应参数包装
 */
public class RestErrorResponse implements Serializable {

    private String errMessage;

    public RestErrorResponse(String errMessage){
        this.errMessage= errMessage;
    }

    public String getErrMessage() {
        return errMessage;
    }

    public void setErrMessage(String errMessage) {
        this.errMessage = errMessage;
    }
}

在exception包下创建XueChengPlusException类:

/**
 * @description 学成在线项目异常类
 * @author Mr.M
 * @date 2022/9/6 11:29
 * @version 1.0
 */
public class XueChengPlusException extends RuntimeException {

   private String errMessage;

   public XueChengPlusException() {
      super();
   }

   public XueChengPlusException(String errMessage) {
      super(errMessage);
      this.errMessage = errMessage;
   }

   public String getErrMessage() {
      return errMessage;
   }

   public static void cast(CommonError commonError){
       throw new XueChengPlusException(commonError.getErrMessage());
   }
   public static void cast(String errMessage){
       throw new XueChengPlusException(errMessage);
   }

}

在exception包下创建CommonError类:

/**
 * @description 通用错误信息
 * @author Mr.M
 * @date 2022/9/6 11:29
 * @version 1.0
 */
public enum CommonError {

   UNKOWN_ERROR("执行过程异常,请重试。"),
   PARAMS_ERROR("非法参数"),
   OBJECT_NULL("对象为空"),
   QUERY_NULL("查询结果为空"),
   REQUEST_NULL("请求参数为空");

   private String errMessage;

   public String getErrMessage() {
      return errMessage;
   }

   private CommonError( String errMessage) {
      this.errMessage = errMessage;
   }

}

2.24 异常处理开发测试 P39

 现在想测试课程名称为空是否会触发XueChengPlusException异常处理。

首先在service模块的service包的impl包下的CourseBaseInfoServiceImpl类中修改下面的代码:

//参数的合法性校验
if (StringUtils.isBlank(dto.getName())) {
    //throw new RuntimeException("课程名称为空");
    XueChengPlusException.cast("课程名称为空");
}

 在xuecheng-plus-base的exception下的GlobalExceptionHandler类下的如下位置打上断点:

最后在api-test包下的xc-content-api.http下面,让name参数为空。

在如下位置添加自定义的异常处理语句。

 把现价改为负数,会触发异常。

 

2.25 系统异常处理 P40

处理自定义异常:程序在编写代码时根据校验结果主动抛出自定义异常类对象,抛出异常时指定详细的异常信息,异常处理器捕获异常信息记录异常日志并相应给客户。

2.26 JSR303校验 P41

JSR303是一个校验框架。

使用方式:只需要在模型类中通过注解指定校验规则(比如@NOTEmpty来指定不能为空,@Size来指定长度的下限和上限),对不同的应用场景可以通过分组来切换校验规则使用groups,在controller方法上开启校验@Validated。

前端请求后端接口传输参数,在controller和service中都需要校验。

controller校验请求参数的合法性,包括必填项校验、数据格式校验(是否符合一定的日期格式)。

service校验业务规则的相关内容。

因为service是根据业务规则去校验所以不方便写成通用代码,controller中则可以将校验的代码写成通用代码。

引入如下依赖:

合法性校验的注解如下:

如果想要使用需要激活:

首先要把service层校验屏蔽掉(我是直接删掉):

然后记得要把启动类重启一下,代码才能生效。

在xc-content-api.http中把name改为空,走的是系统异常处理,所以一律输出为位置错误,执行过程异常,请重试。

现在出现问题,如果多个接口使用同一个模型类时,对校验的需求不一样时会出现问题。

解决方法:分组校验。

在xuecheng-plus-base的exception下面的ValidationGroups类中写入如下代码:

//用于分组校验,定义一些常用的组
public class ValidationGroups {
    public interface Insert{};
    public interface Update{};
    public interface Delete{};
}

在xuecheng-plus-content的xuecheng-plus-content-model的dto下面的类AddCourseDto替换如下代码:

@NotEmpty(message = "新增课程名称不能为空",groups={ValidationGroups.Insert.class})
@NotEmpty(message = "修改课程名称不能为空",groups={ValidationGroups.Update.class})
@ApiModelProperty(value = "课程名称", required = true)
private String name;

 @Validated注解里面写上所分的组类:

2.27 系统参数合法性校验 P42

提问:对表单的数据是怎么校验的?

回答:JSR303校验规则。

如果javax.validation.constraints包下的校验规则满足不了需求怎么办?

1.手写校验代码

2.自定义校验规则注解

2.28 (修改课程)接口开发 P44

在xuecheng-plus-content的xuecheng-plus-content-api的api包下的CourseBaseInfoController中,写入如下代码:

@ApiOperation("根据课程id查询接口")
@GetMapping("/course/{courseId}")
public CourseBaseInfoDto getCourseBaseById(@PathVariable Long courseId){
    CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
    return courseBaseInfo;
}

在xuecheng-plus-content的xuecheng-plus-content-service的service包下的CourseBaseInfoService中写入如下代码:

//根据课程id查询课程信息
public CourseBaseInfoDto getCourseBaseInfo(Long courseId);

在xuecheng-plus-content的xuecheng-plus-content-service的service/impl包下的CourseBaseInfoServiceImpl中写入如下代码:

//查询课程信息
public CourseBaseInfoDto getCourseBaseInfo(Long courseId){
    //从课程基本信息表查询
    CourseBase courseBase = courseBaseMapper.selectById(courseId);
    if(courseBase==null){
        return null;
    }
    //从课程营销表查询
    CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
    //组装在一起
    CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
    BeanUtils.copyProperties(courseBase,courseBaseInfoDto);
    BeanUtils.copyProperties(courseMarket,courseBaseInfoDto);
    //通过courseCategoryMapper查询分类信息,将分类名称放在courseBaseInfoDto对象
    return courseBaseInfoDto;
}

 在api-test的xc-content-api.http中写入如下代码:

### 课程查询
GET {{content_host}}/content/course/40
Content-Type: application/json

返回的结果如下:

在xuecheng-plus-content的xuecheng-plus-content-model下面的dto包下面,重新定义一个类EditCourseDto,代码如下:

因为修改比新增就多了一个id,可以继承新增。

@Data
@ApiModel(value="EditCourseDto",description="修改课程基本信息")
public class EditCourseDto extends AddCourseDto{
    @ApiModelProperty(value="课程id",required=true)
    private Long Id;
}

在xuecheng-plus-content的xuecheng-plus-content-api的api包下的CourseBaseInfoController中,写入如下代码:

@ApiOperation("修改课程")
@PutMapping("/course")
public CourseBaseInfoDto modifyCourseBase(@RequestBody @Validated EditCourseDto editCourseDto){
    //获取到用户所属机构的id
    Long companyId = 1232141425L;
    CourseBaseInfoDto courseBaseInfoDto = courseBaseInfoService.updateCourseBase(companyId, editCourseDto);
    return courseBaseInfoDto;
}

在xuecheng-plus-content的xuecheng-plus-content-service的service包下的CourseBaseInfoService中写入如下代码:

public CourseBaseInfoDto updateCourseBase(Long companyId,EditCourseDto editCourseDto);

在xuecheng-plus-content的xuecheng-plus-content-service的service/impl包下的CourseBaseInfoServiceImpl中写入如下代码(缺少更新营销信息):

@Override
public CourseBaseInfoDto updateCourseBase(Long companyId, EditCourseDto editCourseDto) {
    //拿到课程id
    Long courseId = editCourseDto.getId();
    //查询课程信息
    CourseBase courseBase = courseBaseMapper.selectById(courseId);
    if(courseBase == null){
        XueChengPlusException.cast("课程不存在");
    }
    //数据合法性校验
    //根据具体的业务逻辑去校验
    //本机构只能修改本机构的课程
    if(!companyId.equals(courseBase.getCompanyId())){
        XueChengPlusException.cast("本机构只能膝盖本机构的课程");
    }
    //封装数据
    BeanUtils.copyProperties(editCourseDto,courseBase);
    //修改时间
    courseBase.setChangeDate(LocalDateTime.now());
    //更新数据库
    int i = courseBaseMapper.updateById(courseBase);
    if(i<=0){
        XueChengPlusException.cast("修改课程失败");
    }
    //查询课程信息
    CourseBaseInfoDto courseBaseInfo = getCourseBaseInfo(courseId);
    return courseBaseInfo;
}

 在api-test的xc-content-api.http中写入如下代码:

### 课程查询
GET {{content_host}}/content/course/40
Content-Type: application/json

2.29 (修改课程)接口测试 P45

输入下面的网址,打开前端界面:

http://localhost:8601/#/

 点击编辑按钮,尝试修改基本信息

 修改完毕之后,原先的信息会被更改。

 返回的结果如下(属于正常,跳转到另外一个还没写的界面):

2.30 (计划查询)接口定义 P46

在xuecheng-plus-content的xuecheng-plus-content-model下面的dto包下面,定义一个新类TeachplanDto,代码如下:

@Data
@ToString
public class TeachplanDto extends Teachplan {
    //课程计划关联的媒资信息
    private TeachplanMedia teachplanMedia;
    //子结点
    private List<TeachplanDto> teachPlanTreeNodes;
}

在xuecheng-plus-content的xuecheng-plus-content-api的api包下的TeachplanController中,写入如下代码(初步):

//课程计划管理相关的接口
@Api(value="课程计划编辑接口",tags="课程计划编辑接口")
@RestController
public class TeachplanController {
    @ApiOperation("查询课程计划树形结构")
    @GetMapping("/teachplan/{courseId}/tree-nodes")
    public List<TeachplanDto> getTreeNodes(@PathVariable Long courseId){
        return null;
    }
}

2.31 (计划查询)sql语句 P47

在xuecheng-plus-content的xuecheng-plus-content-service的mapper下的TeachplanMapper中写入如下代码:

public interface TeachplanMapper extends BaseMapper<Teachplan> {
    //课程计划查询
    public List<TeachplanDto> selectTreeNodes(Long courseId);
}

在xuecheng-plus-content的xuecheng-plus-content-service的mapper下的TeachplanMapper.xml中写入如下代码:

<select id="selectTreeNodes" parameterType="long" resultMap="treeNodeResultMap">
    select
        one.id             one_id,
        one.pname          one_pname,
        one.parentid       one_parentid,
        one.grade          one_grade,
        one.media_type     one_mediaType,
        one.start_time     one_stratTime,
        one.end_time       one_endTime,
        one.orderby        one_orderby,
        one.course_id      one_courseId,
        one.course_pub_id  one_coursePubId,
        two.id             two_id,
        two.pname          two_pname,
        two.parentid       two_parentid,
        two.grade          two_grade,
        two.media_type     two_mediaType,
        two.start_time     two_stratTime,
        two.end_time       two_endTime,
        two.orderby        two_orderby,
        two.course_id      two_courseId,
        two.course_pub_id  two_coursePubId,
        m1.media_fileName mediaFilename,
        m1.id teachplanMeidaId,
        m1.media_id mediaId
    from teachplan one
             INNER JOIN teachplan two on one.id = two.parentid
             LEFT JOIN teachplan_media m1 on m1.teachplan_id = two.id
    where one.parentid = 0 and one.course_id=#{value}
    order by one.orderby,two.orderby
</select>

2.32 (计划查询)接口开发 P48

在xuecheng-plus-content的xuecheng-plus-content-service的java的mapper下的TeachplanMapper下写入如下代码:

public interface TeachplanMapper extends BaseMapper<Teachplan> {
    //课程计划查询
    public List<TeachplanDto> selectTreeNodes(Long courseId);
}

在xuecheng-plus-content的xuecheng-plus-content-service的java的mapper下的TeachplanMapper.xml下写入如下代码:

column是从数据库查到的字段,property是目标类的属性,也就是将从数据库查到的字段(column)映射到目标类的属性(property)上。

除了id字段用id标签,其它都用result标签。

<resultMap id="treeNodeResultMap" type="com.xuecheng.content.model.dto.TeachplanDto">
    <id column="one_id" property="id"/>
    <result column="one_pname" property="pname"/>
    <result column="one_pname"      property="pname" />
    <result column="one_parentid"     property="parentid" />
    <result column="one_grade"  property="grade" />
    <result column="one_mediaType"   property="mediaType" />
    <result column="one_stratTime"   property="stratTime" />
    <result column="one_endTime"   property="endTime" />
    <result column="one_orderby"   property="orderby" />
    <result column="one_courseId"   property="courseId" />
    <result column="one_coursePubId"   property="coursePubId" />
    <!--映射子节点,一对多映射,ofType填的是list中对象类型-->
    <collection property="teachPlanTreeNodes" ofType="com.xuecheng.content.model.dto.TeachplanDto">
        <!-- 二级数据映射 -->
        <id     column="two_id"        property="id" />
        <result column="two_pname"      property="pname" />
        <result column="two_parentid"     property="parentid" />
        <result column="two_grade"  property="grade" />
        <result column="two_mediaType"   property="mediaType" />
        <result column="two_stratTime"   property="stratTime" />
        <result column="two_endTime"   property="endTime" />
        <result column="two_orderby"   property="orderby" />
        <result column="two_courseId"   property="courseId" />
        <result column="two_coursePubId"   property="coursePubId" />
        <!--一对一映射-->
        <association property="teachplanMedia" javaType="com.xuecheng.content.model.po.TeachplanMedia">
            <result column="teachplanMeidaId"   property="id" />
            <result column="mediaFilename"   property="mediaFilename" />
            <result column="mediaId"   property="mediaId" />
            <result column="two_id"   property="teachplanId" />
            <result column="two_courseId"   property="courseId" />
            <result column="two_coursePubId"   property="coursePubId" />
        </association>
    </collection>
</resultMap>

在xuecheng-plus-content的xuecheng-plus-content-service的test的content下创建一个TeachplanMapperTests类,写入如下代码:

@SpringBootTest
public class TeachplanMapperTests {
    @Autowired
    TeachplanMapper teachplanMapper;
    @Test
    public void testSelectTreeNodes(){
        List<TeachplanDto> teachplanDtos = teachplanMapper.selectTreeNodes(117L);
        System.out.println(teachplanDtos);
    }
}

当测试数据为117L的时候,会有2个大章节。

在xuecheng-plus-content的xuecheng-plus-content-service的service包下的TeachplanService

中写入如下代码:

//课程计划管理相关接口
public interface TeachplanService {
    //根据课程id查询课程计划
    public List<TeachplanDto> findTeachplanTree(Long courseId);
}

在xuecheng-plus-content的xuecheng-plus-content-service的service/impl包下的TeachplanService

中写入如下代码:

@Service
public class TeachplanServiceImpl implements TeachplanService {
    @Autowired
    TeachplanMapper teachplanMapper;
    @Override
    public List<TeachplanDto> findTeachplanTree(Long courseId) {
        List<TeachplanDto> teachplanTree = teachplanMapper.selectTreeNodes(courseId);
        return teachplanTree;
    }
}

在xuecheng-plus-content的xuecheng-plus-content-api的api包下的TeachplanController中,写入如下代码(完善): 

//课程计划管理相关的接口
@Api(value="课程计划编辑接口",tags="课程计划编辑接口")
@RestController
public class TeachplanController {
    @Autowired
    private TeachplanService teachplanService;
    @ApiOperation("查询课程计划树形结构")
    @GetMapping("/teachplan/{courseId}/tree-nodes")
    public List<TeachplanDto> getTreeNodes(@PathVariable Long courseId){
        List<TeachplanDto> teachplanTree = teachplanService.findTeachplanTree(courseId);
        return teachplanTree;
    }
}

 在api-test的xc-content-api.http中写入如下代码:

### 课程计划查询
GET {{content_host}}/content/teachplan/117/tree-nodes

控制台输出的效果如下,测试成功(前后端联调测试没问题): 

 

2.33 (新增修改计划)定义 P49

点击“添加章”新增第一级课程计划。

点击“添加小节”向某个第一级课程计划下添加小节。

点击“章”、“节”的名称,可以修改名称、选择是否免费。

同一个接口接收新增和修改两个业务请求,以是否传递课程计划id 来判断是新增还是修改(传递了课程计划id说明当前是要修改该课程计划,否则是新增一个课程计划)。

在xuecheng-plus-content的xuecheng-plus-content-model下的dto中新增一个类,写入如下代码:

@Data
@ToString
public class SaveTeachplanDto {
    private Long id;
    private String pname;
    private Long parentid;
    private Integer grade;
    private String mediaType;
    private Long courseId;
    private Long coursePubId;
    private String isPreview;
}

 在xuecheng-plus-conten的xuecheng-plus-content-api下的content/api下的TeachplanController中写入如下代码:

@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan(@RequestBody SaveTeachplanDto teachplan){
}

2.34 (新增修改计划)开发 P50

在xuecheng-plus-content的xuecheng-plus-content-api的api的TeachplanController完善接口:

@ApiOperation("课程计划创建或修改")
@PostMapping("/teachplan")
public void saveTeachplan(@RequestBody SaveTeachplanDto teachplan){
     teachplanService.saveTeachplan(teachplan);
}

在xuecheng-plus-content的xuecheng-plus-content-service的service的TeachplanService下新增接口:

//新增/修改/保存课程计划
public void saveTeachplan(SaveTeachplanDto saveTeachplanDto);

 在xuecheng-plus-content的xuecheng-plus-content-service的service的TeachplanServiceImpl下编写如下代码:

@Override
public void saveTeachplan(SaveTeachplanDto saveTeachplanDto) {
    Long teachplanId = saveTeachplanDto.getId();
    if(teachplanId==null){
        //新增
        Teachplan teachplan = new Teachplan();
        BeanUtils.copyProperties(saveTeachplanDto,teachplan);
        //确定排序字段,找到它的同级节点个数,排序字段就是个数+1
        Long parentId = saveTeachplanDto.getParentid();
        Long courseId = saveTeachplanDto.getCourseId();
        teachplan.setOrderby(getTeachplanCount(courseId, parentId));
        teachplanMapper.insert(teachplan);
    }else{
        //修改
        Teachplan teachplan = teachplanMapper.selectById(teachplanId);
        //将参数复制到teachplan
        BeanUtils.copyProperties(saveTeachplanDto,teachplan);
        teachplanMapper.updateById(teachplan);
    }
}

前后端联调测试没问题, 添加小节没问题:

存在一个BUG,添加章的时候不显示。

2.35 项目实战说明 P51

本节主要讲解了,如何下载gogs的windows版本,然后配置环境,方便分工协作开发接口。

删除课程计划。上移、下移排序。

师资管理,对老师表进行增删改查。

删除课程要把课程相关的基本信息、营销信息、课程计划、课程教师信息也删掉。

三、媒资管理模块

3.1 媒资管理需求分析 P52

媒资管理是媒体资源管理,媒体资源主要包括视频、文章、图片、音频等。

图片会存储到分布式文件系统中。

视频上传会通过断点续传的方式上传。

到当前已经有了3个微服务,内容管理(管理课程),系统管理(数据字典),媒资管理。

3.2 为什么用网关 P53

网关是用来路由的,转发请求;可以实现权限控制、限流等功能;避免前端直接请求微服务,编程前端请求网关,网关来请求微服务。

网关从服务注册中心来拿到微服务实例的地址。

3.3 (nacos)服务发现中心 P54

Spring Cloud是一系列微服务技术栈,是一套规范。

Spring Cloud alibaba:nacos服务注册中心、配置中心。

namespace是命名空间,用于区分环境,比如:开发环境、测试环境、生产环境。

group:用于区分项目。

下面是我登录nacos遇到的问题:

起初输入如下的命令,没能登录上nacos,显示的是拒绝访问:

http://192.168.101.65:8848/nacos/#/login

此时需要到虚拟机中,输入docker ps看看哪些服务启动了: 

一定要确保先启动mysql再启动docker:

docker stop mysql
docker stop nacos
docker start mysql
docker start nacos

 如下图能够正常访问:

 

具体配置:

1.先要在xuecheng-plus-parent中添加如下依赖:

<dependency>
   <groupId>com.alibaba.cloud</groupId>
   <artifactId>spring-cloud-alibaba-dependencies</artifactId>
   <version>${spring-cloud-alibaba.version}</version>
   <type>pom</type>
   <scope>import</scope>
</dependency>

2.然后要在xuecheng-plus-content-api的pom.xml中写入如下代码:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

3.在api的bootstrap.yml配置文件中写入如下代码,配置nacos的地址:

spring:
  application:
    name: content-api #服务名
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery: #服务注册相关配置
        namespace: dev #命名空间
        group: xuecheng-plus-project

 记得刷新maven,然后启动contentApplication启动类,然后到nacos网页可以看到有服务成功注册入

3.4 (nacos)配置api P55

搭建Nacos配置中心,目的是通过Nacos去管理项目的所有配置。更加方便,可以不用重启就去更改配置。

nacos定位一个具体配置文件的方式是:namespace、group、dataid。

dataid由三部分组成:应用名-环境名.配置文件格式

通过spring.profiles.active来指定环境名(开发环境,测试环境,生产环境):

配置文件的名称如下:content-api-dev.yaml

配置如下:

可以把api的配置文件中的datasource给注释掉,记得要添加config的配置,把配置文件配置上,代码如下:

  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery: #服务注册相关配置
        namespace: dev #命名空间
        group: xuecheng-plus-project

      profiles:
        active: dev #环境名

      config: #配置文件的相关信息
        namespace: dev #命名空间
        group: xuecheng-plus-project
        file-extension: yaml
        refresh-enabled: true
        extension-configs:
          - data-id: content-service-${spring.profiles.active}.yml
            group: xuecheng-plus-project
            refresh: true

记得要在api的pom.xml文件中填写如下依赖,会从nacos定时拉取配置:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

然后刷新maven,然后重启ContentApplication启动类,会出现如下场景:

然后在swagger里面进行简单测试,看数据库能否连上,选择查询的接口,输入参数然后发送请求。

3.5 (nacos)配置service P56

service的配置文件在test里面。

api原本不需要数据库配置连接,但因为api依赖了service模块,所以启动后service的代码依赖都会到api中。

所以现在的思路是:只在service中配置数据库依赖,然后api引用service的配置。

第1步:在service中进行相关配置

在service的pom.xml中加依赖,只需要加config依赖,不需要加discovery依赖:

<dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

在service的bootstrap.yml中修改配置文件(这里要注意2点,profiles这里不要写成profile,还有profiles是和cloud和application同级的):

#微服务配置
spring:
  application:
    name: content-service
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      config: #配置文件的相关信息
        namespace: dev #命名空间
        group: xuecheng-plus-project
        file-extension: yaml
        refresh-enabled: true
  profiles:
    active: dev

在nacos中点+号,新建service的配置,把数据库的配置粘贴上即可:

现在启动service的test下的CourseBaseMapperTests单元测试文件:

第2步:让api引用service的配置

注意这里要把api中的数据库连接屏蔽掉,只在service的nacos配置中留有数据库连接:

api的bootstrap.yml配置文件中的代码如下:

#微服务配置
spring:
  application:
    name: content-api #服务名
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery: #服务注册相关配置
        namespace: dev #命名空间
        group: xuecheng-plus-project
      config: #配置文件的相关信息
        namespace: dev #命名空间
        group: xuecheng-plus-project
        file-extension: yaml
        refresh-enabled: true
        extension-configs:
          - data-id: content-service-${spring.profiles.active}.yaml
            group: xuecheng-plus-project
            refresh: true
  profiles:
    active: dev #环境名

这里有几点易错的点:1.冒号后面都要空1格。2.config是在nacos的下一级,与server-addr和 discovery同级。3.profiles是在spring的下一级,与application和cloud同级。4.文件的后缀是yaml。5.${spring.profiles.active}注意这个花括号里都是点号分隔

能出数据就没啥问题:

因为swagger在所有的模块中都需要配置,所以我们想如何在nacos中配置项目的公用配置。

配置内容填写如下内容:

swagger:
  title: "学成在线项目接口文档"
  description: "学成在线项目接口文档"
  base-package: com.xuecheng
  enabled: true
  version: 1.0.0

微服务的完整配置如下,shared-configs必须要在extension-configs的平级位置,在config的次一级位置。

#微服务配置
spring:
  application:
    name: content-api#服务名
  cloud:
    nacos:
      server-addr: 192.168.101.65:8848
      discovery: #服务注册相关配置
        namespace: dev #命名空间
        group: xuecheng-plus-project
      config: #配置文件的相关信息
        namespace: dev #命名空间
        group: xuecheng-plus-project
        file-extension: yaml
        refresh-enabled: true
        extension-configs:
          - data-id: content-service-${spring.profiles.active}.yml
            group: xuecheng-plus-project
            refresh: true
        shared-configs:
          - data-id: swagger-${spring-profiles.active}.yaml
            group: xuecheng-plus-common
            refresh: true
          - data-id: logging-${spring-profiles.active}.yaml
            group: xuecheng-plus-common
            refresh: true

  profiles:
    active: dev #环境名

这里有几点易错的点:1.冒号后面都要空1格。2.config是在nacos的下一级,与server-addr和 discovery同级。3.profiles是在spring的下一级,与application和cloud同级。4.文件的后缀是yaml。5.${spring.profiles.active}注意这个花括号里都是点号分隔 

测试:如果名称改变,控制台有DEBUG语句输出就没问题!

3.6 (nacos)配置优先级 P57

引入配置文件的形式有:

1、以项目应用名方式引入

2、以扩展配置文件方式引入

3、以共享配置文件 方式引入

4、本地配置文件

各配置文件 的优先级:项目应用名配置文件 > 扩展配置文件  > 共享配置文件 > 本地配置文件。

然后我们还要在nacos中加上本地优先。

现在启动contentApplication就没啥问题了。

3.7 (nacos)配置代码导入 P58

把下面的文件解压。

然后点击上传文件把这些配置都上传上来。

3.8 搭建网关 P59

按照如下的方式创建一个网关模块:

先把依赖全部先导入到pom.xml文件中:

把xuecheng-plus-system的配置文件全部拷贝到xuecheng-plus-gateway中:

启动类修改如下:

@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class,args);
    }
}

现在启动启动类,可以看到gateway网关已经申报到nacos:

测试的方法是把content_host改为gateway_host,然后发送请求,看看是否正常返回结果。

3.9 搭建媒资服务工程 P60

媒资服务工程的文件在day05的资料中,将xuecheng-plus-media.zip文件解压到当前文件夹,

把模块直接导入xuecheng-plus-project下面

启动完xuecheng-plus-media的xuecheng-plus-media-api下的启动类MediaApplication之后,然后可以访问下面的地址看看能不能访问接口文档:

http://localhost:63050/media/swagger-ui.html

3.10 分布式文件系统 P61

文件系统:方便对磁盘上的文件进行管理的软件系统。

因为一台计算机无法存储海量的文件,所以通过网络将若干计算机组织起来共同存储海量文件。

3.11 MinIO文件系统 P62

MinIO是一个轻量级的开源文件系统,可以存储大量的非结构化数据。去中心化的共享架构。

采用冗余存储,一个节点2块磁盘,8块磁盘组成一个集合。使用纠删码技术来保护数据,可以恢复丢失和损坏的数据。当上传一个文件会通过纠删码算法计算对文件进行分块存储,分成4个数据块,4个校验块,数据块和校验块会分散的存储在这8块硬盘上。

只要挂掉的节点不超过一半,就可以继续使用。

访问下面的地址:

192.168.101.65:9001/login

 

在xuecheng-plus-media的xuecheng-plus-media-service下的pom.xml中写入如下代码:

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>8.4.3</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.8.1</version>
</dependency>

创建一个testbucket,记得把访问方式改为公有

在xuecheng-plus-media的xuecheng-plus-media-servicesrc下创建test包,然后在test下创建com.xuecheng.media包,然后在media包下面创建MinioTest类,然后写入如下的代码:

//测试minio的sdk
public class MinioTest {
    MinioClient minioClient =
            MinioClient.builder()
                    .endpoint("http://192.168.101.65:9000")
                    .credentials("minioadmin","minioadmin")
                    .build();
    @Test
    public void test_upload() throws Exception{
        //上传文件的参数信息
        UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                .bucket("testbucket") //桶
                .filename("C:\\xuechengzaixian\\1.mp4")//指定本地文件路径
                .object("1.mp4")
                .build();//对象名
        //上传文件
        minioClient.uploadObject(uploadObjectArgs);
    }
}

点击运行之后,可以看到1.mp4文件已经成功上传。

可以通过设置多层目录,来把文件存放在目录中。 

.object("test/01/1.mp4")

删除文件的代码如下:

@Test
public void test_delete() throws Exception{

    RemoveObjectArgs removeObjectArgs = RemoveObjectArgs.builder()
            .bucket("testbucket")
            .object("1.mp4")
            .build();

    //上传文件
    minioClient.removeObject(removeObjectArgs);
}

下载文件,从minio中下载文件到本地的某一目录:

//查询文件,从minio中下载
    @Test
    public void test_getFile() throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
        GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket("testbucket").object("test/01/1.mp4").build();
        FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
        //指定输出流
        FileOutputStream outputStream = new FileOutputStream(new File("C:\\xuechengzaixian\\1.mp4"));
        IOUtils.copy(inputStream,outputStream);
        //校验文件的完整性对文件内容进行md5
        String source_md5 = DigestUtils.md5Hex(inputStream); //minio中文件的md5
        String local_md5 = DigestUtils.md5Hex(new FileInputStream(new File("C:\\code\\result.mp4")));
        if(source_md5.equals(local_md5)){
            System.out.println("下载成功");
        }
    }

可以用md5来校验文件下载是否完整,有无缺损或者被破坏。

现在需要比较的是本地流和输出流文件的区别。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1439072.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RK3568笔记十二:Zlmedia拉流显示测试

若该文为原创文章&#xff0c;转载请注明原文出处。 Zlmediakit功能很强大&#xff0c;测试一下拉流&#xff0c;在通过解码显示。 一、环境 1、平台&#xff1a;rk3568 2、开发板:ATK-RK3568正点原子板子 3、环境&#xff1a;buildroot 测试的代码在GitHub - airockchip/…

ubuntu20安装mongodb

方法一&#xff1a;直接安装(命令是直接从mongo官网Install MongoDB Community Edition on Ubuntu — MongoDB Manual复制的&#xff09; cat /etc/lsb-release sudo apt-get install -y gnupg curl curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | \sudo gp…

VS Code中主程序C文件引用了另一个.h头文件,编译时报错找不到函数

目录 一、问题描述二、问题原因三、解决方法四、扩展五、通过CMake进行配置 一、问题描述 VS Code中主程序C文件引用了另一个.h头文件&#xff0c;编译时报错找不到函数 主程序 main.c #include <stdio.h> #include "sumaa.h"int main(int, char**){printf(&q…

秘塔科技推出AI搜索产品「秘塔AI搜索」

近日&#xff0c;国内一家人工智能科技公司&#xff08;秘塔科技&#xff09;推出了一款AI搜索产品——秘塔AI搜索&#xff0c;能够大幅提升搜索效率&#xff0c;解决日常生活、工作学习等场景中遇到的各类搜索需求。 秘塔AI搜索官网&#xff1a;https://metaso.cn/ 相较于传统…

Java 学习和实践笔记(2)

今天的学习进度&#xff1a; 注册并下载安装好了Java 8&#xff0c;之后进行以下配置。 1&#xff09;path 是一个常见的环境变量&#xff0c;它告诉系统除了在当前的目标下妹寻找此程序外&#xff0c;还可以到path指定的目录下找。 2&#xff09;Java Home 为以后其他的软…

FastAdmin西陆房产系统(xiluHouse)全开源

应用介绍 一款基于FastAdminThinkPHPUniapp开发的西陆房产管理系统&#xff0c;支持小程序、H5、APP&#xff1b;包含房客、房东(高级授权)、经纪人(高级授权)三种身份。核心功能有&#xff1a;新盘销售、房屋租赁、地图找房、房源代理(高级授权)、在线签约(高级授权)、电子合同…

MATLAB环境下用于提取冲击信号的几种解卷积方法

卷积混合考虑了信号的时延&#xff0c;每一个单独源信号的时延信号都会和传递路径发生一 次线性瞬时混合&#xff1b;解卷积的过程就是找一个合适的滤波器&#xff0c;进行反卷积运算&#xff0c;得到源信号的近似解。 声音不可避免的会发生衍射、反射等现象&#xff0c;所以&…

(注解配置AOP)学习Spring的第十七天

基于注解配置的AOP 来看注解式开发 : 先把目标与通知放到Spring里管理 : Service("userService") public class UserServiceImpl implements UserService {Overridepublic void show1() {System.out.println("show1......");}Overridepublic void show2…

Elasticsearch:使用 LangChain 文档拆分器进行文档分块

使用 Elasticsearch 嵌套密集向量支持 这个交互式笔记本将&#xff1a; 将模型 “sentence-transformers__all-minilm-l6-v2” 从 Hugging Face 加载到 Elasticsearch ML Node 中使用 LangChain 分割器将段落分块成句子&#xff0c;并使用嵌套密集向量将它们索引到 Elasticse…

【RL】Bellman Equation (贝尔曼等式)

Lecture2: Bellman Equation State value 考虑grid-world的单步过程&#xff1a; S t → A t R t 1 , S t 1 S_t \xrightarrow[]{A_t} R_{t 1}, S_{t 1} St​At​ ​Rt1​,St1​ t t t, t 1 t 1 t1&#xff1a;时间戳 S t S_t St​&#xff1a;时间 t t t时所处的sta…

基于蒙特卡洛的电力系统可靠性分析matlab仿真,对比EDNS和LOLP

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 1.课题概述 电力系统可靠性是指电力系统按可接受的质量标准和所需数量不间断地向电力用户供应电力和电能量的能力的量度&#xff0c;包括充裕度和安全性两个方面。发电系统可靠性是指统一并网的全部发电机…

【RT-DETR有效改进】重参数化模块DiverseBranchBlock助力特征提取(附代码 + 修改教程)

&#x1f451;欢迎大家订阅本专栏&#xff0c;一起学习RT-DETR&#x1f451; 一、本文介绍 本文给大家带来的是改进机制是一种替换多元分支模块&#xff08;Diverse Branch Block&#xff09;&#xff0c;Diverse Branch Block (DBB) 是一种用于增强卷积神经网络性能的结构…

Vue前端框架--Vue工程项目问题总结{脚手架 Vue-cli}

Vue脚手架部署问题总结 我所遇到的一共两大问题 只有先执行npm install之后 才能run serve 否则会报错 vue-cli-serve不是内部或者外部的命令&#xff0c;也不是可运行的程序或者批处理文件的错误 1. 运行npm install会报错 2. 运行npm run serve报错 nodejs官网为 https://no…

算法之双指针系列1

目录 一&#xff1a;双指针的介绍 1&#xff1a;快慢指针 2&#xff1a;对撞指针 二&#xff1a;对撞指针例题讲述 一&#xff1a;双指针的介绍 在做题中常用两种指针&#xff0c;分别为对撞指针与快慢指针。 1&#xff1a;快慢指针 简称为龟兔赛跑算法&#xff0c;它的基…

Unity引擎学习笔记之【动画层操作】

动画层Animation Layer 一、动画器的三个基本状态 1. Any State&#xff08;任意状态&#xff09; “Any State”&#xff08;任意状态&#xff09;&#xff1a;这个状态可以用来连接多个状态机的任意状态转换。在动画控制器中&#xff0c;你可以使用“Any State”作为过渡条…

前端架构: 从vue-cli探究脚手架原理

从使用角度理解什么是脚手架 脚手架本质是一个操作系统的客户端 在终端中去执行一个命令&#xff0c;这个命令本身它就是一个客户端我们其实可以把脚手架理解为操作系统的一个客户端通过命令去执行它的时候&#xff0c;这个命令往往是这样的一个构造&#xff0c;如下 比如&…

CTFshow web(php命令执行 37-40)

?ceval($_GET[shy]);&shypassthru(cat flag.php); #逃逸过滤 ?cinclude%09$_GET[shy]?>&shyphp://filter/readconvert.base64-encode/resourceflag.php #文件包含 ?cinclude%0a$_GET[cmd]?>&cmdphp://filter/readconvert.base64-encode/…

npm 下载报错

报错信息 : 证书过期 (CERT_HAS_EXPIRED) D:\Apps\nodejs-v18.16.1\npx.cmd --yes create-next-app"latest" . --ts npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED npm ERR! request to https://registry.npm.taobao.org/create-next-app failed…

深入探索 Express.js 的高级特性

引言 Express.js 是一个基于 Node.js 平台的 Web 开发框架&#xff0c;旨在提供一种简单、易于使用的方式来创建 Web 应用程序。由于其灵活性和可扩展性&#xff0c;它已经成为了 Node.js 社区最受欢迎的框架之一。在本文中&#xff0c;我们将重点介绍 Express.js 的高级特性&…

157基于matlab的GVF-snake算法能自动收敛到目标区域

基于matlab的GVF-snake算法能自动收敛到目标区域。关键技术GVF snake模型算法matlab源程序&#xff0c;GVF是根据光流场原理,利用变分方法,从图像中得到的一种向量场,该向量场被称为梯度矢量流(GVF)场。 Snake模型称为动态轮廓模型&#xff08;Active Contour Model&#xff0…