学成在线项目笔记第一期 项目介绍 学成在线项目借鉴了MOOC(大型开放式网络课程,即MOOC(massive open online courses))的设计思想,是一个提供IT职业课程在线学习的平台,它为即将和已经加入IT领域的技术人才提供在线学习服务,用户通过在线学习、在线练习、在线考试等学习内容,最终掌握所学的IT技能,并能在工作中熟练应用。
在线教育的模式出现多种多样,包括:B2C、C2C、B2B2C等业务模式。学成在线采用B2B2C业务模式 ,即向企业或个人在线教育平台提供教学服务,老师和学生通过平台完成整个教学和学习的过程,市场上类似的平台有:网易云课堂、腾讯课堂等,学成在线的特点是IT职业课程在线教学。
功能模块及展示(面试介绍) 项目包括门户、个人学习中心、教学机构管理平台、运营平台、社交系统、系统管理6个模块 。
本项目主要包括三类用户角色:学生、教学机构的老师、平台运营人员。
主要讲解下边的业务流程:
1、教学机构的老师登录教学管理平台,编辑课程信息,发布自己的课程。
2、平台运营人员登录运营平台审核课程、视频等信息,审核通过后课程方可发布。
课程编辑与发布流程:
3、课程发布后学生登录平台进行选课、在线学习。免费课程可直接学习,收费课程需要下单购买。
学生选课流程:
项目的技术架构 项目技术架构 学成在线项目采用当前流行的前后端分离架构开发,由以下流程来构成:用户层、CDN内容分发和加速、负载均衡、UI层、微服务层、数据层。
项目架构列表
序号
名称
功能描述
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上的静态资源将最终在文件存储服务器上保存多份。
项目流程说明
用户可以通过pc、手机等客户端访问系统进行在线学习。
系统应用CDN技术,对一些图片、CSS、视频等资源从CDN调度访问。
所有的请求全部经过负载均衡器。
对于PC、H5等客户端请求,首先请求UI层,渲染用户界面。
客户端UI请求服务层获取进行具体的业务操作。
服务层将数据持久化到数据库。
项目技术栈
项目开发环境搭建 开发工具版本 服务端开发基础工具版本列表:
开发工具
版本号
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 来进行项目的管理和构建。整个项目分为三大类工程:父工程、基础工程 和微服务工程。
每一种类的工程都有不同的作用:
父工程(parent)
对依赖包的版本进行管理
本身为Pom工程,对子工程进行聚合管理
基础工程(base)
微服务工程
分别从业务、技术方面划分模块,每个模块构建为一个微服务。
每个微服务工程依赖基础工程,间接继承父工程。
包括:内容管理服务、媒资管理服务、搜索服务、缓存服务、消息服务等。
Git提交设置
创建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/
确保 .gitignore文件 生效,打开idea终端 或者 用 git bash打开命令行(默认大家已经掌握了哈),在命令行输入以下命令:
1 2 3 git rm -r --cached . git add . git commit -m 'update .gitignore'
然后重新回到idea提交,多余的文件就会消失啦~~~
构建父工程 父程的职责是对依赖包的版本进行管理
创建父工程 为了对代码更好的进行权限管理,这里我们单独创建父工程。
使用idea打开工程目录,进入工程结构界面。
点击File–>Project Structure:
进入Project Structure,首先检查jdk是否配置正确,并进行配置。(注意:取名这一块按个人情况处理!!! )
进入Modules界面,新建模块
进入新建模块界面,选择Spring Initializr ,填写模块的信息。
注意:这里Server URL默认是start.spring.io,如果连接不上可换为start.aliyun.com。
填写模块信息注意坐标信息填写正确,填写完毕,点击Next,进入下一步不用选择任何依赖,点击“Create”,模块创建成功
把里边多余的文件和目录删除,只保留以下文件
依赖管理定义 编辑xuecheng-plus-parent父工程的依赖管理 。父工程中没有代码,不用去依赖其它的包,它的作用是限定其它子工程依赖包的版本号 ,即在dependencyManagement 中去编辑即可。
注意:如果自己的 maven仓库 没有下载过当前版本的依赖包,要先将 dependencyManagement 注释掉,不然有可能会出现依赖包下载失败的情况
确定父工程为一个pom工程,在pom.xml中添加如下内容:
1 <packaging>pom</packaging>
确定项目所以依赖的包及其版本号
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>
编写 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 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>
工程创建完成提交至git存档
构建基础工程 基础工程的职责是提供一些系统架构所需要的基础类库以及一此工具类库 。
创建基础工程 创建的过程同父工程的创建过程:
中间步骤参考 parent工程的操作,最后的基础工程展示:
依赖管理定义 需要注意的是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 本项目作为一个大型的在线教育平台,其内容管理模块主要对课程及相关内容进行管理 ,包括:课程的基本信息、课程图片、课程师资信息、课程的授课计划、课程视频、课程文档等内容的管理。
创建模块工程 模块工程结构 在第一章节创建了项目父工程、项目基础工程,如下图:
接下来要创建 内容管理模块(xuecheng-plus-content) 的工程结构。
本项目是一个前后端分离项目,前端与后端开发人员之间主要依据接口进行开发。
下图是前后端交互的流程图:
1、前端请求后端服务提供的接口。(通常为http协议 )
2、后端服务的控制层Controller接收前端的请求。
3、Contorller层调用Service层进行业务处理。
4、Service层调用Dao持久层对数据持久化。
整个流程分为前端、接口层、业务层三部分,所以模块工程的结构如下图所示:
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: 数据模型工程,存储数据模型类、数据传输类型等。
结合项目父工程、项目基础工程后,如下图:
创建工程
创建内容管理模块的总工程xuecheng-plus-content
删除多余的文件后,对 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报错。
在content下创建依法炮制 api、service和 model 三个工程
创建完成后,工程的结构图如下:
修改 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>
修改 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>
修改 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>
到此内容管理模块的四个工程的基础搭建完毕。
课程查询接口实现 需求分析
教学机构人员点击课程管理首先进入课程查询界面
在课程进行列表查询页面输入查询条件查询课程信息
当不输入查询条件时输入全部课程信息。
输入查询条件查询符合条件的课程信息。
约束:本教学机构查询本机构的课程信息。
数据模型 课程查询功能涉及的数据表有课程基本信息表(course_base)、课程计划表(course_category)
功能分析:
查询条件:
查询结果:
model–生成PO类(实体类) 生成实体类 我觉得黑马的方法过于麻烦,还是使用 MyBatis-Plus插件偷个懒吧。。。
选中表后右键打开:
选择路径以及命名方式:
选一下需要的一些参数:
点击finish后,就可以在自己定义的文件路径下找到po类需要的所有文件:(一次全生成也行,用到哪个生成哪个也行)
将所需的文件 剪切粘贴 到对应 model、service 位置即可。
移动实体类
将生成的 PO类拷贝至 content-model 工程下:(这里是一次全生成了)
添加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工程中定义课程查询参数模型类:
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;@Data @ToString public class QueryCourseParamsDto { private String auditStatus; private String courseName; private String publishStatus; }
定义请求模型类(分页查询) 由于分页查询这一类的接口在项目较多,针对分页查询的参数(当前页码、每页显示记录数)单独在 base基础工程 中定义。(通用类)
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;@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工程定义一个基础的模型类
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;@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类进行返回,即:
service-接口开发 DAO开发
前边生成PO类的同时将Mapper接口及xml文件也生成了,将mapper接口和xml文件拷贝至 service 工程下:
我个人更喜欢将 Mapper 的 xml 文件放到 resource 中,目前没发现什么问题
注意:拷贝到 service 工程后,记得修改 Mapper的 xml文件的 映射路径
导入系统管理数据库,创建系统管理服务 的数据库system,执行课程资料中的xcplus_system.sql脚本
添加依赖 在 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及分页插件:
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;@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
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接口
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;public interface CourseBaseInfoService { PageResult<CourseBase> queryCourseBaseList (PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) ; }
创建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 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;@Service public class CourseBaseInfoServiceImpl implements CourseBaseInfoService { @Autowired CourseBaseMapper courseBaseMapper; @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;@Configuration @EnableSwagger2WebMvc public class Knife4jConfiguration { @Bean(value = "dockerBean") public Docket dockerBean () { Docket docket=new Docket (DocumentationType.SWAGGER_2) .apiInfo(new ApiInfoBuilder () .description("# Knife4j RESTful APIs" ) .termsOfServiceUrl("https://doc.xiaominfo.com/" ) .contact("xiaoymin@foxmail.com" ) .version("1.0" ) .build()) .groupName("课程管理功能测试中心" ) .select() .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;@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(); } 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;@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
添加配置文件
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)
swapper访问 http://localhost:63040/content/swagger-ui.html 查看接口信息
课程分类查询接口实现 需求分析 在新增课程界面,有三处信息需要选择
课程等级、课程类型来源于数据字典表,此部分的信息前端已从系统管理服务读取。
课程分类信息没有在数据字典表中存储,而是由单独一张课程分类表,存储在内容管理数据库中。
这张表是一个树型结构,通过父结点id将各元素组成一个树。
现在的需求是 需要在内容管理服务中编写一个接口读取该课程分类表的数据,组成一个树型结构返回给前端 。
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;@Data public class CourseCategoryTreeDto extends CourseCategory implements Serializable { List childrenTreeNodes; }
service–接口开发 DAO开发
如何生成一个树型结构的对象?
可以将数据从数据库读取出来后,在java程序中遍历数据组成一个树型结构对象。
通过表的自连接查出数据使用mybatis映射成一个树型结构。
项目使用的是第2种方法。
在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;public interface CourseCategoryMapper extends BaseMapper <CourseCategory> { List<CourseCategoryTreeDto> selectTreeNodes () ; }
找到对应的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 >
编写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 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;public interface CourseCategoryService { 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;@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;@RestController @Slf4j public class CourseCategoryController { @Autowired CourseCategoryService courseCategoryService; @GetMapping("/course-category/tree-nodes") public List<CourseCategoryTreeDto> queryTreeNodes () { return courseCategoryService.queryTreeNodes(); } }
接口测试 使用httpclient测试:定义.http文件
测试无误后进行前后端联调
前后端联调 打开前端工程,进入新增课程页面。查看课程分类下拉框能否正常显示
新增课程接口实现 需求分析 根据前边对内容管理模块的数据模型分析,课程相关的信息有:课程基本信息、课程营销信息、课程图片信息、课程计划、课程师资信息,所以新增一门课程需要完成这几部分信息的填写。
进入课程查询列表
点击添加课程,选择课程类型是直播还是录播,课程类型不同课程的授课方式不同
点击下一步,进入课程基本信息添加界面
本界面分两部分信息,一部分是课程基本信息上,一部分是课程营销信息。
填写课程计划信息
课程计划即课程的大纲目录,课程计划分为两级,章节和小节。每个小节需要上传课程视频,用户点击 小节的标题即开始播放视频。如果是直播课程则会进入直播间。
进入课程师资的管理,在该界面维护该课程的授课老师
至此,一门课程新增完成。
数据模型 通过业务流程可知,一门课程信息涉及:课程基本信息、课程营销信息、课程计划信息、课程师资信息。本节开发新增课程按钮功能, 只向课程基本信息、课程营销信息添加记录。
这两部分信息分别在course_base、course_market两张表存储。当点击保存按钮时向这两张表插入数据。这两张表是一对一关联关系。
新建课程的初始审核状态为“未提交”、初始发布状态为“未发布”。
model–生成DTO 通过查看请求参数,发现请求参数相比 CourseBase模型类不一致,需要定义DTO实现请求:
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;@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; 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;@Data public class CourseBaseInfoDto extends CourseBase { private String charge; private Float price; private Float originalPrice; 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;public interface CourseBaseInfoService { PageResult<CourseBase> queryCourseBaseList (PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) ; 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 @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 ("收费规则为空" ); } CourseBase courseBaseNew = new CourseBase (); BeanUtils.copyProperties(dto,courseBaseNew); courseBaseNew.setCompanyId(companyId); courseBaseNew.setCreateDate(LocalDateTime.now()); courseBaseNew.setAuditStatus("202002" ); courseBaseNew.setStatus("203001" ); int insert = courseBaseMapper.insert(courseBaseNew); Long courseId = courseBaseNew.getId(); CourseMarket courseNewMarket = new CourseMarket (); BeanUtils.copyProperties(dto,courseNewMarket); courseNewMarket.setId(courseId); String charge = dto.getCharge(); if (charge.equals("201001" )){ Float price = dto.getPrice(); if (price == null || price.floatValue() <= 0 ){ throw new RuntimeException ("课程收费价格不能为空且必须大于0" ); } } BeanUtils.copyProperties(dto, courseNewMarket); int insertMarket = courseMarketMapper.insert(courseNewMarket); if (insert <= 0 || insertMarket <= 0 ){ throw new RuntimeException ("新增课程基本信息失败" ); } return getCourseBaseInfo(courseId); } 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) { 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。
安装好插件后,进入controller类,点击小地球 ,找到http接口对应的方法
点击Generate request in HTTP Client即可生成的一个测试用例。
为了方便保存.http文件 ,我们单独在项目工程的根目录创建一个目录单独存放它们。
我们以模块为单位创建.http文件,打开内容管理模块的 http文件 ,把刚才测试数据拷贝上去
为了方便将来和网关集成测试,这里我们把测试主机地址在配置文件http-client.env.json 中配置
注意:文件名称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
用变量代替
到此就完成了httpclient的配置与使用测试
系统管理服务system 启动前端工程,发现前端接口指向的是系统管理服务 http://localhost:8601/api/system/dictionary/all
,在实现内容管理模块的需求时我们提到一个数据字典表,此链接正是在前端请求后端获取数据字典数据的接口地址。
可以导入资料或自行创建 xuecheng-plus-system 工程:
system-model 沿用 content-model的模板,生成数据字典表的PO类
system-service 沿用 content-service 的模板:
配置扫描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;public interface DictionaryService extends IService <Dictionary> { List<Dictionary> queryAll () ; 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;@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: username: 用户名 password: 密码 # 日志文件配置路径 logging: config: classpath:log4j2-dev.xml
system-api 主要是沿用 content-api的模板:
导入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;@Configuration public class LocalDateTimeConfig { @Bean public LocalDateTimeSerializer localDateTimeSerializer () { return new LocalDateTimeSerializer (DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" )); } @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;@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,如果可以正常读取数据字典信息则说明系统管理服务安装成功。
跨域–前后端联调 系统管理服务启动完成,此时还需要修改前端工程中访问数据字典信息接口的地址,因为默认前端工程请求的是网关地址,目前网关工程还没有部署。
修改完成,刷新前端工程首页不能正常显示,查看浏览器报错如下:
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: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.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:*
解决跨域问题
JSONP
通过script标签的src属性进行跨域请求,如果服务端要响应内容则首先读取请求参数callback的值,callback是一个回调函数的名称,服务端读取callback的值后将响应内容通过调用callback函数的方式告诉请求方。
添加响应头
服务端在响应头添加 Access-Control-Allow-Origin:*
通过nginx代理跨域
由于服务端之间没有跨域,浏览器通过nginx去访问跨域地址。
这样就实现了跨域访问。
浏览器到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;@Configuration public class GlobalCorsConfig { @Bean public CorsFilter corsFilter () { CorsConfiguration config = new CorsConfiguration (); config.addAllowedOrigin("*" ); 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,观察浏览器记录,成功解决跨域。
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; } ...
安全性
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; 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的情况。这个有点绕,但仔细想一下就会明白。这无疑增加了后期的维护成本。
综合对比
综合以上,我们大致可以得到如下图标:
对比结论
综上呢,对于公共基础服务,由于涉及到对接的前端项目可能比较多,开发测试部署环境也比较多,整体上来讲我更倾向于推荐大家使用CORS方案。而对于一些对立性强的小项目,使用nginx则可以降低你的开发成本,快速发开快速上线。具体使用当然也要结合工作实际,按需使用吧。
此外对于Nginx反向代理方案使用时,推荐使用内部域名/ip作为接口服务器的入口,尽量不要暴露到外面,以免出现不必要的安全问题。
前后端联调 前端启动完毕,再启内容管理服务端。
前端默认连接的是项目的网关地址,由于现在网关工程还没有创建,这里需要更改前端工程的参数配置文件 ,修改网关地址为内容管理服务的地址。
启动前端工程,用前端访问后端接口,观察前端界面的数据是否正确。
访问前端首页,进入课程管理:http://localhost:8601/#/organization/course-list
更改课程条件及分页参数测试课程查询列表是否正常显示。跟踪内容管理服务的输出日志,查看是否正常。
到此基本完成了前后端连调。
第一期OVER~~~~ 下期预告:
TODO 新增课程流程续写
TODO 全局异常处理器