学成在线项目笔记第一期

项目介绍

学成在线项目借鉴了MOOC(大型开放式网络课程,即MOOC(massive open online courses))的设计思想,是一个提供IT职业课程在线学习的平台,它为即将和已经加入IT领域的技术人才提供在线学习服务,用户通过在线学习、在线练习、在线考试等学习内容,最终掌握所学的IT技能,并能在工作中熟练应用。

在线教育的模式出现多种多样,包括:B2C、C2C、B2B2C等业务模式。学成在线采用B2B2C业务模式,即向企业或个人在线教育平台提供教学服务,老师和学生通过平台完成整个教学和学习的过程,市场上类似的平台有:网易云课堂、腾讯课堂等,学成在线的特点是IT职业课程在线教学。

功能模块及展示(面试介绍)

项目包括门户、个人学习中心、教学机构管理平台、运营平台、社交系统、系统管理6个模块 。

image-20230119135856835

本项目主要包括三类用户角色:学生、教学机构的老师、平台运营人员。

主要讲解下边的业务流程:

1、教学机构的老师登录教学管理平台,编辑课程信息,发布自己的课程。

2、平台运营人员登录运营平台审核课程、视频等信息,审核通过后课程方可发布。

​ 课程编辑与发布流程:

image-20230119140020866

3、课程发布后学生登录平台进行选课、在线学习。免费课程可直接学习,收费课程需要下单购买。

​ 学生选课流程:

image-20230119140053650

项目的技术架构

项目技术架构

学成在线项目采用当前流行的前后端分离架构开发,由以下流程来构成:用户层、CDN内容分发和加速、负载均衡、UI层、微服务层、数据层。

image-20230119140215343

项目架构列表

序号 名称 功能描述
1 用户层 用户层描述了本系统所支持的用户类型包括:pc用户、app用户、h5用户。pc用户通过浏览器访问系统、app用户通过android、ios手机访问系统,H5用户通过h5页面访问系统。
2 CDN CDN全称Content Delivery Network,即内容分发网络,本系统所有静态资源全部通过CDN加速来提高访问速度。系统静态资源包括:html页面、js文件、css文件、image图片、pdf和ppt及doc教学文档、video视频等。
3 负载均衡 系统的CDN层、UI层、服务层及数据层均设置了负载均衡服务,上图仅在UI层前边标注了负载均衡。 每一层的负载均衡会根据系统的需求来确定负载均衡器的类型,系统支持4层负载均衡+7层负载均衡结合的方式,4层负载均衡是指在网络传输层进行流程转发,根据IP和端口进行转发,7层负载均衡完成HTTP协议负载均衡及反向代理的功能,根据url进行请求转发。
4 UI层 UI层描述了系统向pc用户、app用户、h5用户提供的产品界面。根据系统功能模块特点确定了UI层包括如下产品界面类型: 1)面向pc用户的门户系统、学习中心系统、教学管理系统、系统管理中心。 2)面向h5用户的门户系统、学习中心系统。 3)面向app用户的门户系统、学习中心系统。
5 微服务层 微服务层将系统服务分类三类:业务服务、基础服务、第三方代理服务。 业务服务:主要为学成在线核心业务提供服务,并与数据层进行交互获得数据。 基础服务:主要管理学成在线系统运行所需的配置、日志、任务调度、短信等系统级别的服务。 第三方代理服务:系统接入第三方服务完成业务的对接,例如认证、支付、视频点播/直播、用户认证和授权。
6 数据层 数据层描述了系统的数据存储的内容类型,关系性数据库:持久化的业务数据使用MySQL。 消息队列:存储系统服务间通信的消息,本身提供消息存取服务,与微服务层的系统服务连接。 索引库:存储课程信息的索引信息,本身提供索引维护及搜索的服务,与微服务层的系统服务连接。 缓存:作为系统的缓存服务,作为微服务的缓存数据便于查询。 文件存储:提供系统静态资源文件的分布式存储服务,文件存储服务器作为CDN服务器的数据来源,CDN上的静态资源将最终在文件存储服务器上保存多份。

项目流程说明

  1. 用户可以通过pc、手机等客户端访问系统进行在线学习。
  2. 系统应用CDN技术,对一些图片、CSS、视频等资源从CDN调度访问。
  3. 所有的请求全部经过负载均衡器。
  4. 对于PC、H5等客户端请求,首先请求UI层,渲染用户界面。
  5. 客户端UI请求服务层获取进行具体的业务操作。
  6. 服务层将数据持久化到数据库。

项目技术栈

image-20230119140407843

项目开发环境搭建

开发工具版本

服务端开发基础工具版本列表:

开发工具 版本号
IntelliJ-IDEA 2021.x以上版本
Java JDK-1.8.x
Maven 3.6.x以上版本
Mysql 8.x
VMware-workstation 15.x
CentOS 7.x
nacos-server- 1.4.1
rabbitmq 3.8.34
redis 6.2.7
xxl-job-admin: 2.3.1

工程结构关系

学成在线使用 Maven 来进行项目的管理和构建。整个项目分为三大类工程:父工程、基础工程 和微服务工程。

image-20230119140521100

每一种类的工程都有不同的作用:

  • 父工程(parent)

    • 对依赖包的版本进行管理
    • 本身为Pom工程,对子工程进行聚合管理
  • 基础工程(base)

    • 继承父类工程

    • 提供基础类库

    • 提供工具类库

  • 微服务工程

    • 分别从业务、技术方面划分模块,每个模块构建为一个微服务。
    • 每个微服务工程依赖基础工程,间接继承父工程。
    • 包括:内容管理服务、媒资管理服务、搜索服务、缓存服务、消息服务等。

Git提交设置

  1. 创建Git仓库存档,在项目工程的根目录(xuecheng-plus) 添加 .gitignore文件,编辑内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**
!**/src/test/**

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
logs/

### VS Code ###
.vscode/

  1. 确保 .gitignore文件 生效,打开idea终端 或者 用 git bash打开命令行(默认大家已经掌握了哈),在命令行输入以下命令:
1
2
3
git rm -r --cached .
git add .
git commit -m 'update .gitignore'
  1. 然后重新回到idea提交,多余的文件就会消失啦~~~

构建父工程

父程的职责是对依赖包的版本进行管理

创建父工程

为了对代码更好的进行权限管理,这里我们单独创建父工程。

使用idea打开工程目录,进入工程结构界面。

  1. 点击File–>Project Structure:

image-20230119140842005

  1. 进入Project Structure,首先检查jdk是否配置正确,并进行配置。(注意:取名这一块按个人情况处理!!!
image-20230119140856895
  1. 进入Modules界面,新建模块
image-20230119141022572
  1. 进入新建模块界面,选择Spring Initializr,填写模块的信息。

注意:这里Server URL默认是start.spring.io,如果连接不上可换为start.aliyun.com。

image-20230119141101069
  1. 填写模块信息注意坐标信息填写正确,填写完毕,点击Next,进入下一步不用选择任何依赖,点击“Create”,模块创建成功
image-20230119141157358
  1. 把里边多余的文件和目录删除,只保留以下文件

image-20230119141228962

依赖管理定义

编辑xuecheng-plus-parent父工程的依赖管理 。父工程中没有代码,不用去依赖其它的包,它的作用是限定其它子工程依赖包的版本号,即在dependencyManagement 中去编辑即可。

注意:如果自己的 maven仓库 没有下载过当前版本的依赖包,要先将 dependencyManagement 注释掉,不然有可能会出现依赖包下载失败的情况

  1. 确定父工程为一个pom工程,在pom.xml中添加如下内容:
1
<packaging>pom</packaging>
  1. 确定项目所以依赖的包及其版本号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
<spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
<org.projectlombok.version>1.18.8</org.projectlombok.version>
<javax.servlet-api.version>4.0.1</javax.servlet-api.version>
<fastjson.version>1.2.83</fastjson.version>
<druid-spring-boot-starter.version>1.2.8</druid-spring-boot-starter.version>
<mysql-connector-java.version>8.0.30</mysql-connector-java.version>
<mybatis-plus-boot-starter.version>3.4.1</mybatis-plus-boot-starter.version>
<commons-lang.version>2.6</commons-lang.version>
<minio.version>8.4.3</minio.version>
<xxl-job-core.version>2.3.1</xxl-job-core.version>
<swagger-annotations.version>1.5.20</swagger-annotations.version>
<commons-lang3.version>3.10</commons-lang3.version>
<okhttp.version>4.8.1</okhttp.version>
<swagger-spring-boot-starter.version>1.9.0.RELEASE</swagger-spring-boot-starter.version>
<elasticsearch.version>7.12.1</elasticsearch.version>
</properties>
  1. 编写 dependencyManagement 来限定所依赖包的版本(注意先注释掉dependencyManagement再导包)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
<!-- lombok,简化类的构建-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</dependency>
<!-- mapstruct 代码生成器,简化java bean之间的映射 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>
<!-- Servlet 容器管理 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax.servlet-api.version}</version>
<scope>provided</scope>
</dependency>
<!-- fastjson ,json解析工具 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- druid 连接池管理 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-spring-boot-starter.version}</version>
</dependency>

<!-- mySQL数据库驱动包管理 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<!-- mybatis plus 集成Spring Boot启动器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
</dependency>

<!-- mybatis plus 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
</dependency>

<!-- 工具类管理 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<!-- 分布式文件系统 minIO的客户端API包 -->
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
<!--google推荐的一套工具类库-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>25.0-jre</version>
</dependency>
<!--分布式任务调度-->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${xxl-job-core.version}</version>
</dependency>
<!--Spring boot单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>com.spring4all</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>${swagger-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>${elasticsearch.version}</version>
</dependency>

<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>${elasticsearch.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
  1. 编辑打包插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
    <build>
<finalName>${project.name}</finalName>
<!--编译打包过虑配置-->
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<!--打包插件-->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<!--指定项目源码jdk的版本-->
<source>1.8</source>
<!--指定项目编译后的jdk的版本-->
<target>1.8</target>
<!--配置注解预编译-->
<annotationProcessorPaths>
<!-- <path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>-->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

<!--责处理项目资源文件并拷贝到输出目录,如果有额外的资源文件目录则需要配置-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<encoding>utf-8</encoding>
<!--使用默认分隔符,resource中可以使用分割符定义过虑的路径-->
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</build>
  1. 工程创建完成提交至git存档

构建基础工程

基础工程的职责是提供一些系统架构所需要的基础类库以及一此工具类库

创建基础工程

创建的过程同父工程的创建过程:

image-20230119141848565

中间步骤参考 parent工程的操作,最后的基础工程展示:

image-20230119141937207

依赖管理定义

需要注意的是xuecheng-plus-base的父工程为xuecheng-plus-parent

xuecheng-plus-base的pom.xml修改为如下状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<?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-parent</artifactId>
<groupId>com.xucheng</groupId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../xuecheng-plus-parent</relativePath>
</parent>
<artifactId>xuecheng-plus-base</artifactId>
<name>xuecheng-plus-base</name>
<description>xuecheng-plus-base</description>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- fast Json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>

<!-- servlet Api 依赖 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>

<!-- 通用组件 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!--根据扩展名取mimetype-->
<dependency>
<groupId>com.j256.simplemagic</groupId>
<artifactId>simplemagic</artifactId>
<version>1.17</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>

<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
</dependencies>
</project>

数据库环境搭建

创建项目数据库,打开黑马给的sql文件,导包即可(注意MySQL版本!!!)

注意:项目中有用到 MySQL 8.0以上的递归方法(with recursive),如果自己的数据库版本是 5.7 的,要注意换方法(我都有写,别钻牛角尖就行)

内容管理模块content

本项目作为一个大型的在线教育平台,其内容管理模块主要对课程及相关内容进行管理,包括:课程的基本信息、课程图片、课程师资信息、课程的授课计划、课程视频、课程文档等内容的管理。

创建模块工程

模块工程结构

在第一章节创建了项目父工程、项目基础工程,如下图:

image-20230119143625673

接下来要创建 内容管理模块(xuecheng-plus-content) 的工程结构。

本项目是一个前后端分离项目,前端与后端开发人员之间主要依据接口进行开发。

下图是前后端交互的流程图:

1、前端请求后端服务提供的接口。(通常为http协议 )

2、后端服务的控制层Controller接收前端的请求。

3、Contorller层调用Service层进行业务处理。

4、Service层调用Dao持久层对数据持久化。

image-20230119143727085

整个流程分为前端、接口层、业务层三部分,所以模块工程的结构如下图所示:

image-20230119143752403
  • xuecheng-plus-content:内容管理模块工程,负责聚合xuecheng-plus-content-api、xuecheng-plus-content-service、xuecheng-plus-content-model。

  • xuecheng-plus-content-api:接口工程,为前端提供接口。

  • xuecheng-plus-content-service: 业务工程,为接口工程提供业务支撑。

  • xuecheng-plus-content-model: 数据模型工程,存储数据模型类、数据传输类型等。

结合项目父工程、项目基础工程后,如下图:

image-20230119143828657

创建工程

  1. 创建内容管理模块的总工程xuecheng-plus-content
image-20230119144037260
  1. 删除多余的文件后,对 content 的pom文件进行修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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-parent</artifactId>
<groupId>com.xuecheng</groupId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../xuecheng-plus-parent</relativePath>
</parent>
<artifactId>xuecheng-plus-content</artifactId>
<name>xuecheng-plus-content</name>
<description>xuecheng-plus-content</description>
<packaging>pom</packaging>

<modules>
<module>xuecheng-plus-content-api</module>
<module>xuecheng-plus-content-model</module>
<module>xuecheng-plus-content-service</module>
</modules>
</project>

由于api、service和 model 这三个子工程还没有创建所以modules报错。

  1. 在content下创建依法炮制 api、service和 model 三个工程
image-20230119144419600 image-20230119144440068 image-20230119144352059

创建完成后,工程的结构图如下:

image-20230119144822026

  1. 修改 model 工程的 pom 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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>

  1. 修改 service 工程的 pom 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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>
  1. 修改 api 工程的 pom 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-content-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>

</project>

到此内容管理模块的四个工程的基础搭建完毕。

课程查询接口实现

需求分析

  1. 教学机构人员点击课程管理首先进入课程查询界面
image-20230119145143924
  1. 在课程进行列表查询页面输入查询条件查询课程信息

    • 当不输入查询条件时输入全部课程信息。

    • 输入查询条件查询符合条件的课程信息。

    • 约束:本教学机构查询本机构的课程信息。

image-20230119145219489

数据模型

课程查询功能涉及的数据表有课程基本信息表(course_base)、课程计划表(course_category)

image-20230119145403591

功能分析:

  1. 查询条件:
  • 包括:课程名称、课程审核状态、课程发布状态

  • 课程名称:可以模糊搜索

  • 课程审核状态:未提交、已提交、审核通过、审核未通过

  • 课程发布状态:未发布、已发布、已下线

  • 因为是分页查询所以查询条件中还要包括当前页码、每页显示记录数。

  1. 查询结果:
  • 查询结果包括:课程id、课程名称、任务数、创建时间、审核状态、类型,从结果上看基本来源于课程基本信息表,任务数需要关联教学计划学查询。

  • 补充:因为是分页查询所以查询结果中还要包括总记录数、当前页、每页显示记录数。

model–生成PO类(实体类)

生成实体类

我觉得黑马的方法过于麻烦,还是使用 MyBatis-Plus插件偷个懒吧。。。

选中表后右键打开:

image-20230119145638230

选择路径以及命名方式:

image-20230119145850093

选一下需要的一些参数:

image-20230119145924980

点击finish后,就可以在自己定义的文件路径下找到po类需要的所有文件:(一次全生成也行,用到哪个生成哪个也行)

image-20230119150045245

将所需的文件 剪切粘贴 到对应 model、service 位置即可。

移动实体类

  1. 将生成的 PO类拷贝至 content-model 工程下:(这里是一次全生成了)
image-20230119150608611
  1. 添加MyBatisPlus框架的依赖,消除PO类编译报错的情况:打开 model工程 的 pom 文件,添加依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<dependencies>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--存在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>
</dependencies>

生成DTO

DTO即数据传输对象(DTO)(Data Transfer Object),用于接口层和业务层之间传输数据。

在内容管理的model工程中定义课程查询参数模型类:

image-20230119151516194

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.xuecheng.contentModel.dto;

import lombok.Data;
import lombok.ToString;

/**
* @author xioaming
* @version 1.0
* @description 课程查询参数Dto:数据传输对象(DTO)(Data Transfer Object),用于接口层和业务层之间传输数据
* @date 2023/1/16 15:39
*/
@Data
@ToString
public class QueryCourseParamsDto {
//审核状态
private String auditStatus;
//课程名称
private String courseName;
//发布状态
private String publishStatus;
}

定义请求模型类(分页查询)

由于分页查询这一类的接口在项目较多,针对分页查询的参数(当前页码、每页显示记录数)单独在 base基础工程 中定义。(通用类)

image-20230119151220911

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.xuecheng.base.model;

import lombok.Data;
import lombok.ToString;

/**
* @author xioaming
* @version 1.0
* @description 分页查询通用参数
* @date 2023/1/16 15:36
*/
@Data
@ToString
public class PageParams {
//当前页码默认值
public static final long DEFAULT_PAGE_CURRENT = 1L;
//每页记录数默认值
public static final long DEFAULT_PAGE_SIZE = 10L;

//当前页码
private Long pageNo = DEFAULT_PAGE_CURRENT;

//每页记录数默认值
private Long pageSize = DEFAULT_PAGE_SIZE;

public PageParams(){

}

public PageParams(long pageNo,long pageSize){
this.pageNo = pageNo;
this.pageSize = pageSize;
}
}

在base工程的 pom 文件中添加依赖:

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

定义响应模型类

针对分页查询结果经过分析也存在固定的数据和格式,所以在base工程定义一个基础的模型类

image-20230119151820564

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.xuecheng.base.model;

import lombok.Data;
import lombok.ToString;

import java.io.Serializable;
import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 分页查询结果模型类
* @date 2023/1/16 15:41
*/
@Data
@ToString
public class PageResult<T> implements Serializable {
// 数据列表
private List<T> items;

//总记录数
private long counts;

//当前页码
private long page;

//每页记录数
private long pageSize;

public PageResult(List<T> items, long counts, long page, long pageSize) {
this.items = items;
this.counts = counts;
this.page = page;
this.pageSize = pageSize;
}
}

模型类中定义了List属性,此属性用于存放数据列表,且支持泛型,课程查询接口的返回类型可以使用生成的PO类进行返回,即:

1
2
//泛型中填写CourseBase类型。
PageResult<CourseBase>

service-接口开发

DAO开发

  1. 前边生成PO类的同时将Mapper接口及xml文件也生成了,将mapper接口和xml文件拷贝至 service 工程下:

我个人更喜欢将 Mapper 的 xml 文件放到 resource 中,目前没发现什么问题

image-20230119154748179

注意:拷贝到 service 工程后,记得修改 Mapper的 xml文件的 映射路径

  1. 导入系统管理数据库,创建系统管理服务的数据库system,执行课程资料中的xcplus_system.sql脚本
image-20230119160155580

添加依赖

service工程的 pom 文件中添加mybatis-plus的支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<dependencies>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-plus-content-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<!-- mybatis plus的依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- Spring Boot 集成 Junit -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 排除 Spring Boot 依赖的日志包冲突 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- Spring Boot 集成 log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

</dependencies>

定义分页拦截器

配置扫描mapper及分页插件:

image-20230119155118329
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.xuecheng.contentService.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author xioaming
* @version 1.0
* @description Mybatis-Plus 配置
* @date 2023/1/16 16:24
*/
@Configuration
@MapperScan("com.xuecheng.contentService.mapper")
public class MybatisPlusConfig {

/**
* 定义分页拦截器
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}

配置resource文件

从课程资料中获取 log4j2-dev.xml,并创建 application.yml

image-20230119155250198

application.yml的配置信息如下:

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: content-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://数据库信息:3306/xconline_content?serverTimezone=UTC&userUnicode=true&useSSL=false
username: 用户名
password: 密码
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml

测试Mapper连接性

在 service 工程的 test 中创建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.xuecheng.contentService;

import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.contentModel.dto.CourseCategoryTreeDto;
import com.xuecheng.contentModel.dto.QueryCourseParamsDto;
import com.xuecheng.contentModel.po.CourseBase;
import com.xuecheng.contentModel.po.CourseCategory;
import com.xuecheng.contentService.mapper.CourseBaseMapper;
import com.xuecheng.contentService.mapper.CourseCategoryMapper;
import com.xuecheng.contentService.service.CourseBaseInfoService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class ContentServiceApplicationTests {

@Autowired
CourseBaseMapper courseBaseMapper;

@Test
void contextLoads() {
}

@Test
void testCourseBaseMapper(){
CourseBase courseBase = courseBaseMapper.selectById(74L);
Assertions.assertNotNull(courseBase);
}
}

运行测试类的测试方法进行测试。

创建service接口

image-20230119160407248

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.xuecheng.contentService.service;

import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.contentModel.dto.AddCourseDto;
import com.xuecheng.contentModel.dto.CourseBaseInfoDto;
import com.xuecheng.contentModel.dto.QueryCourseParamsDto;
import com.xuecheng.contentModel.po.CourseBase;

/**
* @author xioaming
* @version 1.0
* @description 课程基本信息管理业务接口
* @date 2023/1/16 16:33
*/
public interface CourseBaseInfoService {
/**
* @description 课程查询接口
* @param pageParams 分页参数
* @param queryCourseParamsDto 条件
* @author xiaoming
* @date 2023/1/16 16:34
*/
PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto);

}

创建service接口实现类

image-20230119160457824
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
package com.xuecheng.contentService.service.impl;
import java.math.BigDecimal;
import java.time.LocalDateTime;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.contentModel.dto.AddCourseDto;
import com.xuecheng.contentModel.dto.CourseBaseInfoDto;
import com.xuecheng.contentModel.dto.QueryCourseParamsDto;
import com.xuecheng.contentModel.po.CourseBase;
import com.xuecheng.contentModel.po.CourseCategory;
import com.xuecheng.contentModel.po.CourseMarket;
import com.xuecheng.contentService.mapper.CourseBaseMapper;
import com.xuecheng.contentService.mapper.CourseCategoryMapper;
import com.xuecheng.contentService.mapper.CourseMarketMapper;
import com.xuecheng.contentService.service.CourseBaseInfoService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;


/**
* @author xioaming
* @version 1.0
* @description 课程信息管理业务接口实现类
* @date 2023/1/16 16:37
*/
@Service
public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {

@Autowired
CourseBaseMapper courseBaseMapper;

/**
* @description 课程查询接口
* @param pageParams 分页参数
* @param queryCourseParamsDto 条件
* @author xiaoming
* @date 2023/1/16 16:34
*/
@Override
public PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) {
//构建查询条件对象
LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
//构建查询条件,根据课程名称查询
queryWrapper.like(StringUtils.isNotEmpty(queryCourseParamsDto.getCourseName()), CourseBase::getName, queryCourseParamsDto.getCourseName());
//构建查询条件,根据课程审核状态查询
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamsDto.getAuditStatus()), CourseBase::getAuditStatus, queryCourseParamsDto.getAuditStatus());
//构建查询条件,根据课程发布状态查询
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParamsDto.getPublishStatus()), CourseBase::getStatus, queryCourseParamsDto.getPublishStatus());

//分页对象
Page<CourseBase> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper);
// 获取数据列表
List<CourseBase> list = pageResult.getRecords();
// 获取数据总数
long total = pageResult.getTotal();
// 构建结果集
PageResult<CourseBase> courseBasePageResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
return courseBasePageResult;
}
}

注意:视频内 老师故意将 课程发布状态查询 写错了,要将 课程发布状态查询 的CourseBase::getAuditStatus 改成 CourseBase::getStatus,我贴的代码已经修改过了

定义测试类测试 queryCourseBaseList

1
2
3
4
5
6
@Test
void testCourseBaseInfoService(){
PageParams pageParams = new PageParams();
PageResult<CourseBase> courseBasePageResult = courseBaseInfoService.queryCourseBaseList(pageParams, new QueryCourseParamsDto());
System.out.println(courseBasePageResult);
}

测试,观察结果是否正确

api–定义接口

添加依赖

api工程 添加依赖:

注意:视频中黑马使用的是 swapper,我觉得太麻烦了,使用了 knife4j 实现接口文档,功能是一样的哈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<dependencies>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-content-model</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.xuecheng</groupId>
<artifactId>xuecheng-content-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--cloud的基础环境包-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<!-- Spring Boot 的 Spring Web MVC 集成 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 排除 Spring Boot 依赖的日志包冲突 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Boot 集成 log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

<!--引入Knife4j的官方start包,Swagger2基于Springfox2.10.5项目-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<!--使用Swagger2-->
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>

配置knife4j

如果使用的是swapper,这一部分可以省去!!!

api工程 中创建config目录,配置knife4j:(yupi的伙伴匹配系统里有现成的,鱼皮yyds)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.xuecheng.contentApi.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/**
* @author xioaming
* @version 1.0
* @description Knife4j配置依赖
* @date 2023/1/16 15:55
*/
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfiguration {

@Bean(value = "dockerBean")
public Docket dockerBean() {
//指定使用Swagger2规范
Docket docket=new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
//描述字段支持Markdown语法
.description("# Knife4j RESTful APIs")
.termsOfServiceUrl("https://doc.xiaominfo.com/")
.contact("xiaoymin@foxmail.com")
.version("1.0")
.build())
//分组名称
.groupName("课程管理功能测试中心")
.select()
//这里指定Controller扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.github.xiaoymin.knife4j.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.xuecheng.contentApi.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;

/**
* @author xioaming
* @version 1.0
* @description Swagger配置依赖
* @date 2023/1/16 15:58
*/
@Configuration
@EnableSwagger2WebMvc
public class SwaggerConfig {
@Bean(value = "defaultApi2")
public Docket defaultApi2(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//这里一定要标注你控制器的位置
.apis(RequestHandlerSelectors.basePackage("com.xuecheng.contentApi.controller"))//每个人的不一定一样,记得改
.paths(PathSelectors.any())
.build();
}

/**
* api 信息
* @return
*/
private ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("课程管理功能测试中心")
.description("课程管理功能测试文档")
.termsOfServiceUrl("https://www.xmingblog.top")
.contact(new Contact("小明","https://www.xmingblog.top","1212121@qq.com"))
.version("1.0")
.build();
}
}

定义controller方法

api工程 中创建 controller方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.xuecheng.contentApi.controller;


import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.contentModel.dto.AddCourseDto;
import com.xuecheng.contentModel.dto.CourseBaseInfoDto;
import com.xuecheng.contentModel.dto.QueryCourseParamsDto;
import com.xuecheng.contentModel.po.CourseBase;
import com.xuecheng.contentService.service.CourseBaseInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
* @author xioaming
* @version 1.0
* @description 课程信息编辑接口
* @date 2023/1/16 15:45
*/
@RestController
public class CourseBaseInfoController {

@Autowired
CourseBaseInfoService courseBaseInfoService;

@PostMapping("/course/list")
public PageResult<CourseBase> list(PageParams pageParams, @RequestBody QueryCourseParamsDto queryCourseParamsDto){
return courseBaseInfoService.queryCourseBaseList(pageParams, queryCourseParamsDto);
}
}

注意:和视频有出入,因为视频使用的是 swapper 使用 @Api 可以注释信息,我用的 knife4j 好像不能展示注释信息,就没有添加 @Api 注释

说明:pageParams分页参数通过 url 的key/value传入,queryCourseParams 通过 json 数据传入,使用 @RequestBody 注解将 json 转成QueryCourseParamsDto 对象。

定义启动类

打开 api 工程的 启动类,在 @SpringBootApplication 后添加:(scanBasePackages = "com.xuecheng.*")

我不添加会出现controller扫描不到service方法然后报错的情况

1
2
3
4
5
6
7
8
9
10
11
12
package com.xuecheng.contentApi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication(scanBasePackages = "com.xuecheng.*")
public class ContentApiApplication {

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

注意:还是因为 knife4j 所以不需要加 @EnableSwapper2Doc

添加配置文件

image-20230119153238154

log4j2-dev.xml从课程资料获取,bootstrap.yml内容如下:(knife4j版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
servlet:
context-path: /content
port: 63040
#微服务配置
spring:
application:
name: content-api
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER

# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml

swapper版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
servlet:
context-path: /content
port: 63040
#微服务配置
spring:
application:
name: content-api

# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml


# swagger 文档配置
swagger:
title: "学成在线内容管理系统"
description: "内容系统管理系统对课程相关信息进行业务管理数据"
base-package: com.xuecheng.content
enabled: true
version: 1.0.0

运行启动类

启动 api工程,访问http://localhost:63040/content/doc.html查看接口信息(knife4j)

image-20230119154331582

swapper访问 http://localhost:63040/content/swagger-ui.html 查看接口信息

image-20230119154133350

课程分类查询接口实现

需求分析

在新增课程界面,有三处信息需要选择

image-20230119204901256

课程等级、课程类型来源于数据字典表,此部分的信息前端已从系统管理服务读取。

课程分类信息没有在数据字典表中存储,而是由单独一张课程分类表,存储在内容管理数据库中。

image-20230119204922893

这张表是一个树型结构,通过父结点id将各元素组成一个树。

image-20230119204959785

现在的需求是 需要在内容管理服务中编写一个接口读取该课程分类表的数据,组成一个树型结构返回给前端

model–生成PO类

生成实体类

和上文一样,不多赘述

生成DTO

观察前端的请求记录,通过查阅接口文档,发现此接口要返回全部课程分类,以树型结构返回。

数据格式是一个数组结构,数组的元素即为分类信息,分类信息设计两级分类,第二级的分类是第一级分类中childrenTreeNodes属性,它也是一个数组结构

所以,在content-model中定义一个DTO类表示分类信息的模型类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.xuecheng.contentModel.dto;

import com.xuecheng.contentModel.po.CourseCategory;
import lombok.Data;

import java.io.Serializable;
import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 课程分类树型结点dto
* @date 2023/1/18 20:12
*/
@Data
public class CourseCategoryTreeDto extends CourseCategory implements Serializable {
List childrenTreeNodes;
}

service–接口开发

DAO开发

如何生成一个树型结构的对象?

  1. 可以将数据从数据库读取出来后,在java程序中遍历数据组成一个树型结构对象。

  2. 通过表的自连接查出数据使用mybatis映射成一个树型结构。

项目使用的是第2种方法。

  1. 在CourseCategoryMapper中定义一个mapper方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.xuecheng.contentService.mapper;

import com.xuecheng.contentModel.dto.CourseCategoryTreeDto;
import com.xuecheng.contentModel.po.CourseCategory;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import java.util.List;

/**
* @author 15182
* @description 针对表【course_category(课程分类)】的数据库操作Mapper
* @createDate 2023-01-16 16:07:41
* @Entity com.xuecheng.contentModel.po.CourseCategory
*/
public interface CourseCategoryMapper extends BaseMapper<CourseCategory> {

List<CourseCategoryTreeDto> selectTreeNodes();
}

  1. 找到对应的Mapper.xml文件,添加sql语句:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--通用查询结果列-->
<select id="selectTreeNodes" resultMap="treeNodeResultMap" >
select
one.id one_id,
one.name one_name,
one.parentid one_parentid,
one.orderby one_orderby,
one.label one_label,
two.id two_id,
two.name two_name,
two.parentid two_parentid,
two.orderby two_orderby,
two.label two_label
from course_category one
inner join course_category two on one.id = two.parentid
where one.parentid = 1
and one.is_show = 1
and two.is_show = 1
order by one.orderby,
two.orderby
</select>
  1. 编写ResultMap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 课程分类树型结构查询映射结果 -->
<resultMap id="treeNodeResultMap" type="com.xuecheng.contentModel.dto.CourseCategoryTreeDto">
<!-- 一级数据映射 -->
<id column="one_id" property="id" />
<result column="one_name" property="name" />
<result column="one_label" property="label" />
<result column="one_parentid" property="parentid" />
<result column="one_orderby" property="orderby" />
<!-- 一级中包含多个二级数据:二级的集合数据 -->
<collection property="childrenTreeNodes" ofType="com.xuecheng.contentModel.dto.CourseCategoryTreeDto">
<!-- 二级数据映射 -->
<id column="two_id" property="id" />
<result column="two_name" property="name" />
<result column="two_label" property="label" />
<result column="two_parentid" property="parentid" />
<result column="two_orderby" property="orderby" />
</collection>
</resultMap>

完整的CourseCategoryMapper.xml 的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xuecheng.contentService.mapper.CourseCategoryMapper">

<!-- 课程分类树型结构查询映射结果 -->
<resultMap id="treeNodeResultMap" type="com.xuecheng.contentModel.dto.CourseCategoryTreeDto">
<!-- 一级数据映射 -->
<id column="one_id" property="id" />
<result column="one_name" property="name" />
<result column="one_label" property="label" />
<result column="one_parentid" property="parentid" />
<result column="one_orderby" property="orderby" />
<!-- 一级中包含多个二级数据:二级的集合数据 -->
<collection property="childrenTreeNodes" ofType="com.xuecheng.contentModel.dto.CourseCategoryTreeDto">
<!-- 二级数据映射 -->
<id column="two_id" property="id" />
<result column="two_name" property="name" />
<result column="two_label" property="label" />
<result column="two_parentid" property="parentid" />
<result column="two_orderby" property="orderby" />
</collection>
</resultMap>

<sql id="Base_Column_List">
id,name,label,
parentid,is_show,orderby,
is_leaf
</sql>

<!--通用查询结果列-->
<select id="selectTreeNodes" resultMap="treeNodeResultMap" >
select
one.id one_id,
one.name one_name,
one.parentid one_parentid,
one.orderby one_orderby,
one.label one_label,
two.id two_id,
two.name two_name,
two.parentid two_parentid,
two.orderby two_orderby,
two.label two_label
from course_category one
inner join course_category two on one.id = two.parentid
where one.parentid = 1
and one.is_show = 1
and two.is_show = 1
order by one.orderby,
two.orderby
</select>
</mapper>

  1. 单元测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.xuecheng.contentService;

import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.contentModel.dto.CourseCategoryTreeDto;
import com.xuecheng.contentModel.dto.QueryCourseParamsDto;
import com.xuecheng.contentModel.po.CourseBase;
import com.xuecheng.contentModel.po.CourseCategory;
import com.xuecheng.contentService.mapper.CourseBaseMapper;
import com.xuecheng.contentService.mapper.CourseCategoryMapper;
import com.xuecheng.contentService.service.CourseBaseInfoService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.List;

@SpringBootTest
class ContentServiceApplicationTests {

@Autowired
CourseCategoryMapper courseCategoryMapper;

@Test
void contextLoads() {
}

@Test
void testCourseCategoryMapper(){
List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes();
System.out.println(courseCategoryTreeDtos);
}
}

创建service接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.xuecheng.contentService.service;

import com.xuecheng.contentModel.dto.CourseCategoryTreeDto;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 课程分类service
* @date 2023/1/18 21:07
*/
public interface CourseCategoryService {

/**
* @description 课程分类树形结构查询
* @author xiaoming
* @date 2023/1/18 21:08
*/
public List<CourseCategoryTreeDto> queryTreeNodes();
}

创建service接口实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.xuecheng.contentService.service.impl;

import com.xuecheng.contentModel.dto.CourseCategoryTreeDto;
import com.xuecheng.contentService.mapper.CourseCategoryMapper;
import com.xuecheng.contentService.service.CourseCategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 课程分类查询实现类
* @date 2023/1/18 21:08
*/
@Service
@Slf4j
public class CourseCategoryServiceImpl implements CourseCategoryService {

@Autowired
CourseCategoryMapper courseCategoryMapper;


@Override
public List<CourseCategoryTreeDto> queryTreeNodes() {
return courseCategoryMapper.selectTreeNodes();
}
}

api–定义接口

定义controller方法

在content-api中创建新的controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.xuecheng.contentApi.controller;

import com.xuecheng.contentModel.dto.CourseCategoryTreeDto;
import com.xuecheng.contentModel.po.CourseCategory;
import com.xuecheng.contentService.service.CourseCategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Locale;

/**
* @author xioaming
* @version 1.0
* @description 课程分类相关接口
* @date 2023/1/18 20:07
*/
@RestController
@Slf4j
public class CourseCategoryController {

@Autowired
CourseCategoryService courseCategoryService;

/**
* @description 课程分类查询接口
* @return
* @author xiaoming
* @date 2023/1/18 20:13
*/
@GetMapping("/course-category/tree-nodes")
public List<CourseCategoryTreeDto> queryTreeNodes(){
return courseCategoryService.queryTreeNodes();
}
}

接口测试

使用httpclient测试:定义.http文件

image-20230119211015276

测试无误后进行前后端联调

前后端联调

打开前端工程,进入新增课程页面。查看课程分类下拉框能否正常显示

image-20230119211111023

新增课程接口实现

需求分析

根据前边对内容管理模块的数据模型分析,课程相关的信息有:课程基本信息、课程营销信息、课程图片信息、课程计划、课程师资信息,所以新增一门课程需要完成这几部分信息的填写。

  1. 进入课程查询列表
image-20230119211321183
  1. 点击添加课程,选择课程类型是直播还是录播,课程类型不同课程的授课方式不同
image-20230119211343490
  1. 点击下一步,进入课程基本信息添加界面

​ 本界面分两部分信息,一部分是课程基本信息上,一部分是课程营销信息。

  • 课程基本信息:
image-20230119211443910
  • 课程营销信息:
image-20230119211510810
  1. 填写课程计划信息

​ 课程计划即课程的大纲目录,课程计划分为两级,章节和小节。每个小节需要上传课程视频,用户点击 小节的标题即开始播放视频。如果是直播课程则会进入直播间。

image-20230119211610366
  1. 进入课程师资的管理,在该界面维护该课程的授课老师
image-20230119211703780 image-20230119211716075

至此,一门课程新增完成。

数据模型

通过业务流程可知,一门课程信息涉及:课程基本信息、课程营销信息、课程计划信息、课程师资信息。本节开发新增课程按钮功能, 只向课程基本信息、课程营销信息添加记录。

image-20230119211821725

这两部分信息分别在course_base、course_market两张表存储。当点击保存按钮时向这两张表插入数据。这两张表是一对一关联关系。

新建课程的初始审核状态为“未提交”、初始发布状态为“未发布”。

model–生成DTO

通过查看请求参数,发现请求参数相比 CourseBase模型类不一致,需要定义DTO实现请求:

image-20230119212250879
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.xuecheng.contentModel.dto;

import lombok.Data;

import javax.validation.constraints.NotEmpty;
import java.math.BigDecimal;

/**
* @author xioaming
* @version 1.0
* @description 课程添加DTO
* @date 2023/1/18 21:22
*/
@Data
public class AddCourseDto {

@NotEmpty(message = "课程名称不能为空")
private String name;

@NotEmpty(message = "适用人群不能为空")
private String users;

//课程标签
private String tags;

@NotEmpty(message = "课程分类(大)不能为空")
private String mt;

@NotEmpty(message = "课程分类(小)不能为空")
private String st;

@NotEmpty(message = "课程等级不能为空")
private String grade;

//教学模式
private String teachmode;

//课程介绍
private String description;

//课程图片
private String pic;

@NotEmpty(message = "收费规则不能为空")
private String charge;

//价格
private Float price;

//原价
private Float originalPrice;

//qq
private String qq;

//微信
private String wechat;

//电话
private String phone;

//有效期
private Integer validDays;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.xuecheng.contentModel.dto;

import com.xuecheng.contentModel.po.CourseBase;
import lombok.Data;

/**
* @author xioaming
* @version 1.0
* @description 课程营销基本信息dto
* @date 2023/1/18 21:30
*/
@Data
public class CourseBaseInfoDto extends CourseBase {

/**
* 收费规则,对应数据字典
*/
private String charge;

/**
* 价格
*/
private Float price;


/**
* 原价
*/
private Float originalPrice;

/**
* 咨询qq
*/
private String qq;

/**
* 微信
*/
private String wechat;

/**
* 电话
*/
private String phone;

/**
* 有效期天数
*/
private Integer validDays;

/**
* 大分类名称
*/
private String mtName;

/**
* 小分类名称
*/
private String stName;
}

service–接口开发

创建service接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.xuecheng.contentService.service;

import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.contentModel.dto.AddCourseDto;
import com.xuecheng.contentModel.dto.CourseBaseInfoDto;
import com.xuecheng.contentModel.dto.QueryCourseParamsDto;
import com.xuecheng.contentModel.po.CourseBase;

/**
* @author xioaming
* @version 1.0
* @description 课程基本信息管理业务接口
* @date 2023/1/16 16:33
*/
public interface CourseBaseInfoService {
/**
* @description 课程查询接口
* @param pageParams 分页参数
* @param queryCourseParamsDto 条件
* @author xiaoming
* @date 2023/1/16 16:34
*/
PageResult<CourseBase> queryCourseBaseList(PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto);

/**
* @description 添加课程基本信息
* @param companyId 教学机构id
* @param addCourseDto 课程基本信息
* @return com.xuecheng.contentModel.dto.CourseBaseInfoDto
* @author xiaoming
* @date 2023/1/18 21:32
*/
CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto addCourseDto);
}

创建service接口实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
/**
* @description 添加课程基本信息
* @param companyId 教学机构id
* @param dto 课程基本信息
* @return com.xuecheng.contentModel.dto.CourseBaseInfoDto
* @author xiaoming
* @date 2023/1/18 21:32
*/
@Transactional
@Override
public CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto dto) {
//1、合法性校验
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("收费规则为空");
}

//2、新增对象
CourseBase courseBaseNew = new CourseBase();
//将填写的课程信息赋值给新增对象
BeanUtils.copyProperties(dto,courseBaseNew);
//机构id
courseBaseNew.setCompanyId(companyId);
//添加时间
courseBaseNew.setCreateDate(LocalDateTime.now());
//设置审核状态
courseBaseNew.setAuditStatus("202002");
//设置发布状态
courseBaseNew.setStatus("203001");

//3、插入课程基本信息表
int insert = courseBaseMapper.insert(courseBaseNew);
Long courseId = courseBaseNew.getId();

//4、课程营销信息
//4-1 先根据课程id查询营销信息
CourseMarket courseNewMarket = new CourseMarket();
BeanUtils.copyProperties(dto,courseNewMarket);
courseNewMarket.setId(courseId);
//4-2 设置收费规则
String charge = dto.getCharge();
//收费课程必须写价格且价格大于0
if (charge.equals("201001")){
Float price = dto.getPrice();
if (price == null || price.floatValue() <= 0){
throw new RuntimeException("课程收费价格不能为空且必须大于0");
}
}
BeanUtils.copyProperties(dto, courseNewMarket);

//5、插入课程营销信息
int insertMarket = courseMarketMapper.insert(courseNewMarket);
if(insert <= 0 || insertMarket <= 0){
throw new RuntimeException("新增课程基本信息失败");
}

//6、添加成功
//返回添加的课程信息
return getCourseBaseInfo(courseId);
}

/**
* @description 根据课程id查询课程基本信息,包括基本信息和营销信息
* @param courseId 课程id
* @return com.xuecheng.contentModel.dto.CourseBaseInfoDto
* @author xiaoming
* @date 2023/1/18 21:55
*/
public CourseBaseInfoDto getCourseBaseInfo(long courseId){

CourseBase courseBase = courseBaseMapper.selectById(courseId);
CourseMarket courseMarket = courseMarketMapper.selectById(courseId);

if(courseBase == null){
return null;
}
CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
BeanUtils.copyProperties(courseBase,courseBaseInfoDto);
if(courseMarket != null){
BeanUtils.copyProperties(courseMarket,courseBaseInfoDto);
}

//查询分类名称
CourseCategory courseCategoryBySt = courseCategoryMapper.selectById(courseBase.getSt());
courseBaseInfoDto.setStName(courseCategoryBySt.getName());
CourseCategory courseCategoryByMt = courseCategoryMapper.selectById(courseBase.getMt());
courseBaseInfoDto.setMtName(courseCategoryByMt.getName());

return courseBaseInfoDto;
}

api–定义接口

定义controller方法

1
2
3
4
5
6
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto){
//机构id,由于认证系统没有上线暂时硬编码
Long companyId = 1L;
return courseBaseInfoService.createCourseBase(companyId, addCourseDto);
}

接口测试

在xc-content-api.http中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
### 创建课程
POST {{content_host}}/content/course
Content-Type: application/json

{
"charge": "201000",
"price": 0,
"originalPrice":0,
"qq": "22333",
"wechat": "223344",
"phone": "13333333",
"validDays": 365,
"mt": "1-1",
"st": "1-1-1",
"name": "测试课程103",
"pic": "",
"teachmode": "200002",
"users": "初级人员",
"tags": "",
"grade": "204001",
"description": "",
"objectives": ""
}

查看响应情况,检查数据库是否生成数据。

前后端联调

打开新增课程页面,除了课程图片其它信息全部输入。点击保存,观察浏览器请求接口参数及响应结果是否正常,检查数据库是否生成数据。

HTTPClient

在线接口文档虽然可以测试但不能保存测试数据。在IDEA中有一个非常方便的http接口测试工具HTTPClient。

image-20230119161252505

安装好插件后,进入controller类,点击小地球,找到http接口对应的方法

image-20230119161321488

点击Generate request in HTTP Client即可生成的一个测试用例。

image-20230119161357257

为了方便保存.http文件 ,我们单独在项目工程的根目录创建一个目录单独存放它们。

image-20230119161420476

我们以模块为单位创建.http文件,打开内容管理模块的 http文件 ,把刚才测试数据拷贝上去

image-20230119161447600

为了方便将来和网关集成测试,这里我们把测试主机地址在配置文件http-client.env.json 中配置

image-20230119161459766

注意:文件名称http-client.env.json保持一致,否则无法读取dev环境变量的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"dev": {
"access_token": "",
"gateway_host": "localhost:63010",
"content_host": "localhost:63040",
"system_host": "localhost:63110",
"media_host": "localhost:63050",
"search_host": "localhost:63080",
"auth_host": "localhost:63070",
"checkcode_host": "localhost:63075",
"learning_host": "localhost:63020"
}
}

再回到xc-content-api.http文件,将 http://localhost:63040 用变量代替

image-20230119161548993

到此就完成了httpclient的配置与使用测试

系统管理服务system

启动前端工程,发现前端接口指向的是系统管理服务 http://localhost:8601/api/system/dictionary/all,在实现内容管理模块的需求时我们提到一个数据字典表,此链接正是在前端请求后端获取数据字典数据的接口地址。

可以导入资料或自行创建 xuecheng-plus-system 工程:

image-20230119162109530

system-model

沿用 content-model的模板,生成数据字典表的PO类

image-20230119163533422

system-service

沿用 content-service 的模板:

image-20230119163651347

配置扫描mapper及分页插件,拷贝Mapper

注意修改 Mapper .xml 的路径

创建service接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.xuecheng.systemService.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.xuecheng.systemModel.model.po.Dictionary;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 数据字典服务类
* @date 2023/1/17 21:03
*/
public interface DictionaryService extends IService<Dictionary> {

/**
* 查询所有数据字典内容
* @return
*/
List<Dictionary> queryAll();

/**
* 根据code查询数据字典
* @param code -- String 数据字典Code
* @return
*/
Dictionary getByCode(String code);
}

创建service接口实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.xuecheng.systemService.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xuecheng.systemModel.model.po.Dictionary;
import com.xuecheng.systemService.mapper.DictionaryMapper;
import com.xuecheng.systemService.service.DictionaryService;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 数据字典集查询
* @date 2023/1/17 21:04
*/
@Service
public class DictionaryServiceImpl extends ServiceImpl<DictionaryMapper, Dictionary> implements DictionaryService {
@Override
public List<Dictionary> queryAll() {
List<Dictionary> list = this.list();
return list;
}

@Override
public Dictionary getByCode(String code) {
LambdaQueryWrapper<Dictionary> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Dictionary::getCode, code);
Dictionary dictionary = this.getOne(queryWrapper);
return dictionary;
}
}

配置resource

拷贝一份 application.yml 和 log4j2-dev.xml,并对application.yml进行修改:

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: content-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://数据库信息:3306/xconline_system?serverTimezone=UTC&userUnicode=true&useSSL=false
username: 用户名
password: 密码
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml

system-api

主要是沿用 content-api的模板:

image-20230119162340413

导入knif4j 的配置文件,创建数据字典表的controller

配置resource文件

bootstrap.yml 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
servlet:
context-path: /system
port: 63110
#微服务配置
spring:
application:
name: system-service
mvc:
pathmatch:
matching-strategy: ANT_PATH_MATCHER
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml

添加LocalDateTime配置类

为了将LocalDateTime 的毫秒去除,定义一个配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.xuecheng.systemApi.config;

import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
* @author xioaming
* @version 1.0
* @description 本地时间配置类
* @date 2023/1/17 20:40
*/
@Configuration
public class LocalDateTimeConfig {

/*
* 序列化内容
* LocalDateTime -> String
* 服务端返回给客户端内容
* */
@Bean
public LocalDateTimeSerializer localDateTimeSerializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

/*
* 反序列化内容
* String -> LocalDateTime
* 客户端传入服务端数据
* */
@Bean
public LocalDateTimeDeserializer localDateTimeDeserializer() {
return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}

// 配置
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> {
builder.serializerByType(LocalDateTime.class, localDateTimeSerializer());
builder.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());
};
}
}

定义controller文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.xuecheng.systemApi.controller;

import com.xuecheng.systemModel.model.po.Dictionary;
import com.xuecheng.systemService.service.DictionaryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
* @author xioaming
* @version 1.0
* @description 数据字典 前端控制器
* @date 2023/1/17 20:43
*/
@Slf4j
@RestController
public class DictionaryController {

@Autowired
DictionaryService dictionaryService;

@GetMapping("/dictionary/all")
public List<Dictionary> queryAll() {
return dictionaryService.queryAll();
}

@GetMapping("/dictionary/code/{code}")
public Dictionary getByCode(@PathVariable String code) {
return dictionaryService.getByCode(code);
}
}

启动服务

启动系统管理服务,启动成功,在浏览器请求:http://localhost:63110/system/dictionary/all

系统服务的端口是63110,如果可以正常读取数据字典信息则说明系统管理服务安装成功。

跨域–前后端联调

系统管理服务启动完成,此时还需要修改前端工程中访问数据字典信息接口的地址,因为默认前端工程请求的是网关地址,目前网关工程还没有部署。

image-20230119202745529

修改完成,刷新前端工程首页不能正常显示,查看浏览器报错如下:

1
Access to XMLHttpRequest at 'http://localhost:63110/system/dictionary/all' from origin 'http://localhost:8601' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

提示:从http://localhost:8601访问http://localhost:63110/system/dictionary/all被CORS policy阻止,因为没有Access-Control-Allow-Origin 头信息。

CORS全称是 cross origin resource share 表示跨域资源共享

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

比如:

http://localhost:8601http://localhost:8602 由于端口不同,是跨域。

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

http://192.168.101.10:8601https://192.168.101.11:8601 由于协议不同,是跨域。

注意:服务器之间不存在跨域请求。

浏览器判断是跨域请求会在请求头上添加origin,表示这个请求来源哪里。

比如:

1
2
GET / HTTP/1.1
Origin: http://localhost:8601

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

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

如果允许域名来源的跨域请求,则响应如下:

1
Access-Control-Allow-Origin:*

解决跨域问题

  1. JSONP

通过script标签的src属性进行跨域请求,如果服务端要响应内容则首先读取请求参数callback的值,callback是一个回调函数的名称,服务端读取callback的值后将响应内容通过调用callback函数的方式告诉请求方。

image-20230119203033950
  1. 添加响应头

服务端在响应头添加 Access-Control-Allow-Origin:*

  1. 通过nginx代理跨域

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

image-20230119203123651

这样就实现了跨域访问。

浏览器到http://192.168.101.11:8601/api 没有跨域

nginx到http://www.baidu.com:8601通过服务端通信,没有跨域。

配置跨域过滤器

项目使用的是方法二–添加响应头解决跨域问题:在system的api工程的 config包 下编写GlobalCorsConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.xuecheng.systemApi.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
* @author xioaming
* @version 1.0
* @description 跨域过滤器
* @date 2023/1/17 22:01
*/
@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);
}
}

此配置类实现了跨域过虑器,在响应头添加Access-Control-Allow-Origin。

重启系统管理服务,前端工程可以正常进入http://localhost:8601,观察浏览器记录,成功解决跨域。

image-20230119203502824

CORS 和 Nginx 实现跨域的区别

本问题参考文档:CORS跨域与Nginx反向代理跨域优劣对比

前端配置

CORS方案:跨域时部分浏览器默认不携带cookie,因此为了携带cookie需要设置一下xmlhttprequest的withCrendetails属性,使用vue-resouce时设置如下:

1
Vue.http.options.credentials = true 

用axios时,可以在拦截器中设置如下:

1
2
3
4
5
6
axios.interceptors.request.use((config) => {     
config.withCredentials = true
return config
}, (error) => {
return Promise.reject(error)
})

使用原生XMLHttpRequest对象时如下,

1
var xhr = new XMLHttpRequest(); xhr.withCredentials = true; 

如果不需要传递cookie,最好设置成false,避免浏览器默认允许cookie的携带。

Nginx反向代理:此时前端相当于不跨域,和正常请求一致,无需额外配置。

后端配置

CORS方案: 后端需要包装ACA系列header,

1
'Access-Control-Allow-Origin' '*'; 'Access-Control-Allow-Credentials' "true";  'Access-Control-Allow-Headers' 'X-Requested-With'; 

除此以外无需额外配置。

Nginx反向代理:此时后端相当于不跨域,和正常请求一致,无需额外配置。

服务器配置

CORS方案: 无。

Nginx反向代理:该方案跨域工作都集中在nginx服务器上,配置如下:

1
2
3
4
5
6
7
8
9
...
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-Port $remote_port;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
... location /api {
proxy_pass https://b.test.com; # 设置代理服务器的协议和地址
proxy_cookie_domain b.test.com a.test.com; # 修改cookie,针对request和response互相写入cookie
}
...

安全性

CORS方案: 由于此时浏览器会默认添加origin属性,服务端可以直接查到请求来源,便于控制来源、屏蔽黑名单链接。同时服务端域名和端口会暴露出来。

Nginx反向代理:反向代理方案中没有默认的origin头部可以使用,但是可以通过X-Forward-For头部查看客户端及各级代理ip,也可以实现一定程度的回溯追踪和黑名单屏蔽。由于反向代理中,可以采用内网地址访问接口服务器,这样可以一定程度上保护接口服务器不暴露出来。

移植灵活性/扩展性

CORS方案: 只需要在代码或者配置中心进行黑白名单配置即可,方便移植和扩展。

Nginx反向代理:不同环境服务域名可能不一致,因此nginx配置也各不相同,不便于移植。而对于扩展性,当存在新的项目需要访问接口服务器时,需要首先访问nginx中server指定的域名,再由server域名反向代理到接口服务器,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 8443;
server_name a.test.com;
client_max_body_size 100m;
ssl ...
location /micro{
proxy_pass https://b.test.com; #反向代理
proxy_cookie_domain b.test.com a.test.com; #修改cookie
add_header 'Access-Control-Allow-Origin' 'htps://c.test.com';
add_header 'Access-Control-Allow-Credentials' "true";
add_header Access-Control-Allow-Headers X-Requested-With;
}
}

这个时候跨域模型就变了,由单纯的a.test.com反向代理到b.test.com,变成了a.test.com反向代理到b.test.com以及c.test.comCORS到a.test.com再反向代理到b.test.comd的情况。这个有点绕,但仔细想一下就会明白。这无疑增加了后期的维护成本。

综合对比

综合以上,我们大致可以得到如下图标:

img

对比结论

综上呢,对于公共基础服务,由于涉及到对接的前端项目可能比较多,开发测试部署环境也比较多,整体上来讲我更倾向于推荐大家使用CORS方案。而对于一些对立性强的小项目,使用nginx则可以降低你的开发成本,快速发开快速上线。具体使用当然也要结合工作实际,按需使用吧。

此外对于Nginx反向代理方案使用时,推荐使用内部域名/ip作为接口服务器的入口,尽量不要暴露到外面,以免出现不必要的安全问题。

前后端联调

前端启动完毕,再启内容管理服务端。

前端默认连接的是项目的网关地址,由于现在网关工程还没有创建,这里需要更改前端工程的参数配置文件 ,修改网关地址为内容管理服务的地址。

image-20230119204415302

启动前端工程,用前端访问后端接口,观察前端界面的数据是否正确。

访问前端首页,进入课程管理:http://localhost:8601/#/organization/course-list

image-20230119204454712

更改课程条件及分页参数测试课程查询列表是否正常显示。跟踪内容管理服务的输出日志,查看是否正常。

到此基本完成了前后端连调。

第一期OVER~~~~

下期预告:

TODO 新增课程流程续写

TODO 全局异常处理器