项目背景 在线教育市场环境 以下内容摘自https://report.iresearch.cn/content/2021/01/358854.shtml
在线教育行业是一个有着极强的广度和深度的行业,从校内到校外;从早幼教到职业培训;从教育工具到全信息化平台等等。
2020年的新冠疫情外生冲击,让在线教育再次站在聚光灯下。疫情下教育领域获融资最多,而其中在线教育最受资本青睐。据艾瑞咨询 统计,2020年教育行业累计融资1164亿元,其中在线教育融资金额1034亿元,占比89%。与此同时,在行业处于困境的情况下,会加速洗牌,资源向好的企业集中。2020年资源向头部集中趋势明显,中小型机构生存更加困难。2020年资本向在线教育行业累计输送的1034亿元中,80%都流向了头部的5家公司。
To C市场
据艾瑞咨询统计核算,2020年中国在线教育行业市场规模2573亿元,过去4年的CAGR达34.5%,其中低幼及素质教育赛道、K12学科培训赛道在线化进程加快是在线教育市场快速增长的最主要贡献因素。疫情影响下,低幼及素质教育领域的在线化范围持续纵深,职业教育领域的在线化进程也在不断加速,新的供给和需求不断产生。但同时,2020年疫情外生冲击加快了2020年的在线教育进程,将会透支一部分2021年的增速,艾瑞预计2021年在线教育行业同比增速将回落到20%左右。
To B 市场
疫情也加速了整个教育产业链的进化,to B机构快速成长起来,扮演着赋能者的角色,课程内容、招生、师训、直播系统、管理系统等产品及服务大量涌现。随着云服务发展成熟以及疫情对直播课需求的催化,大量提供直播授课系统等PaaS/SaaS服务的机构迅速成长起来,成为各种会展上的主力军。
IT培训市场规模 中国IT人才供给报告(https://new.qq.com/rain/a/20210831A01JI600 )
IT人才总体供不应求,高中低人才分别占比8%、41%、51%,详见下图:
IT服务是贯穿IT应用系统全生命周期的各项服务的统称,下图是IT服务产品图谱,本项目属于IT培训产业。
下图是IT培训市场规模:
1.3 学成在线项目背景
软件总体分为两类:系统软件和应用软件,应用软件包括:运营类、管理类、工具类等,运营类的项目由运营商按照企业的商业模式去运营,比如:外卖项目、滴滴打车、在线教育等;管理类的项目是为某个组织完成某业务的数据及业务管理,比如:医院MIS(管理信息系统 –Management Information System)系统,ERP系统(ERP全称是Enterprise Resource Planning,其中文名字为企业资源计划,这个系统是针对物资资源管理,人力资源管理,财务资源管理,信息资源管理集成一体化的一个企业管理系统。)等;工具类的项目是为了某个领域完成具体的需求,比如:数据采集系统、文件处理系统等。
学成在线项目属于运营类的项目,运营类项目的研发可能是自研也可能是外包,自研是由运营商成立软件研发部门自己进行软件研发,运营商即是甲方也是乙方,外包则是由运营商外包给第三方软件公司进行研发,运营商是甲方,软件公司是乙方。
学成在线项目是本公司自研的一个专门针对成人职业技能教育的网络课堂系统,网站提供了成人职业技能培训的相关课程,如:软件开发培训、职业资格证书培训、成人学历教育培训等课程。项目基于B2B2C的业务模式,培训机构可以在平台入驻、发布课程,运营人员对发布的课程进行审核,审核通过后课程才可以发布成功,课程包括免费和收费两种形式,对于免费课程可以直接选课学习,对于收费课程在选课后需要支付成功才可以继续学习。
什么是B2B2C?
B2B2C是一种电子商务类型的网络购物商业模式,B是Business的简称,C是Consumer的简称,第一个B指的是商品或服务的供应商,第二个B指的是从事电子商务的企业,C则是表示消费者。
B2B的定义:企业跟企业之间的电子商务运作方式。 B2C的定义:企业跟消费者之间的电子商务运作方式。
项目介绍 下边分别从业务和技术两个方式介绍项目。
项目业务介绍 本项目包括了用户端、机构端、运营端。
核心模块包括:内容管理、媒资管理、课程搜索、订单支付、选课管理、认证授权等。
下图是项目的功能模块图:
下边介绍业务流程:
1、课程编辑与发布流程如下:
2、课程发布后学生登录平台进行选课、在线学习。
免费课程可直接学习,收费课程需要下单购买。
学生选课流程如下:
项目技术架构 本项目采用前后端分离架构,后端采用SpringBoot、SpringCloud技术栈开发,数据库使用了MySQL,还使用的Redis、消息队列、分布式文件系统、Elasticsearch等中间件系统。
划分的微服务包括:内容管理服务、媒资管理服务、搜索服务、订单支付服务、 学习中心服务、系统管理服务、认证授权服务、网关服务、注册中心服务、配置中心服务等。
下图是项目的技术架构图:
各层职责说明如下:
名称
功能描述
用户层
用户层描述了本系统所支持的用户类型包括:pc用户、app用户、h5用户。pc用户通过浏览器访问系统、app用户通过android、ios手机访问系统,H5用户通过h5页面访问系统。
CDN
CDN全称Content Delivery Network,即内容分发网络,本系统所有静态资源全部通过CDN加速来提高访问速度。系统静态资源包括:html页面、js文件、css文件、image图片、pdf和ppt及doc教学文档、video视频等。
负载均衡
系统的CDN层、UI层、服务层及数据层均设置了负载均衡服务,上图仅在UI层前边标注了负载均衡。 每一层的负载均衡会根据系统的需求来确定负载均衡器的类型,系统支持4层负载均衡+7层负载均衡结合的方式,4层负载均衡是指在网络传输层进行流程转发,根据IP和端口进行转发,7层负载均衡完成HTTP协议负载均衡及反向代理的功能,根据url进行请求转发。
UI层
UI层描述了系统向pc用户、app用户、h5用户提供的产品界面。根据系统功能模块特点确定了UI层包括如下产品界面类型: 1)面向pc用户的门户系统、学习中心系统、教学管理系统、系统管理中心。 2)面向h5用户的门户系统、学习中心系统。 3)面向app用户的门户系统、学习中心系统。
微服务层
微服务层将系统服务分类三类:业务服务、基础服务、第三方代理服务。 业务服务:主要为学成在线核心业务提供服务,并与数据层进行交互获得数据。 基础服务:主要管理学成在线系统运行所需的配置、日志、任务调度、短信等系统级别的服务。 第三方代理服务:系统接入第三方服务完成业务的对接,例如认证、支付、视频点播/直播、用户认证和授权。
数据层
数据层描述了系统的数据存储的内容类型,关系性数据库:持久化的业务数据使用MySQL。 消息队列:存储系统服务间通信的消息,本身提供消息存取服务,与微服务层的系统服务连接。 索引库:存储课程信息的索引信息,本身提供索引维护及搜索的服务,与微服务层的系统服务连接。 缓存:作为系统的缓存服务,作为微服务的缓存数据便于查询。 文件存储:提供系统静态资源文件的分布式存储服务,文件存储服务器作为CDN服务器的数据来源,CDN上的静态资源将最终在文件存储服务器上保存多份。
项目技术栈 对主要层次使用具体的技术作说明,技术栈(技术结构图):
项目演示 下边在测试环境演示系统的核心业务流程,本项目主要包括三类用户角色:学生、教学机构的老师、平台运营人员,核心业务流程包括课程发布流程、选课学习流程。
课程发布流程:
1、教学机构的老师登录教学管理平台,编辑课程信息,发布自己的课程。
2、平台运营人员登录运营平台审核课程、视频等信息,审核通过后课程方可发布。
流程图如下:
课程发布后学生登录平台进行选课、在线学习。
免费课程可直接学习,收费课程需要下单购买。
学生选课学习流程如下:
环境搭建 安装开发工具 项目基于JDK1.8环境开发,使用Mavne构建项目工程,首先安装开发工具,安装及配置步骤参考:学成在线项目开发环境配置。
学完第一章要求JDK、IDEA、Maven、Git、MySQL环境安装完成。
提示:如果虚拟机环境没有很快安装成功可先将MySQL数据库安装在本机,因为第二章的内容刚开始就要使用MySQL数据库。
MySQL8下载地址:https://dev.mysql.com/downloads/
开发工具版本
开发工具
版本号
安装位置
IntelliJ-IDEA
2021.x以上(2021.3.1)
本地
JDK
1.8.x (1.8)
本地
Maven
3.8.x以上(3.8.4)
本地
Git
2.37.x(2.35.1)
本地
DataGrip
2021.3.1
本地
Navicat
16.0.7
本地
VMware Workstation Pro
16.x(16.2.2 build-19200509)
本地
PowerDesigner
16.5
本地
Postman
9.21.5
本地
VsCode
1.74.2
本地
Cmder
221218
本地
Centos
7.x
虚拟机
Docker
18.09.0
虚拟机
Mysql
8.x
docker
Nacos
1.4.1
docker
RabbitMQ
3.8.34
docker
Redis
6.2.7
docker
XXL-job-admin
2.3.1
docker
Minio
RELEASE.2022-09-07
docker
ElasticSearch
7.12.1
docker
Kibana
7.12.1
docker
Gogs
0.13.0
docker
Nginx
1.12.2
docker
IDEA环境配置 编码配置
自动导包配置
提示忽略大小写
设置java编译级别
设置忽略文件
Maven环境 配置仓库路径和镜像 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 <?xml version="1.0" encoding="UTF-8" ?> <settings xmlns ="http://maven.apache.org/SETTINGS/1.2.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd" > <localRepository > D:\.m2/repository</localRepository > <pluginGroups > </pluginGroups > <proxies > </proxies > <servers > </servers > <mirrors > <mirror > <id > nexus-aliyun</id > <mirrorOf > central</mirrorOf > <name > Nexus aliyun</name > <url > http://maven.aliyun.com/nexus/content/groups/public</url > </mirror > <mirror > <id > maven-default-http-blocker</id > <mirrorOf > external:http:*</mirrorOf > <name > Pseudo repository to mirror external repositories initially using HTTP.</name > <url > http://0.0.0.0/</url > <blocked > true</blocked > </mirror > </mirrors > <profiles > <profile > <id > jdk-1.8</id > <activation > <activeByDefault > true</activeByDefault > <jdk > 1.8</jdk > </activation > <properties > <maven.compiler.source > 1.8</maven.compiler.source > <maven.compiler.target > 1.8</maven.compiler.target > <maven.compiler.compilerVersion > 1.8</maven.compiler.compilerVersion > </properties > </profile > </profiles > </settings >
IDEA配置Maven
安装虚拟机 项目中用到的一些服务软件如:MySQL、Nacos配置中心,RabbitMQ消息队列等通常会在企业局域网的服务器中,开发人员去远程连接它们。我们学习中在自己笔记本上安装虚拟机,代替企业局域网中的服务器。
服务器操作系统使用Centos7
子网ip:192.1678.88.88,子网掩码:255.255.255.0
安装开发环境
见Linux篇安装centos7,以及jdk安装,docker安装等
参考 Linux安装/实操-Centos7安装
参考 Linux软件安装java版/实操-安装Docker
参考 Linux软件安装java版/实操-Docker-安装MySQL (8.0.26),datagrip/Navicat连接
参考 Linux软件安装java版/实操-Docker-安装Redis
参考 Linux软件安装java版/实操-Docker-安装ElasticSearch
参考 Linux软件安装java版/实操-Docker-安装Gogs
参考 Linux软件安装java版/实操-Docker-安装RabbitMQ
参考 Linux软件安装java版/实操-Docker-安装Nacos
参考 Linux软件安装java版/实操-Docker-安装minio
参考 Linux软件安装java版/实操-Docker-安装xxl-job-admin
1 2 3 4 5 xftp 连接到虚拟机 登录 root/root 启动docker: systemctl start docker 查询docker容器: docker ps todo: 学习已有虚拟机怎么将将软件启动写到shell脚本: sh /data/soft/restart.sh
网络配置 配置网段 为88
安装Git 本地安装Git
配置git邮箱
1 2 git config --global user.name "你的名字" git config --global user.email "你的邮箱"
idea 设置Git
安装gogs 在虚拟机中安装,gogs 比github,gitlab,gitee更轻量,可以作为Git的远程仓库
访问:http://192.168.88.88:3000 账号密码:gogs/gogs
初始化配置:
1 2 # 创建数据库 CREATE DATABASE IF NOT EXISTS gogs CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
注册gogs用户,密码为gogs
创建组织,创建团队,创建仓库
Git环境 拉取代码 本项目使用Git进行版本控制,在gogs上创建一个个人使用的git仓库:
http://192.168.88.88:3000/zuoer96/toddle.git
如果gogs安装有问题也可自行选择一个公网的Git仓库,比如:gitee、github,注册自己的账号并创建仓库。
使用git拉取远程仓库。
创建一个目录用于保存所有相关代码:D:\toddle
打开IDEA,从版本控制创建工程。
输入仓库地址,并选择工程路径。
创建成功:
添加.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 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/
提交代码到git仓库(本地),执行push(远程):
注意如果是首次向该git仓库提交代码需要输入账号和密码,可以输入默认的账号和密码:gogs/gogs
如果账号密码输入错误报异常:Incorrect username or password (access token)
如果没有权限访问此仓库则报403错误:
此时就需要在仓库成员管理界面将账号添加到此仓库即可。
如果忘记密码,此时可以先修改gogs中的密码,再修改windows凭据的密码,
在此界面中找到远程仓库的记录,修改密码。
如果在windows凭据中找不到指定记录可以设置IDEA不记录密码
创建开发分支 通常不会在主分支进行开发,本项目在dev开发分支进行开发,下边创建开发分支。
打开Git Log:
下边将dev分支提交到远程仓库
push成功,查看远程git仓库界面显示了dev分支:
接下来我们就在dev分支进行开发,开发完成后将合并到主分支。
想要养成分支的习惯,可以试着每天创建一个分支进行开发,比如:feature_yyyyMMdd
基础工程搭建 工程结构关系 学成在线使用 Maven 来进行项目的管理和构建。整个项目分为三大类工程:父工程、基础工程 和微服务工程。
每一种类的工程都有不同的作用,下面是对其功能进行说明:
父工程
对依赖包的版本进行管理
本身为Pom工程,对子工程进行聚合管理
基础工程
微服务工程
构建父工程 父工程的职责是对依赖包的版本进行管理。创建父工程分两步,第一创建父工程,第二在pom.xml编辑依赖管理。
1、首先创建父工程
为了对代码更好的进行权限管理,这里我们单独创建父工程。
使用idea打开工程目录,进入工程结构界面。
点击File–>Project Structure->进入Project Structure,首先检查jdk是否配置正确,并进行配置。
进入Modules界面,新建模块
进入新建模块界面,选择Spring Initializr,填写模块的信息。
注意:这里Server URL默认是start.spring.io,如果连接不上可换为start.aliyun.com
。
到此父工程创建完成。
2、依赖管理定义
下边开始编辑toddle-parent父工程的依赖管理 。
父工程中没有代码,不用去依赖其它的包,它的作用是限定其它子工程依赖包的版本号,即在dependencyManagement 中去编辑即可。
1)确定父工程为一个pom工程,在pom.xml中添加如下内容:
1 <packaging > pom</packaging >
2)编辑依赖的包的版本号、打包插件等。
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 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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 <?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 > <groupId > com.zuoer</groupId > <artifactId > toddle-parent</artifactId > <version > 0.0.1-SNAPSHOT</version > <name > toddle-parent</name > <description > toddle-parent</description > <packaging > pom</packaging > <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 > <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 > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > ${org.projectlombok.version}</version > </dependency > <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 > <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > ${javax.servlet-api.version}</version > <scope > provided</scope > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > ${fastjson.version}</version > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > ${druid-spring-boot-starter.version}</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > ${mysql-connector-java.version}</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > ${mybatis-plus-boot-starter.version}</version > </dependency > <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 > <dependency > <groupId > io.minio</groupId > <artifactId > minio</artifactId > <version > ${minio.version}</version > </dependency > <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 > <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 > <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.apache.maven.plugins</groupId > <artifactId > maven-compiler-plugin</artifactId > <version > 3.8.1</version > <configuration > <source > 1.8</source > <target > 1.8</target > <annotationProcessorPaths > <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 > <useDefaultDelimiters > true</useDefaultDelimiters > </configuration > </plugin > </plugins > </build > </project >
构建基础工程 基础工程的职责是提供一些系统架构所需要的基础类库以及一此工具类库。
1、首先创建基础工程toddle-base。
创建的过程同父工程的创建过程,如下图:
这里需要注意的是toddle-base的父工程为toddle-parent,toddle-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 98 99 <?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 > <groupId > com.zuoer</groupId > <artifactId > toddle-parent</artifactId > <version > 0.0.1-SNAPSHOT</version > <relativePath > ../toddle-parent</relativePath > </parent > <artifactId > toddle-base</artifactId > <name > toddle-base</name > <description > toddle-base</description > <properties > <java.version > 1.8</java.version > </properties > <dependencies > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > </dependency > <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 > <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 >
基础工程中的内容待需要时再行开发。
至此父工程和基础工程创建完成,最后提交至git,push到远程仓库
目录结构为:
面试 详细说说你的项目吧 从以下几个方面进行项目介绍:
1、项目的背景,包括:是自研还是外包、什么业务、服务的客户群是谁、谁去运营等问题。
2、项目的业务流程
3、项目的功能模块
4、项目的技术架构
5、个人工作职责
6、个人负责模块的详细说明,包括模块的设计,所用到的技术,技术的实现方案等。
一个例子:
我最近参与的项目是我们公司自研的专门针对成人职业技能教育的网络课堂系统,网站提供了成人职业技能培训的相关课程,如:软件开发培训、职业资格证书培训、成人学历教育培训等课程。项目基于B2B2C的业务模式,培训机构可以在平台入驻、发布课程,我们公司作为运营方由专门的人员对发布的课程进行审核,审核通过后课程才可以发布成功,课程包括免费和收费两种形式,对于免费课程普通用户可以直接选课学习,对于收费课程在选课后需要支付成功才可以继续学习。
本项目包括用户端、机构端、运营端三个端。
核心模块包括:内容管理、媒资管理、课程搜索、订单支付、选课管理、认证授权等。
本项目采用前后端分离架构,后端采用SpringBoot、SpringCloud技术栈开发,数据库使用了MySQL,还使用的Redis、消息队列、分布式文件系统、Elasticsearch等中间件系统。
划分的微服务包括:内容管理服务、媒资管理服务、搜索服务、订单支付服务、 学习中心服务、系统管理服务、认证授权服务、网关服务、注册中心服务、配置中心服务等。
我在这个项目中负责了内容管理、媒资管理、订单支付模块的设计与开发。
内容管理模块,是对平台上的课程进行管理,课程的相关信息比较多这里在数据库设计了课程基本信息表、课程营销表、课程计划、课程师资表进行存储 ,培训机构要发布一门课程需要填写课程基本信息、课程营销信息、课程计划信息、课程师资信息,填写完毕后需要提交审核,由运营人员进行课程信息的审核,整个审核过程是程序自动审核加人工确认的方式,通常24小时审核完成。课程审核通过即可发布课程,课程的相关信息会聚合到课程发布表中,这里不仅要将课程信息写到课程发布表还要将课程信息写到索引库、分布式文件系统中,所以这里存在分布式事务的问题,项目使用本地消息表加任务调度的方式去解决这里的分布式事务,保存数据的最终一致性。
Git相关问题 1.Git代码冲突怎么处理?
多人协作使用Git难免出现代码冲突,原因是因为当本地文件的版本和目标分支中文件的版本不一致时,当存在同一行的内容不同,在进行合并的时候就会出现冲突。
代码冲突一般发生在:
多个分支向主分支合并时
同一个分支下pull或push操作时
发生了冲突需要手动合并代码,选择最终的版本。可以通过图形界面merge
或者手动在代码中处理merge
2.你是在哪个分支开发的?
我们不是直接在主分支开发,由技术经理创建独立的开发分支,我们是在独立的开发分支中进行开发,最后由技术经理将开发分支合并到主分支。
Maven相关问题 2.使用dependencyManagement锁定版本号
通常在父工程对依赖的版本统一管理,比如我们只依赖B的1.0版本,此时可以在父工程中限定B的版本为1.0
3.maven的常用命令
1 2 3 4 5 6 mvn clean 清理target目录 mvn compile 编译源代码 mvn test 执行带院测试 mvn package 打包 mvn install 打包并上传到本地仓库 mvn deploy 打包并上传到远程仓库
4.maven的依赖版本冲突了怎么处理?
一般是由于间接依赖导致一个jar包有多个不同的版本,比如:A依赖了B的1.0版本,C依赖了B的2.0版本,项目依赖A和C,从而间接依赖了B的1.0和2.0版本,此时B有2个版本引入到了项目中,当存在版本冲突时可能会出现ClassNotFoundException,NoSuchMethodError等错误。
处理版本冲突可以使用以下方法:
使用exclusions排除依赖:比如,我们只依赖B的1.0版本,此时可以在依赖C时排除对B的依赖
使用dependencyManagement锁定版本号:比如,我们只依赖B的1.0版本,此时可以在父工程中限定B的版本为1.0
MySQL存储引擎及区别 1.MySQL场景的存储引擎及区别
InnoDB
支持事务
使用的锁粒度默认为行级锁,可以支持更高的并发;也支持表锁
支持外键约束;外键约束其实降低了表的查询速度,增加了表之间的耦合度
MyISAM
memory
总结:
MyISAM管理费事务表,提供高速存储和检索以及全文搜索能力,如果在应用中执行大量select操作,应该选择MyISAM
InnoDB用于事务处理,具有ACID事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB
2.MySQL建表时注意什么?
注意选择存储引擎,如果要支持事务需要选择InnoDB
注意字段类型的选择,对于日期类型如果需要记录时分秒建议用datetime,只记录年月日使用date类型,对于字符类型的选择,固定长度字段选择char,不固定长度的字段选择varchar,varchar比char节省空间但速度没有char块;对于内容介绍类的长广文本字段使用text或longtext类型;如果要存储图片等二进制数据使用blob或longblob类型;对于金额字段建议使用DECIMAL;对于数值类型的字段在确保取值范围足够的前提下尽量使用占用空间较小的类型。
主键字段建议使用自然主键,不要有业务意义,建议使用int unsigned类型,特殊场景使用bigint类型
如果要存储text、blob字段建议单独键一张表,使用外键关联
设置字段默认值,比如:状态、创建时间等
每个字段写清楚注释
注意字段的约束,比如:非空、唯一、主键等
Springboot接口开发注解有哪些 1.SpringBoot接口开发的常用注解有哪些?
1 2 3 4 5 6 7 8 9 10 11 12 13 @Controller:标记此类是一个控制器,可以返回视图解析器指定的html页面,通过@ResponseBody可以将结果返回json,xml数据 @RestController: 相当于@Controller+@ResponseBody,s实现rest接口开发,返回json数据,不能返回html页面 @RequestMapping: 定义接口地址,可以标记在类和方法上,支持http的post,get,put等方法 @PostMapping: 定义Post接口,只能标记在方法上,用于添加记录,复杂条件的查询接口 @GetMapping: 定义Get接口,只能标记在方法上,用于查询接口 @PutMapping: 定义Put接口,只能标记在方法上,用于修改接口 @DeleteMapping: 定义Post接口,只能标记在方法上,用于删除接口 @RequestBody: 定义在方法上,用于将json串转成java对象 @PathVarible: 接收请求路径中占位符的值 @ApiOperation: swagger注解,对接口类进行说明 @Api Wagger注解,对接口类进行说明 @Autowired: 基于类型注入 @Resource:基于名称注入,如果失败,转为基于类型注入
2.项目的开发流程是什么?
产品人员设计产品原型
讨论需求
分模块设计接口
出接口文档
将接口文档给到前端人员,前后端分离开发
开发完毕进行测试
测试完毕发布项目,由运维人员进行部署安装
Mybatis相关问题 1、mybatis分页插件原理?
首先分页参数放到ThreadLocal中,拦截执行的SQL,根据数据库类型添加对应的分页语句重写SQL
例如:
1 2 3 4 select * from table where a转换为: (select count (* ) from table where a)和 (select * from table where a limit ,)
计算出了total总数,pageNum当前第几页,pageSize每页大小和当前页的数据,是否为首页,是否为尾页,总页数等。
2.树形表的标记字段是什么?如何查询MySQL树形表?
标记字段为parentId这种的。
查询树形表方法:
层级固定:表自连接查询
如果想灵活查询每个层级可以使用mysql8 递归方法,with recursive实现。
3.mybatis的resultType和resultMap区别?
查询到的SQL字段名字和最终要映射的模型树形名字可以对应上,就可以直接使用resultType,mybatis可以帮我们自动映射(下划线转驼峰)。
如果对应不上,就要用resultMap手动映射。比如返回的数据嵌套了个list
4.#{}
和${}
区别
前者是占位符,可以防止SQL注入。后者是动态SQL拼接字符串,可能导致SQL注入
系统异常处理 1、系统如何处理异常?
我们自定义一个统一的异常处理器去捕获并处理异常。
使用控制器增加注解@ControllerAdcive和异常助力注解@ExceptionHandler来实现
开发在编写代码时根据检验结果主动排除自定义异常对象,抛出异常时指定详细的异常信息,异常处理器捕获异常信息记录异常日志并响应给用户。
接口执行过程中的一些运行时异常也会由异常处理器统一捕获,记录异常日志,统一响应给用户500错误。
在异常处理器中还可以针对某个异常类型进行单独处理。
系统参数合法性校验 1、请求参数的合法性校验如何做?
使用基于JSR303的校验框架实现,SpringBoot提供了JSR303的支持,它就是spring-boot-start-validation,它包括了很多校验规则,只需要在模型类中通过注解指定校验规则,在controller方法上开启校验。
事务相关 1、什么情况spring事务会失效
1)在方法中捕获异常没有抛出
2)非事务方法调用事务方法
3)事务方法内容部调用事务方法
4)@Transactional标记的方法不是public
5)抛出的异常和rollbackFor指定的异常不匹配,默认rollbackFor指定的异常为runtimeException
6)数据库表不支持事务,比如MySQL的MyISAM
7)Spring的传播行为导致事务失效,比如:PROPAGATION_NEVER、PROPAGATION_NOT_SUPPORTED
PROPAGATION_REQUIRED: 支持当前事务,如果当前没有事务,就新建一个事务,这是最常见的选择
PROPAGATION_SUPPORTS: 支持当前事务,如果当前没有事务,就以非事务方式执行
PROPAGATION_MANDATORY:支持当前事务,如果当前没有事务,就抛出异常
PROPAGATION_REQUIRES_NEW:新建事务,如果当前存在事务,把当前事务挂起
PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,把当前事务挂起
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED: 如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则与PROPAGATION_REQUIRED类似的操作
文件传输相关 1、断点续传是什么做的?
我们是基于分块上传的模式实现断点续传的需求,当文件上传一部分断网后前边已经上传过的不再上传。
1)前端对文件分块。
2)前端使用多线程一块一块上传,上传前给服务端发一个消息校验该分块是发上传,如果已上传则不再上传
3)等所有分块上传完毕,服务端合并所有分块,校验文件的完整性
4)前端给服务传了一个md5值,服务端合并文件后计算合并后文件的md5是否和前端传的一样,如果一样说明文件完整,如果不易说明可能由于网络丢包导致文件不完整,这时上传失败需要重新上传。
2、分块文件清理问题?
上传一个文件进行分块上传,上传一半不传了,之前上传到minio的分开文件要清理吗?怎么做的?
1)在数据库中有一张文件表记录minio中的文件信息
2)文件开始上传时会写入文件表,状态为上传中,上传完成则会更新状态为上传完成。
3)当一个文件传了一半不再上传了说明该文件没有上传完成,会有定时任务去查询文件表中的记录,如果文件未上传完成则删除minio中没有上传成功的文件目录。
任务处理相关 1、xxl-job的工作原理是什么?xxl-job是怎么工作的?
XXL-JOB分布式任务调度服务由调度中心和执行器组成,调度中心负责按任务调度策略向执行器下发任务,执行器负责接收任务执行任务。
1)首先部署并启动xxl-job调度中心(1个java工程)
2)首先在微服务添加xxl-job依赖,在微服务中配置执行器
3)启动微服务,执行器向调度中心上报自己
4)在微服务中写一个任务方法并用xxl-job的注解去标记执行任务的方法名称
5)在调度中心配置任务调度策略,调度策略就是每隔多长时间执行还是在每天或每个月的固定时间去执行,比如每天0点执行,或每隔1小时执行一次等。
6)在调度中心启动任务。
7)调度中心更具任务调度策略,到达时间就开始下发任务给执行器。
8)执行器收到任务就开始执行任务。
内容管理模块 模块需求分析 什么是需求分析 在百度百科中对需求分析的定义如下:
需求分析也称为软件需求分析、系统需求分析或需求分析工程等,是开发人员经过深入细致的调研和分析,准确理解用户和项目的功能、性能、可靠性等具体要求 ,将用户非形式的需求表述转化为完整的需求定义,从而确定系统必须做什么的过程。
简单理解就是要搞清楚问题域,问题域就是用户的需求,软件要为用户解决什么问题,实现哪些业务功能,满足什么样的性能要求。
如何作需求分析?
第一:首先确认用户需求
用户需求即用户的原始需求。
通过用户访谈、问卷调查、开会讨论、查阅资料等调研手段梳理用户的原始需求。
产品人员根据用户需求会绘制界面原型,通过界面原型再和用户确认需求。
第二:确认关键问题
用户的原始需求可能 是含糊不清的,需求分析要从繁杂的问题中梳理出关键问题。
比如:教学机构的老师想要将课程发布到网上,这是原始需求,根据这个用户需求我们需要进行扩展分析,扩展出几下几点:
1)课程发布需要发布哪些信息
2)如果发布了不良信息怎么办?
3)课程发布后用户怎么查看课程?
根据以上几点继续延伸性分析:
1)课程发布需要发布哪些信息
课程名称、课程介绍、课程价格、课程图片、师资等信息
继续延伸分析:
这么多课程信息进行归类,方便用户编辑,分为课程基本信息、课程营销信息、课程师资等信息。
按照这样的思路对用户需求逐项分析,梳理出若干问题,再从中找到关键问题。比如:上边对课程信息分类后,哪些是关键信息,课程名称、课程图片、课程介绍等基本信息为关键信息,所以发布课程的第一步要编写课程基本信息。
找到了关键问题,下一步就可以进行数据建模,创建课程基本信息表,并设计其中的字段。
第三:梳理业务流程
业务流程是由一个或多个用户参与完成为了完成一个目标所进行的一系列的业务操作,不论是整个系统还是一个模块通常首先分析核心的业务流程,比如:内容管理模块的核心业务流程是课程发布,本项目的核心业务流程是学生选课学习流程。
第四:数据建模
数据建模要根据分析的关键问题将其相关的信息全部建模。比如:根据发布课程的用户需求,可创建课程基本信息表、课程营销信息表、课程师资表、课程发布记录表、课程审核记录表等。
第五:编写需求规格说明书
需求分析阶段的成果物是需求分析规格说明书,针对每一个问题编写需求用例,需求用例包括:功能名称、功能描述、参与者、基本事件流程、可选事件流、数据描述、前置条件、后置条件等内容。
比如:添加课程的需求用例如下:
项目
添加课程
功能名称
添加课程
功能描述
添加课程基本信息
参与者
教学机构管理员
前置条件
教学机构管理只允许向自己机构添加课程 拥有添加课程的权限
基本事件流程
1、登录教学机构平台 2、进入课程列表页面 3、点击添加课程按钮进入添加课程界面 4、填写课程基本信息 5、点击提交。
可选事件流程
成功:提示添加成功,跳转到课程营销信息添加界面 失败:提示具体的失败信息,用户根据失败信息进行修改。
数据描述
课程基本信息:课程id、课程名称、课程介绍、课程大分类、课程小分类、课程等级、课程图片、所属机构、课程创建时间、课程修改时间、课程状态
后置条件
向课程基本信息插入一条记录
补充说明
模块介绍 内容管理这个词存在于很多软件系统,什么是内容管理 ?
通过百度百科查询其意思
内容管理系统(content management system,CMS),是一种位于WEB前端(Web 服务器)和后端办公系统或流程(内容创作、编辑)之间的软件系统。内容的创作人员、编辑人员、发布人员使用内容管理系统来提交、修改、审批、发布内容。这里指的“内容”可能包括文件、表格、图片、数据库中的数据甚至视频等一切你想要发布到Internet、Intranet以及Extranet网站的信息。
本项目作为一个大型的在线教育平台,其内容管理模块主要对课程及相关内容进行管理 ,包括:课程的基本信息、课程图片、课程师资信息、课程的授课计划、课程视频、课程文档等内容的管理。
业务流程 内容管理的业务由教学机构人员和平台的运营人员共同完成。
教学机构人员的业务流程如下:
1、登录教学机构。
2、维护课程信息,添加一门课程需要编辑课程的基本信息、上传课程图片、课程营销信息、课程计划、上传课程视频、课程师资信息等内容。
3、课程信息编辑完成,通过课程预览确认无误后提交课程审核。
4、待运营人员对课程审核通过后方可进行课程发布。
运营人员的业务流程如下:
1、查询待审核的课程信息。
2、审核课程信息。
3、提交审核结果。
下图是课程编辑与发布的整体流程。
界面原型 产品工程师根据用户需求制作产品界面原型,开发工程师除了根据用户需求进行需求分析以外,还会根据界面原型上的元素信息进行需求分析。
内容管理模块的界面原型如下:
课程列表 :
点击添加课程:
选择录播课程 ,填写课程信息:
填写课程计划信息:
填写课程师资信息:
课程填写完毕进行课程 发布:
当审核状态为通过时发布按钮点亮,点击发布按钮 即可对该课程进行发布。
数据模型 内容管理模块的基础表涉及9张,如下:
使用PowerDesigner打开课程资料下的 “数据库\模型\学成在线项目.sws”
创建模块工程 模块工程结构 在第一章节创建了项目父工程、项目基础工程,如下图:
接下来要创建内容管理模块的工程结构。
本项目是一个前后端分离项目,前端与后端开发人员之间主要依据接口进行开发。
下图是前后端交互的流程图:
1、前端请求后端服务提供的接口。(通常为http协议 )
2、后端服务的控制层Controller接收前端的请求。
3、Contorller层调用Service层进行业务处理。
4、Service层调用Dao持久层对数据持久化。
整个流程分为前端、接口层、业务层三部分。
所以模块工程的结构如下图所示:
toddle-content-api:接口工程,为前端提供接口。
toddle-content-service: 业务工程,为接口工程提供业务支撑。
toddle-content-model: 数据模型工程,存储数据模型类、数据传输类型等。
结合项目父工程、项目基础工程后,如下图:
为什么要拆成api和service? https://www.530311.com/know/show-2142736.html
toddle-content:内容管理模块工程,负责聚合toddle-content-api、toddle-content-service、toddle-content-model。
创建模块工程 1、首先在项目根目录创建内容管理模块的父工程toddle-content
创建完成,只保留pom.xml文件,删除多余的文件。
内容管理父工程的主要职责是聚合内容管理接口和内容管理接口实现两个工程,它的父工程是toddle-parent。
pom.xml如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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 > <groupId > com.zuoer</groupId > <artifactId > toddle-parent</artifactId > <version > 0.0.1-SNAPSHOT</version > <relativePath > ../toddle-parent</relativePath > </parent > <artifactId > toddle-content</artifactId > <name > toddle-content</name > <description > 内容管理父工程的主要职责是聚合内容管理接口和内容管理接口实现两个工程</description > <packaging > pom</packaging > <modules > <module > toddle-content-api</module > <module > toddle-content-model</module > <module > toddle-content-service</module > </modules > </project >
由于toddle-content-api和toddle-content-service两个工程还没有创建所以modules报错。
2、在toddle-content下创建toddle-content-model数据模型工程。
创建完成,只保留包和pom.xml文件 ,删除多余的文件。
修改pom.xml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?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 > <groupId > com.zuoer</groupId > <artifactId > toddle-content</artifactId > <version > 0.0.1-SNAPSHOT</version > </parent > <artifactId > toddle-content-model</artifactId > <name > toddle-content-model</name > <description > toddle-content-model数据模型工程</description > <dependencies > <dependency > <groupId > com.zuoer</groupId > <artifactId > toddle-base</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > </dependencies > </project >
3、在toddle-content下创建toddle-content-service接口实现工程。
创建完成,只保留包和pom.xml文件 ,删除多余的文件
pom.xml如下:
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 > <groupId > com.zuoer</groupId > <artifactId > toddle-content</artifactId > <version > 0.0.1-SNAPSHOT</version > </parent > <artifactId > toddle-content-service</artifactId > <name > toddle-content-service</name > <description > toddle-content-service接口实现工程</description > <dependencies > <dependency > <groupId > com.zuoer</groupId > <artifactId > toddle-content-model</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > </dependencies > </project >
4、在toddle-content下创建toddle-content-api接口工程。
toddle-content-api接口工程的父工程是toddle-content,它依赖了toddle-base基础工程。
编辑pom.xml
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 > <groupId > com.zuoer</groupId > <artifactId > toddle-content</artifactId > <version > 0.0.1-SNAPSHOT</version > </parent > <artifactId > toddle-content-api</artifactId > <name > toddle-content-api</name > <description > toddle-content-api接口工程</description > <dependencies > <dependency > <groupId > com.zuoer</groupId > <artifactId > toddle-content-service</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > </dependencies > </project >
到此内容管理模块的四个工程创建完毕,工程结构图如下:
课程查询 需求分析 业务流程 课程查询的业务流程如下:
1、教学机构人员点击课程管理首先进入课程查询界面,如下:
2.在课程进行列表查询页面输入查询条件查询课程信息
当不输入查询条件时输入全部课程信息。
输入查询条件查询符合条件的课程信息。
约束:本教学机构查询本机构的课程信息。
数据模型 下边从查询条件、查询列表两个方面分析数据模型
1、查询条件:
包括:课程名称、课程审核状态、课程发布状态
课程名称:可以模糊搜索
课程审核状态:未提交、已提交、审核通过、审核未通过
课程发布状态:未发布、已发布、已下线
因为是分页查询所以查询条件中还要包括当前页码、每页显示记录数。
2、查询结果:
查询结果中包括:课程id、课程名称、任务数、创建时间、是否付费、审核状态、类型,操作
任务数:该课程所包含的课程计划数,即课程章节数。
是否付费:课程包括免费、收费两种。
类型:录播、直播。
因为是分页查询所以查询结果中还要包括总记录数、当前页、每页显示记录数。
创建数据库表及PO类型 创建数据库表 1、创建内容管理数据库
2、向创建的内容管理数据库导入数据
选择课程资料中的toddle_content.sql脚本,这里使用DataGrid 客户端工具连接mysql并执行脚本。
执行成功,查询course_base数据表,如下:
生成PO类 PO即持久对象(Persistent Object),它们是由一组属性和属性的get和set方法组成,PO对应于数据库的表。
在开发持久层代码时需要根据数据表编写PO类,在实际开发中通常使用代码生成器(工具)生成PO类的代码 。
由于在需求分析阶段对数据模型进行分析,PO类对应于数据模型,所以在需求分析阶段即可使用工具生成PO类,为下面的接口定义准备好模型类。
在企业开发中通常使用代码生成工具去自动生成这些文件,
本项目使用mybatis-plus的generator工程生成PO类、Mapper接口、Mapper的xml文件,地址在:https://github.com/baomidou/generator
将课程资料目录下的toddle-generator.zip解压后拷贝至项目工程根目录,如下图:
打开IDEA将其导入项目工程 ,打开toddle-generator工程的pom.xml,右键 点击“Add as Maven Project” 自动识别maven工程。
本次生成内容管理模块的PO类、Mapper接口和Mapper的xml文件 ,找到ContentCodeGenerator类,修改ContentCodeGenerator类中的信息,包括:数据库地址、数据库账号、数据库密码、生成的表、生成路径,如下:
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 public class ContentCodeGenerator { private static final String SERVICE_NAME = "content" ; private static final String DATA_SOURCE_USER_NAME = "root" ; private static final String DATA_SOURCE_PASSWORD = "root" ; private static final String[] TABLE_NAMES = new String []{ "course_base" , "course_market" , "course_teacher" , "course_category" , "teachplan" , "teachplan_media" , "course_publish" , "course_publish_pre" }; private static final Boolean IS_DTO = false ; public static void main (String[] args) { AutoGenerator mpg = new AutoGenerator (); mpg.setTemplateEngine(new FreemarkerTemplateEngine ()); GlobalConfig gc = new GlobalConfig (); gc.setFileOverride(true ); gc.setOutputDir(System.getProperty("user.dir" ) + "/toddle-generator/src/main/java" ); gc.setAuthor("zuoer" ); gc.setOpen(false ); gc.setSwagger2(false ); gc.setServiceName("%sService" ); gc.setBaseResultMap(true ); gc.setBaseColumnList(true ); if (IS_DTO) { gc.setSwagger2(true ); gc.setEntityName("%sDTO" ); } mpg.setGlobalConfig(gc); DataSourceConfig dsc = new DataSourceConfig (); dsc.setDbType(DbType.MYSQL); dsc.setUrl("jdbc:mysql://192.168.88.88:3306/toddle_" + SERVICE_NAME + "?serverTimezone=UTC&useUnicode=true&useSSL=false&characterEncoding=utf8" ); dsc.setDriverName("com.mysql.cj.jdbc.Driver" ); dsc.setUsername(DATA_SOURCE_USER_NAME); dsc.setPassword(DATA_SOURCE_PASSWORD); mpg.setDataSource(dsc); PackageConfig pc = new PackageConfig (); pc.setModuleName(SERVICE_NAME); pc.setParent("com.zuoer" ); pc.setServiceImpl("service.impl" ); pc.setXml("mapper" ); pc.setEntity("model.po" ); mpg.setPackageInfo(pc); TemplateConfig tc = new TemplateConfig (); mpg.setTemplate(tc); StrategyConfig strategy = new StrategyConfig (); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true ); strategy.setRestControllerStyle(true ); strategy.setInclude(TABLE_NAMES); strategy.setControllerMappingHyphenStyle(true ); strategy.setTablePrefix(pc.getModuleName() + "_" ); strategy.setEntityBooleanColumnRemoveIsPrefix(true ); strategy.setRestControllerStyle(true ); strategy.setTableFillList(Arrays.asList( new TableFill ("create_date" , FieldFill.INSERT), new TableFill ("change_date" , FieldFill.INSERT_UPDATE), new TableFill ("modify_date" , FieldFill.UPDATE) )); mpg.setStrategy(strategy); mpg.execute(); } }
修改完成,执行该类的main方法,自动生成content包,如下:
在该包下自动生成了内容管理模块的controller、mapper、po及service相关代码,这里我们只需要po类。
将po类拷贝到model工程
打开一个PO类发现编译报错,这是缺少依赖包导致,本项目使用的持久层框架是MyBatisPlus,在生成的po类中加了一些MyBatisPlus框架的注解,这里需要添加MyBatisPlus框架的依赖,消除错误。
下边在model工程添加依赖
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.zuoer</groupId > <artifactId > toddle-base</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > <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 >
设计接口 接口设计分析 设计一个接口需要包括以下几个方面:
1)协议
通常协议采用HTTP,查询类接口通常为get或post,查询条件较少的使用get,较多的使用post。
本接口使用 http post。
还要确定content-type,参数以什么数据格式提交,结果以什么数据格式响应。
一般情况没有特殊情况结果以json 格式响应。
2)分析请求参数
根据前边对数据模型的分析,请求参数为:课程名称、课程审核状态、当前页码、每页显示记录数。
根据分析的请求参数定义模型类。
3)分析响应结果
根据前边对数据模型的分析,响应结果为数据列表加一些分页信息(总记录数、当前页、每页显示记录数)。
数据列表中数据的属性包括:课程id、课程名称、任务数、创建时间、审核状态、类型。
注意:查询结果中的审核状态为数据字典中的代码字段,前端会根据审核状态代码 找到对应的名称显示。
根据分析的响应结果定义模型类。
4)分析完成,使用SpringBoot注解开发一个Http接口。
5)使用接口文档工具查看接口的内容。
6)接口中调用Service方法完成业务处理。
4)接口请求示例
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 ###请求路径 POST /content/course/list?pageNo=2 &pageSize=1 ###请求类型 Content-Type: application/json ###请求参数 { "auditStatus" : "202002" , "courseName" : "" , "publishStatus" : "" } ###成功响应结果 { "items" : [ { "id" : 26 , "companyId" : 1232141425 , "companyName" : null , "name" : "spring cloud实战" , "users" : "所有人" , "tags" : null , "mt" : "1-3" , "mtName" : null , "st" : "1-3-2" , "stName" : null , "grade" : "200003" , "teachmode" : "201001" , "description" : "本课程主要从四个章节进行讲解: 1.微服务架构入门 2.spring cloud 基础入门 3.实战Spring Boot 4.注册中心eureka。" , "pic" : "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg" , "createDate" : "2019-09-04 09:56:19" , "changeDate" : "2021-12-26 22:10:38" , "createPeople" : null , "changePeople" : null , "auditStatus" : "202002" , "auditMind" : null , "auditNums" : 0 , "auditDate" : null , "auditPeople" : null , "status" : 1 , "coursePubId" : null , "coursePubDate" : null } ] , "counts" : 23 , "page" : 2 , "pageSize" : 1 }
定义模型类 根据接口分析需要定义模型类接收请求的参数,并定义模型类用于响应结果。
1、分页查询模型类
由于分页查询这一类的接口在项目较多,这里针对分页查询的参数(当前页码、每页显示记录数)单独在xuecheng-plus-base基础工程中定义。
内容如下:
1 2 3 4 5 6 7 @Data @NoArgsConstructor @AllArgsConstructor public class PageParams { private Long pageNo = 1L ; private Long pageSize = 10L ; }
由于上边类中用到了lombok注解所以在base工程添加lombok依赖包
2、查询条件模型类
除了分页查询参数,剩下的就是课程查询的特有参数,此时需要在内容管理的model工程中定义课程查询参数模型类。
内容如下:
1 2 3 4 5 6 @Data public class QueryCourseParamsDto { private String auditStatus; private String courseName; private String publishStatus; }
3、响应模型类
根据接口分析,下边定义响应结果模型类。
针对分页查询结果经过分析也存在固定的数据和格式,所以在base工程定义一个基础的模型类。
内容如下:
1 2 3 4 5 6 7 8 @Data @AllArgsConstructor public class PageResult <T> implements Serializable { private List<T> items; private Long counts; private Long page; private Long pageSize; }
我们发现此模型类中定义了List属性,此属性存放数据列表,且支持泛型,课程查询接口的返回类型可以是此模型类型。
List中的数据类型用什么呢?根据需求分析使用生成的PO类即可,所以课程查询接口返回结果类型如下:
1 2 泛型中填写CourseBase类型。 PageResult<CourseBase>
定义接口 根据分析,此接口提供 HTTP post协议,查询条件以json格式提交,响应结果为json 格式。
可使用SpringBoot注解在Controller类中实现。
1、首先在api工程添加依赖
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.zuoer</groupId > <artifactId > toddle-content-service</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-context</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <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 > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > </dependency > <dependency > <groupId > com.spring4all</groupId > <artifactId > swagger-spring-boot-starter</artifactId > </dependency > </dependencies >
2、定义controller方法
1 2 3 4 5 6 7 @RestController public class CourseBaseInfoController { @PostMapping("/course/list") public PageResult<CourseBase> list (PageParams pageParams, @RequestBody QueryCourseParamsDto queryCourseParamsDto) { return null ; } }
说明:pageParams分页参数通过url的key/value传入,queryCourseParams通过json数据传入,使用@RequestBody注解将json转成QueryCourseParamsDto对象。
3、定义启动类
1 2 3 4 5 6 @SpringBootApplication public class ContentApplication { public static void main (String[] args) { SpringApplication.run(ContentApplication.class,args); } }
3、添加配置文件
创建 log4j2-dev.xml、bootstrap.yml文件。
log4j2-dev.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 <?xml version="1.0" encoding="UTF-8" ?> <Configuration monitorInterval ="180" packages ="" > <properties > <property name ="logdir" > logs</property > <property name ="PATTERN" > %date{YYYY-MM-dd HH:mm:ss,SSS} %level [%thread][%file:%line] - %msg%n%throwable</property > </properties > <Appenders > <Console name ="Console" target ="SYSTEM_OUT" > <PatternLayout pattern ="${PATTERN}" /> </Console > <RollingFile name ="ErrorAppender" fileName ="${logdir}/error.log" filePattern ="${logdir}/$${date:yyyy-MM-dd}/error.%d{yyyy-MM-dd-HH}.log" append ="true" > <PatternLayout pattern ="${PATTERN}" /> <ThresholdFilter level ="ERROR" onMatch ="ACCEPT" onMismatch ="DENY" /> <Policies > <TimeBasedTriggeringPolicy interval ="1" modulate ="true" /> </Policies > </RollingFile > <RollingFile name ="DebugAppender" fileName ="${logdir}/info.log" filePattern ="${logdir}/$${date:yyyy-MM-dd}/info.%d{yyyy-MM-dd-HH}.log" append ="true" > <PatternLayout pattern ="${PATTERN}" /> <ThresholdFilter level ="DEBUG" onMatch ="ACCEPT" onMismatch ="DENY" /> <Policies > <TimeBasedTriggeringPolicy interval ="1" modulate ="true" /> </Policies > </RollingFile > <Async name ="AsyncAppender" includeLocation ="true" > <AppenderRef ref ="ErrorAppender" /> <AppenderRef ref ="DebugAppender" /> </Async > </Appenders > <Loggers > <logger name ="com.zuoer.content.mapper" level ="DEBUG" > </logger > <logger name ="springfox" level ="INFO" > </logger > <logger name ="org.apache.http" level ="INFO" > </logger > <logger name ="com.netflix.discovery" level ="INFO" > </logger > <logger name ="RocketmqCommon" level ="INFO" > </logger > <logger name ="RocketmqRemoting" level ="INFO" > </logger > <logger name ="RocketmqClient" level ="WARN" > </logger > <logger name ="org.dromara.hmily" level ="WARN" > </logger > <logger name ="org.dromara.hmily.lottery" level ="WARN" > </logger > <logger name ="org.dromara.hmily.bonuspoint" level ="WARN" > </logger > <Root level ="DEBUG" includeLocation ="true" > <AppenderRef ref ="AsyncAppender" /> <AppenderRef ref ="Console" /> <AppenderRef ref ="DebugAppender" /> </Root > </Loggers > </Configuration >
bootstrap.yml内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 server: servlet: context-path: /content port: 63040 spring: application: name: content-api datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.88.88:3306/toddle_content?serverTimezone=UTC&userUnicode=true&useSSL=false& username: root password: root logging: config: classpath:log4j2-dev.xml
4、下边启动服务,测试接口是否可以正常请求
在controller方法中打断点,debug启动微服务,在浏览器访问http://localhost:63040/content/course/list
浏览器报400错误,400错误通常由于你访问的页面域名不存在或者请求错误。一般是因为我们输入的语法格式有错误,服务器无法理解用户的请求,不知道要表达的是什么。这个时候我们需要认真检查下语义、请求参数是否有误,不然再怎么刷新都没有用。
接口接收两部分参数,一部分是分页参数,它是通过http url传递key/value串,另一部分是业务查询条件,通过http body传入json内容。
服务端使用RequestBody接收json内容,我们在测试并没有传递json内容这里导致错误。
下边在@RequestBody后添加(required=false)表示此参数不是必填项,如下:
1 2 3 4 @PostMapping("/course/list") public PageResult<CourseBase> list (PageParams pageParams, @RequestBody(required = false) QueryCourseParamsDto queryCourseParamsDto) { return null ; }
再次测试,运行到 断点处暂停。
模型类的作用 现在项目中有两类模型类:DTO数据传输对象、PO持久化对象,DTO用于接口层向业务层之间传输数据,PO用于业务层与持久层之间传输数据,有些项目还会设置VO对象,VO对象用在前端与接口层之间传输数据,如下图:
当前端有多个平台且接口存在差异时就需要设置VO对象用于前端和接口层传输数据 。
比如:
课程列表查询接口,根据需求用户在手机端也要查询课程信息,此时课程查询接口是否需要编写手机端和PC端两个接口呢?如果用户要求通过手机和PC的查询条件或查询结果不一样,此时就需要定义两个Controller课程查询接口,每个接口定义VO对象与前端传输数据。
此时,Service业务层尽量提供一个业务接口,即使两个前端接口需要的数据不一样,Service可以提供一个最全查询结果,由Controller进行数据整合。
如下图:
如果前端的接口没有多样性且比较固定,此时可以取消VO,只用DTO即可。
如下图:
生成接口文档 在前后端分离开发中通常由后端程序员设计接口,完成后需要编写接口文档,最后将文档交给前端工程师,前端工程师参考文档进行开发。
可以通过一些工具快速生成接口文档 ,本项目通过Swagger生成接口在线文档 。
什么是Swagger?
OpenAPI规范(OpenAPI Specification 简称OAS)是Linux基金会的一个项目,试图通过定义一种用来描述API格式或API定义的语言,来规范RESTful服务开发过程,目前版本是V3.0,并且已经发布并开源在github上。
(https://github.com/OAI/OpenAPI-Specification )
Swagger是全球最大的OpenAPI规范(OAS)API开发工具框架,Swagger是一个在线接口文档的生成工具,前后端开发人员依据接口文档进行开发。 (https://swagger.io/ )
Spring Boot 可以集成Swagger,Swaager根据Controller类中的注解生成接口文档 ,只要添加Swagger的依赖和配置信息即可使用它。
1、在API工程添加swagger-spring-boot-starter依赖
1 2 3 4 5 <dependency > <groupId > com.spring4all</groupId > <artifactId > swagger-spring-boot-starter</artifactId > </dependency >
2、在 bootstrap.yml中配置swagger的扫描包路径及其它信息,base-package为扫描的包路径,扫描Controller类。
1 2 3 4 5 6 swagger: title: "学成在线内容管理系统" description: "内容系统管理系统对课程相关信息进行管理" base-package: com.xuecheng.content enabled: true version: 1.0 .0
3、在启动类中添加@EnableSwagger2Doc
注解
再次启动服务,工程启动起来,访问http://localhost:63040/content/swagger-ui.html 查看接口信息
下图为swagger接口文档的界面:
这个文档存在两个问题:
1、接口名称显示course-info-controller名称不直观
2、课程查询是post方式只显示post /course/list即可。
下边进行修改,添加一些接口说明的注解,并且将RequestMapping(支持多种方式)改为PostMapping,如下:
1 2 3 4 5 @ApiOperation("课程查询接口") @PostMapping("/course/list") public PageResult<CourseBase> list (PageParams pageParams, @RequestBody(required = false) QueryCourseParamsDto queryCourseParamsDto) { return null ; }
5、再次启动服务,工程启动起来,访问http://localhost:63040/content/swagger-ui.html 查看接口信息
下图为swagger接口文档的界面:
接口文档中会有关于接口参数的说明,在模型类上也可以添加注解对模型类中的属性进行说明,方便对接口文档的阅读。
比如:下边标红的属性名称,可以通过swaager注解标注一个中文名称,方便阅读接口文档。
标注的方法非常简单:
找到模型类,在属性上添加注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Data @NoArgsConstructor @AllArgsConstructor public class PageParams { @ApiModelProperty("当前页码") private Long pageNo = 1L ; @ApiModelProperty("每页记录数") private Long pageSize = 10L ; } @Data public class QueryCourseParamsDto { @ApiModelProperty("审核状态") private String auditStatus; @ApiModelProperty("课程名称") private String courseName; @ApiModelProperty("课程发布状态 未发布 已发布 下线") private String publishStatus; }
重启服务,再次进入接口文档,如下图:
Swaager的常用注解如下:
在Java类中添加Swagger的注解即可生成Swagger接口,常用Swagger注解如下:
1 2 3 4 5 6 7 8 9 10 11 @Api :修饰整个类,描述Controller的作用@ApiOperation :描述一个类的一个方法,或者说一个接口@ApiParam :单个参数描述@ApiModel :用对象来接收参数@ApiModelProperty :用对象接收参数时,描述对象的一个字段@ApiResponse :HTTP响应其中1 个描述@ApiResponses :HTTP响应整体描述@ApiIgnore :使用该注解忽略这个API@ApiError :发生错误返回的信息@ApiImplicitParam :一个请求参数@ApiImplicitParams :多个请求参数
@ApiImplicitParam属性如下:
属性
取值
作用
paramType
查询参数类型
path
以地址的形式提交数据
query
直接跟参数完成自动映射赋值
body
以流的形式提交 仅支持POST
header
参数在request headers 里边提交
form
以form表单的形式提交 仅支持POST
dataType
参数的数据类型 只作为标志说明,并没有实际验证
Long
String
name
接收参数名
value
接收参数的意义描述
required
参数是否必填
true
必填
false
非必填
defaultValue
默认值
使用Swagger可以进行接口的测试。
修改接口内容,添加一些测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 Java @ApiOperation("课程查询接口") @PostMapping("/course/list") public PageResult<CourseBase> list (PageParams pageParams, @RequestBody(required=false) QueryCourseParamsDto queryCourseParams) { CourseBase courseBase = new CourseBase (); courseBase.setName("测试名称" ); courseBase.setCreateDate(LocalDateTime.now()); List<CourseBase> courseBases = new ArrayList (); courseBases.add(courseBase); PageResult pageResult = new PageResult <CourseBase>(courseBases,10 ,1 ,10 ); return pageResult; }
debug方式启动,在 return 处打断点,再用swagger请求接口。
通过下图可以看到请求参数已经正常请求至controller方法
放行继续运行,观察swagger界面,结果可以正常返回
不过存在一个问题就是LocalDateTime类型的数据转json后数据格式并不是我们要的年月日时分秒
在base工程com.zuoer.base.config包下加配置LocalDateTimeConfig 类实现转json时字符串与LocalDateTime类型的转换
LocalDateTimeConfig类
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 @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()); }; } }
开发持久层 生成mapper 本项目使用MyBatis-Plus开发持久层,需要创建PO类、Mapper接口、Mapper的xml文件,每个PO类对应数据库的每张表,每张表需要创建一个Mapper接口和Mapper的xml映射文件 。
使用generator工程生成的mapper接口和mapper映射文件 拷贝到service工程:
service工程即业务层为api接口工程提供业务处理支撑,本项目业务层包括了持久层的代码,一些大型公司的团队职责划分更细,会将持久层和业务层分为两个工程,不过这需要增加成本。
本项目使用持久层框架MyBatis-Plus进行开发,下边将mapper接口和xml文件 拷贝到 service工程 ,拷贝后如下图所示:
到此mapper接口与mapper映射文件生成完毕。
测试mapper 下边对mapper进行单元测试,测试course_base表的查询接口。
1、下边在service工程的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 <dependencies > <dependency > <groupId > com.zuoer</groupId > <artifactId > toddle-content-model</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > </dependency > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-context</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <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-log4j2</artifactId > </dependency > </dependencies >
2、配置扫描mapper及分页插件
从课程资料/工程目录下拷贝MybatisPlusConfig 到 service工程的com.xuecheng.content.config包下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration @MapperScan("com.zuoer.content.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor (DbType.MYSQL)); return interceptor; } }
分页插件的原理:
首先分页参数放到ThreadLocal中,拦截执行的sql,根据数据库类型添加对应的分页语句重写sql,例如:
1 2 (select * from table where a) 转换为 (select count (* ) from table where a)和(select * from table where a limit ,)
计算出了total总条数、pageNum当前第几页、pageSize每页大小和当前页的数据,是否为首页,是否为尾页,总页数等。
4、单元测试所需要的配置文件
单元测试要用,所以只拷贝到test下的resources就行
在test/resources下创建 log4j2-dev.xml、bootstrap.yml:
log4j2-dev.xml:从课程资料/项目工程 获取.
bootstrap.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: application: name: content-service datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.88.88:3306/toddle_content?serverTimezone=UTC&userUnicode=true&useSSL=false& username: root password: root logging: config: classpath:log4j2-dev.xml
5、编写启动类:
单元测试工作在test目录,在test下添加启动类
代码如下:
1 2 3 4 5 6 @SpringBootApplication public class ContentApplication { public static void main (String[] args) { SpringApplication.run(ContentApplication.class,args); } }
6、编写测试类
代码如下:
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 package com.zuoer.content;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.extension.plugins.pagination.Page;import com.zuoer.base.model.PageParams;import com.zuoer.base.model.PageResult;import com.zuoer.content.mapper.CourseBaseMapper;import com.zuoer.content.model.dto.QueryCourseParamsDto;import com.zuoer.content.model.po.CourseBase;import java.util.List;import org.apache.commons.lang3.StringUtils;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;@SpringBootTest public class CourseBaseMapperTests { @Autowired CourseBaseMapper courseBaseMapper; @Test public void testCourseBaseMapper () { CourseBase courseBase = courseBaseMapper.selectById(1 ); Assertions.assertNotNull(courseBase); QueryCourseParamsDto courseParamsDto = new QueryCourseParamsDto (); courseParamsDto.setCourseName("java" ); courseParamsDto.setAuditStatus("202004" ); courseParamsDto.setPublishStatus("203001" ); LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.like(StringUtils.isNotEmpty(courseParamsDto.getCourseName()), CourseBase::getName,courseParamsDto.getCourseName()); queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getAuditStatus()), CourseBase::getAuditStatus,courseParamsDto.getAuditStatus()); queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getPublishStatus()), CourseBase::getStatus,courseParamsDto.getPublishStatus()); PageParams pageParams = new PageParams (); pageParams.setPageNo(1L ); pageParams.setPageSize(2L ); Page<CourseBase> page = new Page <>(pageParams.getPageNo(), pageParams.getPageSize()); Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper); List<CourseBase> items = pageResult.getRecords(); Long total = pageResult.getTotal(); PageResult<CourseBase> courseBasePageResult = new PageResult <>(items, total, pageParams.getPageNo(), pageParams.getPageSize()); System.out.println(courseBasePageResult); } }
运行测试类的测试方法进行测试,测试成功:
开发业务层 创建数据字典表 课程基本信息查询的主要数据来源是课程基本信息表,这里有一个点需要注意,就是课程的审核状态、发布状态。
审核状态在查询条件和查询结果中都存在,审核状态包括:未审核、审核通过、审核未通过三种,下边思考一个问题:一个课程的审核状态如果是“审核未通过”那么在课程基本信息表记录“审核未通过”三个字合适吗?
如果将“审核未通过”五个字记录在课程基本信息表中,显示出来的审核状态就是“审核未通过”这五个字,看起来没有什么问题,如果有一天客户想要将审核未通过的记录在显示时改为“未通过”三个字,怎么办?
这时你可以需要批量处理数据库中记录了,写一个 update 语句,审核状态等于“审核未通过”的全部更新 为“未通过”。看起来解决了问题,如果有一天客户又让改了呢?
和审核状态同类的有好多这样的信息,比如:课程状态、课程类型、用户类型等等,这一类数据有一个共同点就是它有一些分类项,且这些分类项较为固定。针对这些数据,为了提高系统的可扩展性,专门定义数据字典表去维护。
下边是课程审核状态的定义:
1 2 3 4 5 [ { "code" : "202001" , "desc" : "审核未通过" } , { "code" : "202002" , "desc" : "未审核" } , { "code" : "202003" , "desc" : "审核通过" } ]
每一项都由代码和名称组成。
此时我们好像知道要干什么了 ,该课程 的审核状态为审核未通过,那么我们在课程基本信息表存储202001,也就是审核未通过对应的代码,这样查询出的数据在前端展示时根据代码取出它对应的内容显示给用户。如果用户要修改“审核未通过”的显示内容只需要在数据字典表修改,无法修改课程基本信息表。
数据字典表在系统管理数据库中存储,首先导入系统管理数据库,创建系统管理服务的数据库
创建系统管理的数据库,导入课程资料中的toddle_system.sql脚本。
编写Service 接下来开发Service方法,首先创建Service接口:
1 2 3 4 5 6 7 8 9 10 public interface CourseBaseService extends IService <CourseBase> { public PageResult<CourseBase> queryCourseBaseList (PageParams pageParams, QueryCourseParamsDto queryCourseParamsDto) ; }
再创建接口实现类
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 @Slf4j @Service public class CourseBaseServiceImpl extends ServiceImpl <CourseBaseMapper, CourseBase> implements CourseBaseService { @Autowired CourseBaseMapper courseBaseMapper; @Override public PageResult<CourseBase> queryCourseBaseList (PageParams pageParams, QueryCourseParamsDto courseParamsDto) { LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.like(StringUtils.isNotEmpty(courseParamsDto.getCourseName()), CourseBase::getName,courseParamsDto.getCourseName()); queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getAuditStatus()), CourseBase::getAuditStatus,courseParamsDto.getAuditStatus()); queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getPublishStatus()), CourseBase::getStatus,courseParamsDto.getPublishStatus()); Page<CourseBase> page = new Page <>(pageParams.getPageNo(), pageParams.getPageSize()); Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper); List<CourseBase> items = pageResult.getRecords(); Long total = pageResult.getTotal(); PageResult<CourseBase> courseBasePageResult = new PageResult <>(items, total, pageParams.getPageNo(), pageParams.getPageSize()); System.out.println(courseBasePageResult); return courseBasePageResult; } }
测试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 @SpringBootTest public class CourseBaseServiceTests { @Autowired CourseBaseService courseBaseService; @Test public void testCourseBaseMapper () { PageParams pageParams = new PageParams (); pageParams.setPageNo(1L ); pageParams.setPageSize(2L ); QueryCourseParamsDto courseParamsDto = new QueryCourseParamsDto (); courseParamsDto.setCourseName("java" ); courseParamsDto.setAuditStatus("202004" ); courseParamsDto.setPublishStatus("203001" ); PageResult<CourseBase> courseBasePageResult = courseBaseService.queryCourseBaseList(pageParams, courseParamsDto); System.out.println(courseBasePageResult); } }
接口测试 接口完善 控制层、业务层以及持久层三层通常可以面向接口并行开发,比如:业务层开发的同事可以先只编写一个Service接口,接口层的同事即可面向Service接口去开发,待接口层和业务层完成后进行连调。
下边课程查询接口的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Api(value = "课程信息管理接口",tags = "课程信息管理接口") @RestController public class CourseBaseInfoController { @Autowired CourseBaseService courseBaseService; @ApiOperation("课程查询接口") @PostMapping("/course/list") public PageResult<CourseBase> list (PageParams pageParams, @RequestBody(required = false) QueryCourseParamsDto queryCourseParamsDto) { return courseBaseService.queryCourseBaseList(pageParams, queryCourseParamsDto); } }
代码编辑完毕,再次打开Swagger进行测试。
输入查询条件:
测试,观察结果是否正确。
Httpclient测试
Swagger是一个在线接口文档,虽然使用它也能测试但需要浏览器进入Swagger,最关键的是它并不能保存测试数据。
在IDEA中有一个非常方便的http接口测试工具httpclient,下边介绍它的使用方法,后边我们会用它进行接口测试。
如果IDEA版本较低没有自带httpclient,需要安装httpclient插件
进入controller类,找到http接口对应的方法
点击Generate request in HTTP Client即可生成的一个测试用例。
可以看到自己生成了一个.http结尾的文件
我们可以添加请求参数进行测试
参数添加完毕可以运行它
观察控制台,测试通过。
.http文件即测试用例文档 ,它可以随着项目工程一起保存,这样测试的数据就可以保存下来,方便进行测试。
为了方便保存.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" } }
再回到toddle-content-api.http文件,将http://localhost:63040 用变量代替
到此就完成了httpclient的配置与使用测试。
前后端联调 准备环境
什么是前后端联调?
通常由后端工程师将接口设计好并编写接口文档,将接口文档交给前端工程师,前后端的工程师就开始并行开发,前端开发人员会使用mock数据(假数据)进行开发,当前后端代码完成后开始进行接口联调,前端工程师将mock数据改为请求后端接口获取,前端代码请求后端服务测试接口是否正常,这个过程是前后端联调。
当前后端联调出现问题需要根据测试环境下接口的请求及响应数据内容去判断是否符合接口文档的要求。查出是前端或后端的问题由具体的工程师负责修改缺陷,修改后再次回归测试。
在教学中进行前后端联调,首先配置前端环境,下边我们安装前端工程运行的环境。
首先从软件工具目录找到node-v16.17.0-x64.msi安装nodejs
安装完成,查看版本号
在idea中配置node.js的路径
下边启动前端工程,从前端工程拷贝toddle-portal-vue-ts.zip到代码目录并解压,并使用IDEA或VS Code打开toddle-portal-vue-ts目录,npm run dev
访问http://localhost:8601即可访问前端工程。
如果存在问题通过以下命令启动:
1、cmd进入工程根目录
2、运行以下命令
1 2 3 4 5 npm install -g cnpm --registry=https : cnpm i npm run serve
安装系统管理服务 启动前端工程成功,在浏览器通过http://localhost:8601/ 地址访问前端工程。
前端工程报错如下:
http://localhost:8601/system/dictionary/all 指向的是系统管理服务。在前端讲解内容管理模块的需求时我们提到一个数据字典表,此链接正是在前端请求后端获取数据字典数据的接口地址。
数据字典表中配置了项目用的字典信息,此接口是查询字典中的全部数据 ,在此我们不再开发,按照下边的步骤安装系统管理服务即可。
从课程资料/项目工程目录获取toddle-system.zip,并解压
将toddle-system目录拷贝到项目工程根目录,刷新maven,或进入pom.xml右键转为pom工程。
进入toddle-system-service工程,找到resources下的application.yml修改数据库连接参数。
启动系统管理服务,启动成功,在浏览器请求:http://localhost:63110/system/dictionary/all
系统服务的端口是63110
如果可以正常读取数据字典信息则说明系统管理服务安装成功。
3.8.3 解决跨域问题
在浏览器通过http://localhost:8601/ 地址访问前端工程。
chrome浏览器报错如下:
提示:从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.10: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函数的方式告诉请求方。如下图:
2、添加响应头
服务端在响应头添加 Access-Control-Allow-Origin:*
3、通过nginx代理跨域
由于服务端之间没有跨域,浏览器通过nginx去访问跨域地址。
1)浏览器先访问http://192.168.101.10:8601 nginx提供的地址,进入页面
2)此页面要跨域访问http://192.168.101.11:8601 ,不能直接跨域访问http://www.baidu.com:8601 ,而是访问nginx的一个同源地址,比如:http://192.168.101.11:8601/api ,通过http://192.168.101.11:8601/api 的代理去访问http://www.baidu.com:8601 。
这样就实现了跨域访问。
浏览器到http://192.168.101.11:8601/api 没有跨域
nginx到http://www.baidu.com:8601 通过服务端通信,没有跨域。
我们准备使用方案2解决跨域问题。在内容管理的api工程config包下编写GlobalCorsConfig.java,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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 ,观察浏览器记录,成功解决跨域。
前后端联调 这里进行前后联调的目的是体会前后端联调的流程,测试的功能为课程查询功能。
1、启动前端工程,再启内容管理服务端。
2、修改服务端地址
前端默认连接的是项目的网关地址,由于现在网关工程还没有创建,这里需要更改前端工程的参数配置文件 ,修改网关地址为内容管理服务的地址。
启动前端工程,用前端访问后端接口,观察前端界面的数据是否正确。
访问前端首页,进入课程管理:http://localhost:8601/#/organization/course-list
更改课程条件及分页参数测试课程查询列表是否正常显示。
跟踪内容管理服务的输出日志,查看是否正常。
到此基本完成了前后端连调。
课程分类查询 需求分析 下边根据内容管理模块的业务流程,下一步要实现新增课程,在新增课程界面,有三处信息需要选择,如下图:
课程等级、课程类型来源于数据字典表,此部分的信息前端已从系统管理服务读取。
课程分类信息没有在数据字典表中存储,而是由单独一张课程分类表,存储在内容管理数据库中。
下边看下course_category课程分类表的结构
这张表是一个树型结构,通过父结点id将各元素组成一个树。
我们可以看下该表的数据,下图是一部分数据:
现在的需求是需要在内容管理服务中编写一个接口读取该课程分类表的数据,组成一个树型结构返回给前端。
课程分类的PO类如下:
如果没有此po类则需要生成的此表的po类拷贝到内容管理模块的model工程中,将mapper拷贝到内容管理模块的service工程中。
接口定义 我们可以点击新增课程,观察前端的请求记录:
http://localhost:8601/api/content/course-category/tree-nodes 该地址正是前端获取课程分类的接口地址。
通过上图界面的内容可以看出该接口的协议为:HTTP GET
请求参数为空。
通过查阅接口文档,此接口要返回全部课程分类,以树型结构返回,如下所示。
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 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 [ { "childrenTreeNodes" : [ { "childrenTreeNodes" : null , "id" : "1-1-1" , "isLeaf" : null , "isShow" : null , "label" : "HTML/CSS" , "name" : "HTML/CSS" , "orderby" : 1 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-2" , "isLeaf" : null , "isShow" : null , "label" : "JavaScript" , "name" : "JavaScript" , "orderby" : 2 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-3" , "isLeaf" : null , "isShow" : null , "label" : "jQuery" , "name" : "jQuery" , "orderby" : 3 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-4" , "isLeaf" : null , "isShow" : null , "label" : "ExtJS" , "name" : "ExtJS" , "orderby" : 4 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-5" , "isLeaf" : null , "isShow" : null , "label" : "AngularJS" , "name" : "AngularJS" , "orderby" : 5 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-6" , "isLeaf" : null , "isShow" : null , "label" : "ReactJS" , "name" : "ReactJS" , "orderby" : 6 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-7" , "isLeaf" : null , "isShow" : null , "label" : "Bootstrap" , "name" : "Bootstrap" , "orderby" : 7 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-8" , "isLeaf" : null , "isShow" : null , "label" : "Node.js" , "name" : "Node.js" , "orderby" : 8 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-9" , "isLeaf" : null , "isShow" : null , "label" : "Vue" , "name" : "Vue" , "orderby" : 9 , "parentid" : "1-1" } , { "childrenTreeNodes" : null , "id" : "1-1-10" , "isLeaf" : null , "isShow" : null , "label" : "其它" , "name" : "其它" , "orderby" : 10 , "parentid" : "1-1" } ] , "id" : "1-1" , "isLeaf" : null , "isShow" : null , "label" : "前端开发" , "name" : "前端开发" , "orderby" : 1 , "parentid" : "1" } , { "childrenTreeNodes" : [ { "childrenTreeNodes" : null , "id" : "1-2-1" , "isLeaf" : null , "isShow" : null , "label" : "微信开发" , "name" : "微信开发" , "orderby" : 1 , "parentid" : "1-2" } , { "childrenTreeNodes" : null , "id" : "1-2-2" , "isLeaf" : null , "isShow" : null , "label" : "iOS" , "name" : "iOS" , "orderby" : 2 , "parentid" : "1-2" } , { "childrenTreeNodes" : null , "id" : "1-2-3" , "isLeaf" : null , "isShow" : null , "label" : "手游开发" , "name" : "手游开发" , "orderby" : 3 , "parentid" : "1-2" } , { "childrenTreeNodes" : null , "id" : "1-2-4" , "isLeaf" : null , "isShow" : null , "label" : "Swift" , "name" : "Swift" , "orderby" : 4 , "parentid" : "1-2" } , { "childrenTreeNodes" : null , "id" : "1-2-5" , "isLeaf" : null , "isShow" : null , "label" : "Android" , "name" : "Android" , "orderby" : 5 , "parentid" : "1-2" } , { "childrenTreeNodes" : null , "id" : "1-2-6" , "isLeaf" : null , "isShow" : null , "label" : "ReactNative" , "name" : "ReactNative" , "orderby" : 6 , "parentid" : "1-2" } , { "childrenTreeNodes" : null , "id" : "1-2-7" , "isLeaf" : null , "isShow" : null , "label" : "Cordova" , "name" : "Cordova" , "orderby" : 7 , "parentid" : "1-2" } , { "childrenTreeNodes" : null , "id" : "1-2-8" , "isLeaf" : null , "isShow" : null , "label" : "其它" , "name" : "其它" , "orderby" : 8 , "parentid" : "1-2" } ] , "id" : "1-2" , "isLeaf" : null , "isShow" : null , "label" : "移动开发" , "name" : "移动开发" , "orderby" : 2 , "parentid" : "1" } ]
上边的数据格式是一个数组结构,数组的元素即为分类信息,分类信息设计两级分类,第一级的分类信息示例如下:
1 2 3 4 5 6 7 "id" : "1-2", "isLeaf" : null, "isShow" : null, "label" : "移动开发", "name" : "移动开发", "orderby" : 2, "parentid" : "1"
第二级的分类是第一级分类中childrenTreeNodes属性,它是一个数组结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "id" : "1-2" , "isLeaf" : null , "isShow" : null , "label" : "移动开发" , "name" : "移动开发" , "orderby" : 2 , "parentid" : "1" , "childrenTreeNodes" : [ { "childrenTreeNodes" : null , "id" : "1-2-1" , "isLeaf" : null , "isShow" : null , "label" : "微信开发" , "name" : "微信开发" , "orderby" : 1 , "parentid" : "1-2" } }
所以,定义一个DTO类表示分类信息的模型类,如下:
1 2 3 4 @Data public class CourseCategoryTreeDto extends CourseCategory implements Serializable { List<CourseCategoryTreeDto> childrenTreeNodes; }
接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Slf4j @RestController @RequestMapping("courseCategory") public class CourseCategoryController { @Autowired private CourseCategoryService courseCategoryService; @GetMapping("/course-category/tree-nodes") public List<CourseCategoryTreeDto> queryTreeNodes () { return null ; } }
接口开发 树型表查询 课程分类表是一个树型结构,其中parentid字段为父结点ID,它是树型结构的标志字段。
如果树的层级固定可以使用表的自链接去查询,比如:我们只查询两级课程分类,可以用下边的SQL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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
如果树的层级不确定,此时可以使用MySQL递归实现,使用with语法,如下:
1 2 3 WITH [RECURSIVE ] cte_name [(col_name [, col_name] ...)] AS (subquery) [, cte_name [(col_name [, col_name] ...)] AS (subquery)] ...
cte_name :公共表达式的名称,可以理解为表名,用来表示as后面跟着的子查询
col_name :公共表达式包含的列名,可以写也可以不写
下边是一个递归的简单例子:
1 2 3 4 5 6 7 with RECURSIVE t1 AS ( SELECT 1 as n UNION ALL SELECT n + 1 FROM t1 WHERE n < 5 ) SELECT * FROM t1;
输出:
说明:
t1 相当于一个表名
select 1 相当于这个表的初始值,这里使用UNION ALL 不断将每次递归得到的数据加入到表中。
n<5为递归执行的条件,当n>=5时结束递归调用。
向下递归
下边我们使用递归实现课程分类的查询
1 2 3 4 5 6 7 with recursive t1 as ( select * from course_category p where id= '1' union all select t.* from course_category t inner join t1 on t1.id = t.parentid ) select * from t1 order by t1.id, t1.orderby
查询结果如下:
t1表中初始的数据是id等于1的记录,即根结点。
通过inner join t1 t2 on t2.id = t.parentid 找到id=’1’的下级节点 。
通过这种方法就找到了id=’1’的所有下级节点,下级节点包括了所有层级的节点。
上边这种方法是向下递归,即找到初始节点的所有下级节点。
如何向上递归?
下边的sql实现了向上递归:
1 2 3 4 5 6 7 with recursive t1 as ( select * from course_category p where id= '1-1-1' union all select t.* from course_category t inner join t1 on t1.parentid = t.id ) select * from t1 order by t1.id, t1.orderby
初始节点为1-1-1,通过递归找到它的父级节点,父级节点包括所有级别的节点。
以上是我们研究了树型表的查询方法,通过递归的方式查询课程分类比较灵活,因为它可以不限制层级。
mysql为了避免无限递归默认递归次数为1000,可以通过设置cte_max_recursion_depth
参数增加递归深度,还可以通过max_execution_time
限制执行时间,超过此时间也会终止递归操作。
mysql递归相当于在存储过程中执行若干次sql语句,java程序仅与数据库建立一次链接执行递归操作,所以只要控制好递归深度,控制好数据量性能就没有问题。
思考:如果java程序在递归操作中连接数据库去查询数据组装数据,这个性能高吗?
如果是多次连接数据库查询拼接的java代码,性能不会太高
开发Mapper 下边我们可自定义mapper方法查询课程分类,最终将查询结果映射到List<CourseCategoryTreeDto>
中。
生成课程分类表的mapper文件并拷贝至内容管理模块 的service工程中。
1、下边 定义一个mapper方法,并定义sql语句。
1 2 3 4 5 6 public interface CourseCategoryMapper extends BaseMapper <CourseCategory> { public List<CourseCategoryTreeDto> selectTreeNodes (String id) ; }
2、找到对应 的mapper.xml文件,编写sql语句。
1 2 3 4 5 6 7 8 9 < select id= "selectTreeNodes" resultType= "com.zuoer.content.model.dto.CourseCategoryTreeDto" parameterType= "string"> with recursive t1 as ( select * from course_category p where id = #{id} union all select t.* from course_category t inner join t1 on t.parentid = t1.id ) select * from t1 order by t1.id, t1.orderby < / select >
开发service 定义service接口,调用mapper查询课程分类,遍历数据按照接口要求对数据进行封装
1 2 3 4 5 6 7 8 public interface CourseCategoryService extends IService <CourseCategory> { public List<CourseCategoryTreeDto> selectTreeNodes (String id) ; }
编写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 @Slf4j @Service public class CourseCategoryServiceImpl extends ServiceImpl <CourseCategoryMapper, CourseCategory> implements CourseCategoryService { @Autowired CourseCategoryMapper courseCategoryMapper; @Override public List<CourseCategoryTreeDto> selectTreeNodes (String id) { List<CourseCategoryTreeDto> courseCategoryTreeDtos = courseCategoryMapper.selectTreeNodes(id); Map<String, CourseCategoryTreeDto> mapTemp = courseCategoryTreeDtos.stream().filter(item -> !id.equals(item.getId())) .collect(Collectors.toMap( CourseCategory::getId, value -> value, (key1, key2) -> key2)); List<CourseCategoryTreeDto> categoryTreeDtos = new ArrayList <>(); courseCategoryTreeDtos.stream().filter(item -> !id.equals(item.getId())) .forEach(item -> { if (item.getParentid().equals(id)) { categoryTreeDtos.add(item); } CourseCategoryTreeDto courseCategoryTreeDto = mapTemp.get(item.getParentid()); if (courseCategoryTreeDto != null ) { if (courseCategoryTreeDto.getChildrenTreeNodes() == null ) { courseCategoryTreeDto.setChildrenTreeNodes( new ArrayList <CourseCategoryTreeDto>()); } courseCategoryTreeDto.getChildrenTreeNodes().add(item); } }); return categoryTreeDtos; } }
单元测试 定义单元测试类对service接口进行测试
1 2 3 4 5 6 7 8 9 10 11 @SpringBootTest public class CourseCategoryServiceTests { @Autowired CourseCategoryService courseCategoryService; @Test public void testCourseCategoryService () { List<CourseCategoryTreeDto> categoryTreeDtos = courseCategoryService.selectTreeNodes("1" ); System.out.println("categoryTreeDtos = " + categoryTreeDtos); } }
接口测试 接口层代码完善 完善controller方法,注入service调用业务层方法查询课程分类。
1 2 3 4 5 6 7 8 9 10 11 @RestController public class CourseCategoryController { @Autowired private CourseCategoryService courseCategoryService; @GetMapping("/course-category/tree-nodes") public List<CourseCategoryTreeDto> queryTreeNodes () { return courseCategoryService.selectTreeNodes("1" ); } }
测试接口 使用httpclient测试:
定义.http文件
运行测试。
完成前后端连调:
打开前端工程,进入新增课程页面。
课程分类下拉框可以正常显示
新增课程 需求分析 业务流程 根据前边对内容管理模块的数据模型分析,课程相关的信息有:课程基本信息、课程营销信息、课程图片信息、课程计划、课程师资信息,所以新增一门课程需要完成这几部分信息的填写。
以下是业务流程:
1、进入课程查询列表
2、点击添加课程,选择课程形式为录播。
3、选择完毕,点击下一步,进入课程基本信息添加界面。
本界面分两部分信息,一部分是课程基本信息上,一部分是课程营销信息。
课程基本信息:
课程营销信息:
在这个界面中填写课程的基本信息、课程营销信息上。
填写完毕,保存并进行下一步。
4、在此界面填写课程计划信息
课程计划即课程的大纲目录。
课程计划分为两级,章节和小节。
每个小节需要上传课程视频,用户点击 小节的标题即开始播放视频。
如果是直播课程则会进入直播间。
5、课程 计划填写完毕进入课程师资的管理。
在课程师资界面维护该课程的授课老师。
至此,一门课程新增完成。
数据模型 通过业务流程可知,一门课程信息涉及:课程基本信息、课程营销信息、课程计划信息、课程师资信息。
本节开发新增课程按钮功能, 只向课程基本信息、课程营销信息添加记录。
这两部分信息分别在course_base、course_market两张表存储。当点击保存按钮时向这两张表插入数据。这两张表是一对一关联关系。
新建课程的初始审核状态为“未提交”、初始发布状态为“未发布”。
生成课程基本信息、课程营销信息的PO、Mapper文件
接口定义 根据业务流程,这里先定义提交课程基本信息的接口。
1、接口协议 :HTTP POST,Content-Type为application/json
2、请求及响应结果如下
3、接口请求示例如下
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 ### 创建课程 POST { { content_host} } /content/course Content-Type: application/json { "mt" : "" , "st" : "" , "name" : "" , "pic" : "" , "teachmode" : "200002" , "users" : "初级人员" , "tags" : "" , "grade" : "204001" , "description" : "" , "charge" : "201000" , "price" : 0 , "originalPrice" : 0 , "qq" : "" , "wechat" : "" , "phone" : "" , "validDays" : 365 } ###响应结果如下 #成功响应结果如下 { "id" : 109 , "companyId" : 1 , "companyName" : null , "name" : "测试课程103" , "users" : "初级人员" , "tags" : "" , "mt" : "1-1" , "mtName" : null , "st" : "1-1-1" , "stName" : null , "grade" : "204001" , "teachmode" : "200002" , "description" : "" , "pic" : "" , "createDate" : "2022-09-08 07:35:16" , "changeDate" : null , "createPeople" : null , "changePeople" : null , "auditStatus" : "202002" , "status" : 1 , "coursePubId" : null , "coursePubDate" : null , "charge" : "201000" , "price" : null , "originalPrice" : 0 , "qq" : "" , "wechat" : "" , "phone" : "" , "validDays" : 365 }
3、定义请求参数类型和响应结构类型
根据接口定义内容,请求参数相比 CourseBase模型类不一致,需要在dto包下自定义,模型类从课程资料/工程目录获取。
4、定义接口如下
1 2 3 4 5 @ApiOperation("新增课程基础信息") @PostMapping("/course") public CourseBaseInfoDto createCourseBase (@RequestBody AddCourseDto addCourseDto) { return null ; }
接口开发 保存课程基本信息 根据需求分析,新增课程表单中包括了课程基本信息、课程营销信息,需要分别向课程基本信息表、课程营销表保证数据。
首先定义service接口,
1 2 3 4 5 6 7 public 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 @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.setAuditStatus("202002" ); courseBaseNew.setStatus("203001" ); courseBaseNew.setCompanyId(companyId); courseBaseNew.setCreateDate(LocalDateTime.now()); int insert = courseBaseMapper.insert(courseBaseNew); if (insert <= 0 ) { throw new RuntimeException ("新增课程基本信息失败" ); } return null ; }
保存营销信息 下边实现向课程营销表保存课程营销信息
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 @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.setAuditStatus("202002" ); courseBaseNew.setStatus("203001" ); courseBaseNew.setCompanyId(companyId); courseBaseNew.setCreateDate(LocalDateTime.now()); int insert = courseBaseMapper.insert(courseBaseNew); if (insert <= 0 ) { throw new RuntimeException ("新增课程基本信息失败" ); } CourseMarket courseMarketNew = new CourseMarket (); Long courseId = courseBaseNew.getId(); BeanUtils.copyProperties(dto,courseMarketNew); courseMarketNew.setId(courseId); int i = saveCourseMarket(courseMarketNew); if (i<= 0 ) { throw new RuntimeException ("保存课程营销信息失败" ); } return getCourseBaseInfo(courseId); } private CourseBaseInfoDto getCourseBaseInfo (Long courseId) { CourseBase courseBase = courseBaseMapper.selectById(courseId); if (courseBase == null ) { return null ; } CourseMarket courseMarket = courseMarketMapper.selectById(courseId); CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto (); BeanUtils.copyProperties(courseBase,courseBaseInfoDto); 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; } private int saveCourseMarket (CourseMarket courseMarketNew) { String charge = courseMarketNew.getCharge(); if (StringUtils.isBlank(charge)) { throw new RuntimeException ("收费规则没有选择" ); } if ("201000" .equals(charge)) { if (courseMarketNew.getPrice() == null || courseMarketNew.getPrice().floatValue() <= 0 ) { throw new RuntimeException ("课程为收费时价格不能为空且必须大于0" ); } } CourseMarket courseMarketObj = courseMarketMapper.selectById(courseMarketNew.getId()); if (courseMarketObj == null ) { return courseMarketMapper.insert(courseMarketNew); } else { BeanUtils.copyProperties(courseMarketNew,courseMarketObj); courseMarketObj.setId(courseMarketNew.getId()); return courseMarketMapper.updateById(courseMarketObj); } }
接口测试 1、首先去完善controller方法:
1 2 3 4 5 6 7 @ApiOperation("新增课程基础信息") @PostMapping("/course") public CourseBaseInfoDto createCourseBase (@RequestBody AddCourseDto addCourseDto) { Long companyId = 1232141425L ; return courseBaseService.createCourseBase(companyId,addCourseDto); }
2、使用httpclient测试
在toddle-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 ### 创建课程 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" : "" }
3、前后端联调
打开新增课程页面,除了课程图片其它信息全部输入。
点击保存,观察浏览器请求接口参数及响应结果是否正常。
异常处理 异常问题分析 在service方法中有很多的参数合法性校验,当参数不合法则抛出异常,下边我们测试下异常处理。
请求创建课程基本信息,故意将必填项设置为空。
测试发现报500异常,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Java http: HTTP/1.1 500 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 07 Sep 2022 11 : 40 : 29 GMT Connection: close { "timestamp" : "2022-09-07T11:40:29.677+00:00" , "status" : 500 , "error" : "Internal Server Error" , "message" : "" , "path" : "/content/course" }
问题:并没有输出我们抛出异常时指定的异常信息。
所以,现在我们的需求是当正常操作时按接口要求返回数据,当非正常流程时要获取异常信息进行记录,并提示给用户。
异常处理除了输出在日志中,还需要提示给用户,前端和后端需要作一些约定:
1、错误提示信息统一以json格式返回给前端。
2、以HTTP状态码决定当前是否出错,非200为操作异常。
如何规范异常信息?
代码中统一抛出项目的自定义异常类型,这样可以统一去捕获这一类或几类的异常。
规范了异常类型就可以去获取异常信息。
如果捕获了非项目自定义的异常类型统一向用户提示“执行过程异常,请重试”的错误信息。
如何捕获异常?
代码统一用try/catch方式去捕获代码比较臃肿,可以通过SpringMVC提供的控制器增强类统一由一个类去完成异常的捕获。
如下图:
统一异常处理实现 根据上边分析的方案,统一在base基础工程实现统一异常处理,各模块依赖了base基础工程都 可以使用。
首先在base基础工程添加需要依赖的包:
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > </dependency >
1、定义一些通用的异常信息
从课程资料/工程目录 拷贝CommonError 类到base工程com.zuoer.base.execption下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.zuoer.base.exception;public enum CommonError { UNKOWN_ERROR("执行过程异常,请重试。" ), PARAMS_ERROR("非法参数" ), OBJECT_NULL("对象为空" ), QUERY_NULL("查询结果为空" ), REQUEST_NULL("请求参数为空" ); private String errMessage; public String getErrMessage () { return errMessage; } private CommonError ( String errMessage) { this .errMessage = errMessage; } }
2、自定义异常类型
在base工程com.zuoer.base.execption下自定义异常类。
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 package com.zuoer.base.exception;public class ToddleException extends RuntimeException { private String errMessage; public ToddleException () { super (); } public ToddleException (String errMessage) { super (errMessage); this .errMessage = errMessage; } public String getErrMessage () { return errMessage; } public static void cast (CommonError commonError) { throw new ToddleException (commonError.getErrMessage()); } public static void cast (String errMessage) { throw new ToddleException (errMessage); } }
3、响应用户的统一类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.zuoer.base.exception;import java.io.Serializable;public class RestErrorResponse implements Serializable { private String errMessage; public RestErrorResponse (String errMessage) { this .errMessage= errMessage; } public String getErrMessage () { return errMessage; } public void setErrMessage (String errMessage) { this .errMessage = errMessage; } }
4、全局异常处理器
从 Spring 3.0 - Spring 3.2 版本之间,对 Spring 架构和 SpringMVC 的Controller 的异常捕获提供了相应的异常处理。
@ExceptionHandler: Spring3.0提供的标识在方法上或类上的注解,用来表明方法的处理异常类型。
@ControllerAdvice: Spring3.2提供的新注解,从名字上可以看出大体意思是控制器增强, 在项目中来增强SpringMVC中的Controller。通常和**@ExceptionHandler** 结合使用,来处理SpringMVC的异常信息。
@ResponseStatus: Spring3.0提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。 调用处理程序方法时,状态代码将应用于HTTP响应。
通过上面的两个注解便可实现微服务端全局异常处理,具体代码如下:
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.zuoer.base.exception;import lombok.extern.slf4j.Slf4j;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice;@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(ToddleException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public RestErrorResponse customException (ToddleException e) { log.error("【系统异常】{}" ,e.getErrMessage(),e); return new RestErrorResponse (e.getErrMessage()); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public RestErrorResponse exception (Exception e) { log.error("【系统异常】{}" ,e.getMessage(),e); return new RestErrorResponse (CommonError.UNKOWN_ERROR.getErrMessage()); } }
异常处理测试 在内容管理的api工程添加base工程的依赖
1 2 3 4 5 <dependency > <groupId > com.xuecheng</groupId > <artifactId > xuecheng-plus-base</artifactId > <version > 0.0.1-SNAPSHOT</version > </dependency >
在异常处理测试之前首先在代码中抛出自定义类型的异常,这里以新增课程的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 @Override public CourseBaseInfoDto createCourseBase (Long companyId,AddCourseDto dto) { if (StringUtils.isBlank(dto.getName())) { ToddleException.cast("课程名称为空" ); } if (StringUtils.isBlank(dto.getMt())) { ToddleException.cast("课程分类为空" ); } if (StringUtils.isBlank(dto.getSt())) { ToddleException.cast("课程分类为空" ); } if (StringUtils.isBlank(dto.getGrade())) { ToddleException.cast("课程等级为空" ); } if (StringUtils.isBlank(dto.getTeachmode())) { ToddleException.cast("教育模式为空" ); } if (StringUtils.isBlank(dto.getUsers())) { ToddleException.cast("适应人群为空" ); } if (StringUtils.isBlank(dto.getCharge())) { ToddleException.cast("收费规则为空" ); } ...... if (charge.equals("201001" )){ if (courseMarketNew.getPrice() ==null || courseMarketNew.getPrice().floatValue()<=0 ){ throw new XueChengPlusException ("课程的价格不能为空并且必须大于0" ); } } }
1、首先使用httpclient测试
请求新增课程接口,故意将必填项课程名称设置为空。
测试结果与预期一致,可以捕获异常并响应异常信息,如下:
1 2 3 4 5 6 7 8 9 10 11 http: HTTP/1.1 500 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 07 Sep 2022 13 : 17 : 14 GMT Connection: close { "errMessage" : "课程名称为空。" }
2、前后端调试
仍然测试新增课程接口,当课程收费的时候必须填写价格,这里设置课程为收费,价格设置为空。
通过测试发现,前端正常提示代码 中抛出的异常信息。
至此,项目异常处理的测试完毕,我们在开发中对于业务分支中错误的情况要抛出项目自定义的异常类型。
JSR303校验 统一校验的需求 前端请求后端接口传输参数,是在controller中校验还是在Service中校验?
答案是都需要校验,只是分工不同。
Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。
Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。
Service中根据业务规则去校验不方便写成通用代码,Controller中则可以将校验的代码写成通用代码。
早在JavaEE6规范中就定义了参数校验的规范,它就是JSR-303,它定义了Bean Validation,即对bean属性进行校验。
SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。
所以,我们准备在Controller层使用spring-boot-starter-validation完成对请求参数的基本合法性 进行校验。
统一校验实现 首先在Base工程添加spring-boot-starter-validation的依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-validation</artifactId > </dependency >
在javax.validation.constraints包下有很多这样的校验注解,直接使用注解定义校验规则即可。
规则如下:
现在准备对内容管理模块添加课程接口进行参数校验,如下接口
1 2 3 4 5 6 7 @ApiOperation("新增课程基础信息") @PostMapping("/course") public CourseBaseInfoDto createCourseBase (@RequestBody AddCourseDto addCourseDto) { Long companyId = 1232141425L ; return courseBaseInfoService.createCourseBase(companyId,addCourseDto); }
此接口使用AddCourseDto模型对象接收参数,所以进入AddCourseDto类,在属性上添加校验规则。
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 @Data @ApiModel(value = "AddCourseDto", description = "新增课程基本信息") public class AddCourseDto { @NotEmpty(message = "课程名称不能为空") @ApiModelProperty(value = "课程名称", required = true) private String name; @NotEmpty(message = "适用人群不能为空") @Size(message = "适用人群内容过少", min = 10) @ApiModelProperty(value = "适用人群", required = true) private String users; @ApiModelProperty(value = "课程标签") private String tags; @NotEmpty(message = "课程分类不能为空") @ApiModelProperty(value = "大分类", required = true) private String mt; @NotEmpty(message = "课程分类不能为空") @ApiModelProperty(value = "小分类", required = true) private String st; @NotEmpty(message = "课程等级不能为空") @ApiModelProperty(value = "课程等级", required = true) private String grade; @ApiModelProperty(value = "教学模式(普通,录播,直播等)", required = true) private String teachmode; @ApiModelProperty(value = "课程介绍") @Size(message = "课程描述内容过少",min = 10) private String description; @ApiModelProperty(value = "课程图片", required = true) private String pic; @NotEmpty(message = "收费规则不能为空") @ApiModelProperty(value = "收费规则,对应数据字典", required = true) private String charge; @ApiModelProperty(value = "价格") private Float price; @ApiModelProperty(value = "原价") private Float originalPrice; @ApiModelProperty(value = "qq") private String qq; @ApiModelProperty(value = "微信") private String wechat; @ApiModelProperty(value = "电话") private String phone; @ApiModelProperty(value = "有效期") private Integer validDays; }
上边用到了@NotEmpty和@Size两个注解,@NotEmpty表示属性不能为空,@Size表示限制属性内容的长短。
定义好校验规则还需要开启校验,在controller方法中添加@Validated注解,如下:
1 2 3 4 5 6 7 @ApiOperation("新增课程基础信息") @PostMapping("/course") public CourseBaseInfoDto createCourseBase (@RequestBody @Validated AddCourseDto addCourseDto) { Long companyId = 1L ; return courseBaseInfoService.createCourseBase(companyId,addCourseDto); }
如果校验出错Spring会抛出MethodArgumentNotValidException异常,我们需要在统一异常处理器中捕获异常,解析出异常信息。
代码 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 @ResponseBody @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public RestErrorResponse methodArgumentNotValidException (MethodArgumentNotValidException e) { BindingResult bindingResult = e.getBindingResult(); List<String> msgList = new ArrayList <>(); bindingResult.getFieldErrors().stream().forEach(item->msgList.add(item.getDefaultMessage())); String msg = StringUtils.join(msgList, "," ); log.error("【系统异常】{}" ,msg); return new RestErrorResponse (msg); }
重启内容管理服务。
使用httpclient进行测试,将必填项设置为空,“适用人群” 属性的内容设置1个字。
执行测试,接口响应结果如下:
1 2 3 { "errMessage" : "课程名称不能为空,课程分类不能为空,课程分类不能为空,适用人群内容过少" }
可以看到校验器生效。
还是很方便的,不然要自己写一大堆if判断
分组校验 有时候在同一个属性上设置一个校验规则不能满足要求,比如:订单编号由系统生成,在添加订单时要求订单编号为空,在更新 订单时要求订单编写不能为空。此时就用到了分组校验,同一个属性定义多个校验规则属于不同的分组,比如:添加订单定义@NULL规则属于insert分组,更新订单定义@NotEmpty规则属于update分组,insert和update是分组的名称,是可以修改的。
下边举例说明
我们用class类型来表示不同的分组,所以我们定义不同的接口类型(空接口)表示不同的分组,由于校验分组是公用的,所以定义在 base工程中。如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.zuoer.base.exception;public class ValidationGroups { public interface Inster {}; public interface Update {}; public interface Delete {}; }
下边在定义校验规则时指定分组:
1 2 3 4 5 6 Bash @NotEmpty(groups = {ValidationGroups.Inster.class},message = "添加课程名称不能为空") @NotEmpty(groups = {ValidationGroups.Update.class},message = "修改课程名称不能为空") @ApiModelProperty(value = "课程名称", required = true) private String name;
在Controller方法中启动校验规则指定要使用的分组名:
1 2 3 4 5 6 7 8 Bash @ApiOperation("新增课程基础信息") @PostMapping("/course") public CourseBaseInfoDto createCourseBase (@RequestBody @Validated({ValidationGroups.Inster.class}) AddCourseDto addCourseDto) { Long companyId = 1L ; return courseBaseInfoService.createCourseBase(companyId,addCourseDto); }
再次测试,由于这里指定了Insert分组,所以抛出 异常信息:添加课程名称不能为空。
如果修改分组为ValidationGroups.Update.class,异常信息为:修改课程名称不能为空。
校验规则不满足? 如果javax.validation.constraints包下的校验规则满足不了需求怎么办?
1、手写校验代码 。
2、自定义校验规则注解。
如何自定义校验规则注解,请自行查阅资料实现。
修改课程 需求分析 业务流程 1、进入课程列表查询
2、点击编辑
因为课程审核通过方可发布,任何时候都 可以编辑,下图是编辑课程的界面:
进入编辑界面显示出当前课程的信息。
3、修改成功自动进入课程计划编辑页面。
数据模型 修改课程的涉及到的数据表是课程基本信息表
课程营销信息表:
1、进入课程编辑界面
界面中显示了课程的当前信息,需要根据课程id查询课程基本和课程营销信息,显示在表单上。
2、编辑、提交
修改课程提交的数据比新增课程多了一项课程id,因为修改课程需要针对某个课程进行修改。
3、保存数据
编辑完成保存课程基础信息和课程营销信息。
更新课程基本信息表中的修改人、修改时间。
接口定义 查询课程信息 定义根据课程id查询课程信息接口。
接口示例如下:
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 GET /content/course/40 Content-Type: application/json #响应结果 #{ # "id" : 40 , # "companyId" : 1232141425 , # "companyName" : null , # "name" : "SpringBoot核心" , # "users" : "Spring Boot初学者" , # "tags" : "Spring项目的快速构建" , # "mt" : "1-3" , # "mtName" : null , # "st" : "1-3-2" , # "stName" : null , # "grade" : "200003" , # "teachmode" : "201001" , # "description" : "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。" , # "pic" : "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg" , # "createDate" : "2019-09-10 16:05:39" , # "changeDate" : "2022-09-09 07:27:48" , # "createPeople" : null , # "changePeople" : null , # "auditStatus" : "202004" , # "status" : "203001" , # "coursePubId" : 21 , # "coursePubDate" : null , # "charge" : "201001" , # "price" : 0.01 #}
查询结果为单条课程信息,内容和新增课程返回结果一致,所以采用与新增课程一致的模型类。
接口定义如下:
1 2 3 4 5 @ApiOperation("根据课程id查询课程基础信息") @GetMapping("/course/{courseId}") public CourseBaseInfoDto getCourseBaseById (@PathVariable Long courseId) { return null ; }
修改课程信息 根据前边的数据模型分析,修改课程提交的数据比新增多了课程id,接口示例如下:
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 ### 修改课程 PUT /content/course Content-Type: application/json { "id" : 40 , "companyName" : null , "name" : "SpringBoot核心" , "users" : "Spring Boot初学者" , "tags" : "Spring项目的快速构建" , "mt" : "1-3" , "st" : "1-3-2" , "grade" : "200003" , "teachmode" : "201001" , "description" : "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。" , "pic" : "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg" , "charge" : "201001" , "price" : 0.01 } ###修改成功响应结果如下 #{ # "id" : 40 , # "companyId" : 1232141425 , # "companyName" : null , # "name" : "SpringBoot核心" , # "users" : "Spring Boot初学者" , # "tags" : "Spring项目的快速构建" , # "mt" : "1-3" , # "mtName" : null , # "st" : "1-3-2" , # "stName" : null , # "grade" : "200003" , # "teachmode" : "201001" , # "description" : "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。" , # "pic" : "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg" , # "createDate" : "2019-09-10 16:05:39" , # "changeDate" : "2022-09-09 07:27:48" , # "createPeople" : null , # "changePeople" : null , # "auditStatus" : "202004" , # "status" : "203001" , # "coursePubId" : 21 , # "coursePubDate" : null , # "charge" : "201001" , # "price" : 0.01 #}
这里定义修改课程提交的数据模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.zuoer.content.model.dto;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.Data;@Data @ApiModel(value="EditCourseDto", description="修改课程基本信息") public class EditCourseDto extends AddCourseDto { @ApiModelProperty(value = "课程id", required = true) private Long id; }
修改后返回最新课程信息,采用与新增课程接口返回类型一致的数据模型。
接口定义如下:
1 2 3 4 5 @ApiOperation("修改课程基础信息") @PutMapping("/course") public CourseBaseInfoDto modifyCourseBase (@RequestBody @Validated EditCourseDto editCourseDto) { }
接口开发 查询课程信息 查询课程信息的Service方法在新增课程接口开发中已实现,无需实现,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public CourseBaseInfoDto getCourseBaseInfo (long courseId) { CourseBase courseBase = courseBaseMapper.selectById(courseId); if (courseBase == null ){ return null ; } CourseMarket courseMarket = courseMarketMapper.selectById(courseId); CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto (); BeanUtils.copyProperties(courseBase,courseBaseInfoDto); 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; }
需要将查询课程信息的方法提到接口上,这样在controller中通过接口调用此方法。
1 2 3 4 5 6 7 8 9 10 11 public interface CourseBaseInfoService { .... public CourseBaseInfoDto getCourseBaseInfo (long courseId) ; ...
完善接口层代码 :
1 2 3 4 5 @ApiOperation("根据课程id查询课程基础信息") @GetMapping("/course/{courseId}") public CourseBaseInfoDto getCourseBaseById (@PathVariable Long courseId) { return courseBaseInfoService.getCourseBaseInfo(courseId); }
测试查询课程
用httpclient测试查询课程接口:
1 2 ### 查询课程信息 GET /content/course/40
修改课程信息
修改Service修改课程的接口与方法:
1 2 3 4 5 6 7 public CourseBaseInfoDto updateCourseBase (Long companyId, EditCourseDto editCourseDto) ;
实现方法如下:
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 @Transactional @Override public CourseBaseInfoDto updateCourseBase (Long companyId, EditCourseDto dto) { Long courseId = dto.getId(); CourseBase courseBase = courseBaseMapper.selectById(courseId); if (courseBase==null ){ XueChengPlusException.cast("课程不存在" ); } if (!courseBase.getCompanyId().equals(companyId)){ XueChengPlusException.cast("本机构只能修改本机构的课程" ); } BeanUtils.copyProperties(dto,courseBase); courseBase.setChangeDate(LocalDateTime.now()); int i = courseBaseMapper.updateById(courseBase); CourseMarket courseMarket = new CourseMarket (); BeanUtils.copyProperties(dto,courseMarket); saveCourseMarket(courseMarket); CourseBaseInfoDto courseBaseInfo = this .getCourseBaseInfo(courseId); return courseBaseInfo; }
最后完善接口层代码:
1 2 3 4 5 6 7 @ApiOperation("修改课程基础信息") @PutMapping("/course") public CourseBaseInfoDto modifyCourseBase (@RequestBody @Validated EditCourseDto editCourseDto) { Long companyId = 1232141425L ; return courseBaseInfoService.updateCourseBase(companyId,editCourseDto); }
接口测试 接口开发完成进行测试,使用httpclient测试
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 ### 根据课程id查询课程信息 GET { { content_host} } /content/course/40 Content-Type: application/json #响应结果 #{ # "id" : 40 , # "companyId" : 1232141425 , # "companyName" : null , # "name" : "SpringBoot核心" , # "users" : "Spring Boot初学者" , # "tags" : "Spring项目的快速构建" , # "mt" : "1-3" , # "mtName" : null , # "st" : "1-3-2" , # "stName" : null , # "grade" : "200003" , # "teachmode" : "201001" , # "description" : "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。" , # "pic" : "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg" , # "createDate" : "2019-09-10 16:05:39" , # "changeDate" : "2022-09-09 07:27:48" , # "createPeople" : null , # "changePeople" : null , # "auditStatus" : "202004" , # "status" : "203001" , # "coursePubId" : 21 , # "coursePubDate" : null , # "charge" : "201001" , # "price" : 0.01 #} ### 修改课程 PUT { { content_host} } /content/course Content-Type: application/json { "id" : 40 , "name" : "SpringBoot核心" , "users" : "Spring Boot初学者" , "tags" : "Spring项目的快速构建" , "mt" : "1-3" , "st" : "1-3-2" , "grade" : "200003" , "teachmode" : "201001" , "description" : "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。" , "pic" : "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg" , "charge" : "201001" , "price" : 0.01 } ###修改成功响应结果如下 #{ # "id" : 40 , # "companyId" : 1232141425 , # "companyName" : null , # "name" : "SpringBoot核心" , # "users" : "Spring Boot初学者" , # "tags" : "Spring项目的快速构建" , # "mt" : "1-3" , # "mtName" : null , # "st" : "1-3-2" , # "stName" : null , # "grade" : "200003" , # "teachmode" : "201001" , # "description" : "课程系统性地深度探讨 Spring Boot 核心特性,引导小伙伴对 Java 规范的重视,启发对技术原理性的思考,掌握排查问题的技能,以及学习阅读源码的方法和技巧,全面提升研发能力,进军架构师队伍。" , # "pic" : "https://cdn.educba.com/academy/wp-content/uploads/2018/08/Spring-BOOT-Interview-questions.jpg" , # "createDate" : "2019-09-10 16:05:39" , # "changeDate" : "2022-09-09 07:27:48" , # "createPeople" : null , # "changePeople" : null , # "auditStatus" : "202004" , # "status" : "203001" , # "coursePubId" : 21 , # "coursePubDate" : null , # "charge" : "201001" , # "price" : 0.01 #}
前端开发完毕进行前后端接口联调。
过程略。
查询课程计划 需求分析 业务流程 课程基本信息添加或修改成功将自动进入课程计划编辑器界面,如下图:
课程计划即课程的大纲目录。
课程计划分为两级:大章节和小章节。
本小节完成课程计划信息的查询。
数据模型 从课程计划查询界面上可以看出整体上是 一个树型结构,课程计划表teachplan如下:
每个课程计划都有所属课程。
每个课程的课程计划有两个级别,第一级为大章节,grade为1、第二级为小章节,grade为2
3。第二级的parentid为第一级的id。
课程计划的显示顺序根据排序字段去显示。
根据业务流程中的界面原型,课程计划列表展示时还有课程计划关联的视频信息。
课程计划关联的视频信息在teachplan_media表,结构如下:
两张表是一对一关系,每个课程计划只能在teachplan_media表中存在一个视频。
从课程资料目录下的db目录中,从toddle_content.sql文件中找到课程计划表teachplan、teachplan_media表的建表语句以及数据初始化语句,并通过mysql客户端去执行脚本。
这里使用DataGrid 客户端工具连接mysql并执行脚本。
接口定义 接口示例如下:
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 GET /teachplan/22 /tree-nodes [ { "changeDate" : null , "courseId" : 74 , "cousePubId" : null , "createDate" : null , "endTime" : null , "grade" : "2" , "isPreview" : "0" , "mediaType" : null , "orderby" : 1 , "parentid" : 112 , "pname" : "第1章基础知识" , "startTime" : null , "status" : null , "id" : 113 , "teachPlanTreeNodes" : [ { "changeDate" : null , "courseId" : 74 , "cousePubId" : null , "createDate" : null , "endTime" : null , "grade" : "3" , "isPreview" : "1" , "mediaType" : "001002" , "orderby" : 1 , "parentid" : 113 , "pname" : "第1节项目概述" , "startTime" : null , "status" : null , "id" : 115 , "teachPlanTreeNodes" : null , "teachplanMedia" : { "courseId" : 74 , "coursePubId" : null , "mediaFilename" : "2.avi" , "mediaId" : 41 , "teachplanId" : 115 , "id" : null } } ] , "teachplanMedia" : null } , { "changeDate" : null , "courseId" : 74 , "cousePubId" : null , "createDate" : null , "endTime" : null , "grade" : "2" , "isPreview" : "0" , "mediaType" : "" , "orderby" : 1 , "parentid" : 112 , "pname" : "第2章快速入门" , "startTime" : null , "status" : null , "id" : 242 , "teachPlanTreeNodes" : [ { "changeDate" : null , "courseId" : 74 , "cousePubId" : null , "createDate" : null , "endTime" : null , "grade" : "3" , "isPreview" : "1" , "mediaType" : "001002" , "orderby" : 2 , "parentid" : 242 , "pname" : "第1节搭建环境" , "startTime" : null , "status" : null , "id" : 244 , "teachPlanTreeNodes" : null , "teachplanMedia" : { "courseId" : 74 , "coursePubId" : null , "mediaFilename" : "3.avi" , "mediaId" : 42 , "teachplanId" : 244 , "id" : null } } , { "changeDate" : null , "courseId" : 74 , "cousePubId" : null , "createDate" : null , "endTime" : null , "grade" : "3" , "isPreview" : "0" , "mediaType" : "001002" , "orderby" : 3 , "parentid" : 242 , "pname" : "第2节项目概述" , "startTime" : null , "status" : null , "id" : 245 , "teachPlanTreeNodes" : null , "teachplanMedia" : { "courseId" : 74 , "coursePubId" : null , "mediaFilename" : "1a.avi" , "mediaId" : 39 , "teachplanId" : 245 , "id" : null } } ] , "teachplanMedia" : null } ]
查询课程计划的请求参数:课程id
响应结果需要自定义模型类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.zuoer.content.model.dto;import com.zuoer.content.model.po.Teachplan;import com.zuoer.content.model.po.TeachplanMedia;import java.util.List;@Data public class TeachplanDto extends Teachplan { TeachplanMedia teachplanMedia; List<TeachplanDto> teachPlanTreeNodes; }
定义接口如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.zuoer.content.api;@Slf4j @RestController @Api(value = "课程计划编辑接口",tags = "课程计划编辑接口") public class TeachplanController { @Autowired private TeachplanService teachplanService; @ApiOperation("查询课程计划树形结构") @ApiImplicitParam(value = "courseId",name = "课程Id",required = true,dataType = "Long",paramType = "path") @GetMapping("/teachplan/{courseId}/tree-nodes") public List<TeachplanDto> getTreeNodes (@PathVariable Long courseId) { return null ; } }
接口开发 DAO开发 Mapper接口使用sql查询课程计划,组成一个树型结构。
在TeachplanMapper自定义方法:
1 2 3 4 5 6 7 8 9 public interface TeachplanMapper extends BaseMapper<Teachplan> { /** * 查询某课程的课程计划,组成树型结构 * @param courseId courseId * @return com.zuoer.content.model.dto.TeachplanDto */ public List<TeachplanDto> selectTreeNodes(Long courseId); }
定义mapper.xml中的sql语句,分析如下:
1、一级分类和二级分类通过teachplan表的自链接进行,如果只有一级分类其下边没有二级分类,此时也需要显示一级分类,这里使用左连接,左边是一级分类,右边是二级分类。
2、由于当还没有关联 视频时teachplan_media对应的记录为空,所以需要teachplan和teachplan_media左链接。
sql如下:
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 select one.id one_id, one.pname one_pname, one.parentid one_parentid, one.grade one_grade, one.media_type one_mediaType, one.start_time one_stratTime, one.end_time one_endTime, one.orderby one_orderby, one.course_id one_courseId, one.course_pub_id one_coursePubId, two.id two_id, two.pname two_pname, two.parentid two_parentid, two.grade two_grade, two.media_type two_mediaType, two.start_time two_stratTime, two.end_time two_endTime, two.orderby two_orderby, two.course_id two_courseId, two.course_pub_id two_coursePubId, m1.media_fileName mediaFilename, m1.id teachplanMeidaId, m1.media_id mediaId from teachplan one INNER JOIN teachplan two on one.id = two.parentid LEFT JOIN teachplan_media m1 on m1.teachplan_id = two.id where one.parentid = 0 and one.course_id= #{value } order by one.orderby, two.orderby
定义mapper.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 <resultMap id ="treeNodeResultMap" type ="com.zuoer.content.model.dto.TeachplanDto" > <id column ="one_id" property ="id" /> <result column ="one_pname" property ="pname" /> <result column ="one_parentid" property ="parentid" /> <result column ="one_grade" property ="grade" /> <result column ="one_mediaType" property ="mediaType" /> <result column ="one_stratTime" property ="startTime" /> <result column ="one_endTime" property ="endTime" /> <result column ="one_orderby" property ="orderby" /> <result column ="one_courseId" property ="courseId" /> <result column ="one_coursePubId" property ="coursePubId" /> <collection property ="teachPlanTreeNodes" ofType ="com.zuoer.content.model.dto.TeachplanDto" > <id column ="two_id" property ="id" /> <result column ="two_pname" property ="pname" /> <result column ="two_parentid" property ="parentid" /> <result column ="two_grade" property ="grade" /> <result column ="two_mediaType" property ="mediaType" /> <result column ="two_stratTime" property ="startTime" /> <result column ="two_endTime" property ="endTime" /> <result column ="two_orderby" property ="orderby" /> <result column ="two_courseId" property ="courseId" /> <result column ="two_coursePubId" property ="coursePubId" /> <association property ="teachplanMedia" javaType ="com.zuoer.content.model.po.TeachplanMedia" > <id column ="teachplanMeidaId" property ="id" /> <result column ="mediaFilename" property ="mediaFilename" /> <result column ="mediaId" property ="mediaId" /> <result column ="two_id" property ="teachplanId" /> <result column ="two_courseId" property ="courseId" /> <result column ="two_coursePubId" property ="coursePubId" /> </association > </collection > </resultMap > <select id ="selectTreeNodes" resultMap ="treeNodeResultMap" parameterType ="long" > select one.id one_id, one.pname one_pname, one.parentid one_parentid, one.grade one_grade, one.media_type one_mediaType, one.start_time one_stratTime, one.end_time one_endTime, one.orderby one_orderby, one.course_id one_courseId, one.course_pub_id one_coursePubId, two.id two_id, two.pname two_pname, two.parentid two_parentid, two.grade two_grade, two.media_type two_mediaType, two.start_time two_stratTime, two.end_time two_endTime, two.orderby two_orderby, two.course_id two_courseId, two.course_pub_id two_coursePubId, m1.media_fileName mediaFilename, m1.id teachplanMeidaId, m1.media_id mediaId from teachplan one INNER JOIN teachplan two on one.id = two.parentid LEFT JOIN teachplan_media m1 on m1.teachplan_id = two.id where one.parentid = 0 and one.course_id = #{value} order by one.orderby, two.orderby </select >
单元测试方法,略。
Service开发 定义service接口
1 2 3 4 5 6 7 8 9 public interface TeachplanService extends IService <Teachplan> { public List<TeachplanDto> findTeachplanTree (Long courseId) ; }
定义service接口实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 @Slf4j @Service public class TeachplanServiceImpl extends ServiceImpl <TeachplanMapper, Teachplan> implements TeachplanService { @Autowired TeachplanMapper teachplanMapper; @Override public List<TeachplanDto> findTeachplanTree (Long courseId) { return teachplanMapper.selectTreeNodes(courseId); } }
接口测试 1、完善接口层代码
1 2 3 4 5 6 7 8 9 @Autowired TeachplanService teachplanService; @ApiOperation("查询课程计划树形结构") @ApiImplicitParam(value = "courseId",name = "课程基础Id值",required = true,dataType = "Long",paramType = "path") @GetMapping("teachplan/{courseId}/tree-nodes") public List<TeachplanDto> getTreeNodes (@PathVariable Long courseId) { return teachplanService.findTeachplanTree(courseId); }
2、使用httpclient测试
找一个有课程计划的课程进行测试
1 2 3 ### 查询某个课程的课程计划 GET {{content_host}}/content/teachplan/74/tree-nodes
3、前后端联调
1)进入课程编辑页面
2)保存进入下一步
观察课程计划获取是否成功。
1)进入新增课程页面
2)新增课程成功,自动进入课程计划编辑界面。
由于是新增的课程,课程计划为空。
新增/修改计划 需求分析 业务流程 1、进入课程计划界面
2、点击“添加章”新增第一级课程计划。
新增成功自动刷新课程计划列表。
3、点击“添加小节”向某个第一级课程计划下添加小节。
新增成功自动刷新课程计划列表。
新增的课程计划自动排序到最后。
4、点击“章”、“节”的名称,可以修改名称、选择是否免费。
数据模型 1、新增第一级课程计划
名称默认为:新章名称 [点击修改]
grade:1
orderby: 所属课程中同级别下排在最后
2、新增第二级课程计划
名称默认为:新小节名称 [点击修改]
grade:2
orderby: 所属课程计划中排在最后
3、修改第一级、第二级课程计划的名称,修改第二级课程计划是否免费
接口定义 接口示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ### 新增课程计划--章, 当grade为1 时parentid为0 POST { { content_host} } /content/teachplan Content-Type: application/json { "courseId" : 74 , "parentid" : 0 , "grade" : 1 , "pname" : "新章名称 [点击修改]" } ### 新增课程计划--节 POST { { content_host} } /content/teachplan Content-Type: application/json { "courseId" : 74 , "parentid" : 247 , "grade" : 2 , "pname" : "小节名称 [点击修改]" }
同一个接口接收新增和修改两个业务请求,以是否传递课程计划id 来判断是新增还是修改。
如果传递了课程计划id说明当前是要修改该课程计划,否则是新增一个课程计划。
定义接收请求参数的数据模型类:
定义SaveTeachplanDto
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 package com.zuoer.content.model.dto;import lombok.Data;@Data public class SaveTeachplanDto { private Long id; private String pname; private Long parentid; private Integer grade; private String mediaType; private Long courseId; private Long coursePubId; private String isPreview; }
定义接口如下:
1 2 3 4 5 @ApiOperation("课程计划创建或修改") @PostMapping("/teachplan") public void saveTeachplan ( @RequestBody SaveTeachplanDto teachplan) {}
接口开发 Mapper开发 根据业务的分析,Mapper使用自动生成的mapper即可满足要求。
Service开发 定义保存课程计划的Service接口。
1 2 3 4 5 public void saveTeachplan (SaveTeachplanDto teachplanDto) ;
编写接口实现:
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 @Transactional @Override public void saveTeachplan (SaveTeachplanDto teachplanDto) { Long id = teachplanDto.getId(); if (id == null ) { Teachplan teachplan = new Teachplan (); BeanUtils.copyProperties(teachplanDto,teachplan); Long parentid = teachplanDto.getParentid(); Long courseId = teachplanDto.getCourseId(); Integer count = getTeachplanCount(parentid, courseId); teachplan.setOrderby(count); teachplanMapper.insert(teachplan); } else { Teachplan teachplan = teachplanMapper.selectById(id); BeanUtils.copyProperties(teachplanDto,teachplan); teachplanMapper.updateById(teachplan); } } private Integer getTeachplanCount (Long parentid, Long courseId) { LambdaQueryWrapper<Teachplan> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(Teachplan::getCourseId,courseId).eq(Teachplan::getParentid,parentid); Integer count = teachplanMapper.selectCount(queryWrapper); return count+1 ; }
接口测试 1、完善接口的代码 ,调用service方法完成课程计划的创建和修改。
1 2 3 4 5 @ApiOperation("课程计划创建或修改") @PostMapping("/teachplan") public void saveTeachplan( @RequestBody SaveTeachplanDto teachplan){ teachplanService.saveTeachplan(teachplan); }
2、首先使用httpclient做以下测试。
添加章
1 2 3 4 5 6 7 8 9 10 ### 新增课程计划--章 POST { { content_host} } /content/teachplan Content-Type: application/json { "courseId" : 74 , "parentid" : 0 , "grade" : 1 , "pname" : "新章名称 [点击修改]" }
2、添加小节
1 2 3 4 5 6 7 8 9 10 ### 新增课程计划--节, 从数据库找到第一级的课程计划id向其下边添加计划 POST { { content_host} } /content/teachplan Content-Type: application/json { "courseId" : 74 , "parentid" : 247 , "grade" : 2 , "pname" : "小节名称 [点击修改]" }
3、保存课程计划
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ### 课程课程计划, 需要从数据库找到修改的课程计划id POST { { content_host} } /content/teachplan Content-Type: application/json { "changeDate" : null , "courseId" : 22 , "cousePubId" : null , "createDate" : null , "ctlBarShow" : false , "ctlEditTitle" : true , "endTime" : null , "grade" : "2" , "isPreview" : "1" , "mediaType" : "" , "orderby" : 1 , "parentid" : 237 , "pname" : "第1节修改名称" , "startTime" : null , "status" : null , "teachPlanId" : 240 }
4、前后端联调
分别联调新增章、新增小节、保存计划信息。
Bug修改 通过接口测试我们发现:
1、使用httpclient测试没有问题
2、前后端联调时发现新增的第一级目录不能显示在列表中。
请自己分析并修复。
1 查询树形结构时的inner join需要改为left join
3、思考添加课程计划的实现方式是否存在bug?
如有bug进行修改。
项目实战 实战环境 实战流程 项目实战是模拟企业实际开发的场景,自己参考文档独立完成开发任务,项目实战可以有效的培养自己面对需求进行分析与开发的能力。
实战流程如下:
1、由组长将实战的初始代码提交至本组git仓库。
2、每位成员从此仓库clone项目。
3、小组共同讨论实战功能需求及接口。
4、根据自己小组的情况进行分工,每人至少写一个接口并测试通过、提交至仓库。
注意:每人在开发接口时创建自己的service、controller接口和类,不要出现多人共用同一个文件的情况。
5、待功能开发完毕小组成员拉下全部代码,进行交叉测试,测试出来的bug信息记录在word文档上提交给组长由组长汇总。
6、根据bug记录进行修复自己接口中的bug,修复完成并测试没有问题后提交给Git。
7、整体流程测试,包括如下:
1)从网上找一门详细的课程信息(包括课程大纲) 添加到系统中。
功能包括:添加课程、添加课程计划、添加师资信息。
2)演示修改课程、修改课程计划及修改师资信息功能。
3)演示课程计划上移、下移功能。
4)演示删除课程计划、删除师资、删除课程功能。
8、项目评比
小组推荐一名成员作工作汇总,老师根据团队协作情况、功能完成情况、演讲能力进行打分(满分10分)。
创建Git远程仓库 因为组员无法访问组长虚拟机中的gogs所以由组长在自己的电脑上安装gogs,这里提供windwos版本安装包,如果安装有问题也可以使用公网的git仓库,比如码云。
组长解压 软件工具目录下的gogs_0.12.10_windows_amd64.zip,安装gogs
解压后cmd进入gogs安装目录,输入gogs.exe web
自动打开安装界面:
第一步填写数据库信息
输入虚拟机中的数据库地址和账号、密码,数据库名称为gogs_windows,需要提前在数据库中创建gogs_windows数据库
第二步应用基本设置:
仓库目录可以设置在gogs的安装目录下
域名为虚拟域名,组长和组员在自己的hosts文件中配置该域名及对应的组长电脑的IP地址。
下边配置日志路径 ,日志路径可以设置在gogs的安装目录下
下边配置管理员账号和密码:gogs/gogs
输入完毕点击立即安装
安装完毕自动跳转到 http://group1.xuecheng.com:3000/
组长使用管理员账号密码登录,登录后参考 学成在线项目开发环境配置文档去创建组织及仓库,仓库的代码是老师下发的实战基础代码。
组长为组员创建git账号,创建完成将账号和密码发给每位组员。
组员需要记住自己的git账号和密码,
拉取代码 组员拉取本组git远程仓库的代码,根据任务分工开始协作开发。
例如:本组的git仓库地址为http://group1.xuecheng.com:3000/xuecheng-plus-group01/xuecheng-plus-project.git
本组全体成员拉取此仓库的代码,根据任务分工开始协作开发。
删除课程计划 需求分析 课程计划添加成功,如果课程还没有提交时可以删除课程计划。
删除第一级别的大章节时要求大章节下边没有小章节时方可删除。
删除第二级别的小章节的同时需要将teachplan_media表关联的信息也删除。
接口定义 删除课程计划的接口定义:
传入课程计划id进行删除操作。
1 2 3 4 5 6 7 Request URL: /content/teachplan/246 Request Method: DELETE 如果失败返回: {"errCode":"120409","errMessage":"课程计划信息还有子级信息,无法操作"} 如果成功:状态码200,不返回信息
api接口
1 2 3 4 5 6 @ApiOperation("删除课程计划") @ApiImplicitParam(value = "id",name = "课程计划Id",required = true,dataType = "Long",paramType = "path") @DeleteMapping("/teachplan/{id}") public void deleteTeachplan (@PathVariable Long id) { teachplanService.removeTeachplan(id); }
service接口
1 2 3 4 5 void removeTeachplan (Long id) ;
service实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override public void removeTeachplan (Long id) { Teachplan teachplan = teachplanMapper.selectById(id); Integer childCount = getTeachplanChildCount(id,teachplan.getCourseId()); if (childCount > 0 ){ ToddleException.cast("大章节下面存在小节信息,无法删除" ); } teachplanMapper.deleteById(id); removeTeachMedia(id,teachplan.getCourseId()); } private void removeTeachMedia (Long id, Long courseId) { LambdaQueryWrapper<TeachplanMedia> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(TeachplanMedia::getTeachplanId,id).eq(TeachplanMedia::getCourseId,courseId); teachplanMediaMapper.delete(queryWrapper); }
接口测试 首先使用httpclient工具进行测试
1 2 ### 删除课程计划 DELETE {{content_host}}/content/teachplan/43
分以下情况测试:
1、删除大章节,大章节下有小章节时不允许删除。
2、删除大章节,大单节下没有小章节时可以正常删除。
3、删除小章节,同时将关联的信息进行删除。
课程计划排序 需求分析 课程计划新增后默认排在同级别最后,课程计划排序功能是可以灵活调整课程计划的显示顺序,如下图:
上移表示将课程计划向上移动。
下移表示将课程计划向下移动。
向上移动后和上边同级的课程计划交换位置,可以将两个课程计划的排序字段值进行交换。
向下移动后和下边同级的课程计划交换位置,可以将两个课程计划的排序字段值进行交换。
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 @Transactional @Override public void movedownTeachplan (Long id) { Teachplan teachplan = teachplanMapper.selectById(id); Teachplan after = teachplanMapper.getAfter(teachplan); if (after == null ) { ToddleException.cast("已为底部,无需下移" ); } Integer currOrder = teachplan.getOrderby(); Integer afterOrder = after.getOrderby(); teachplan.setOrderby(afterOrder); teachplanMapper.updateById(teachplan); after.setOrderby(currOrder); teachplanMapper.updateById(after); } @Transactional @Override public void moveupTeachplan (Long id) { Teachplan teachplan = teachplanMapper.selectById(id); Teachplan before = teachplanMapper.getBefore(teachplan); if (before == null ) { ToddleException.cast("已为顶部,无需上移" ); } Integer currOrder = teachplan.getOrderby(); Integer beforeOrder = before.getOrderby(); teachplan.setOrderby(beforeOrder); teachplanMapper.updateById(teachplan); before.setOrderby(currOrder); teachplanMapper.updateById(before); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 < select id= "getBefore" resultType= "com.zuoer.content.model.po.Teachplan" parameterType= "com.zuoer.content.model.po.Teachplan"> select * from teachplan where parentid = #{parentid} AND course_id = #{courseId} AND #{orderby} > orderby ORDER BY orderby DESC LIMIT 0 ,1 < / select > < select id= "getAfter" resultType= "com.zuoer.content.model.po.Teachplan" parameterType= "com.zuoer.content.model.po.Teachplan"> select * from teachplan where parentid = #{parentid} AND course_id = #{courseId} AND orderby > #{orderby} ORDER BY orderby LIMIT 0 ,1 < / select >
接口定义 接口示例如下:
向下移动:
1 2 Request URL: http://localhost:8601/api/content/teachplan/movedown/43 Request Method: POST
参数1:movedown 为 移动类型,表示向下移动 参数2:43为课程计划id
向上移动:
1 2 Request URL: http://localhost:8601/api/content/teachplan/moveup/43 Request Method: POST
参数1:moveup 为 移动类型,表示向上移动 参数2:43为课程计划id
每次移动传递两个参数:
1、移动类型: movedown和moveup
2、课程计划id
接口测试 该功能可直接进行前后端联调,可以立即看到 效果。
1、向上移动测试
先找一个上边有课程计划的进行测试,向上移动后两个交换顺序。
再找最上边的课程计划向上移动,操作后位置不变因为已经在最上边了。
2、向下移动测试
先找一个下边有课程计划的进行测试,向下移动后两个交换顺序。
再找最下边的课程计划向下移动,操作后位置不变因为已经在最下边了。
师资管理 需求分析 在课程计划维护界面点击下一步进入师资管理界面:
点击添加教师打开添加界面,如下图,不用实现上传照片。
添加成功查询教师信息如下:
在这个界面可以删除老师,也可以点击编辑,修改教师信息:
注意:
只允许向机构自己的课程中添加老师、删除老师。
机构id统一使用:1232141425L
接口定义 1、查询教师接口请求示例
1 2 3 4 5 get /courseTeacher/list/75 75为课程id,请求参数为课程id 响应结果 [{"id":23,"courseId":75,"teacherName":"张老师","position":"讲师","introduction":"张老师教师简介张老师教师简介张老师教师简介张老师教师简介","photograph":null,"createDate":null}]
2、添加教师请求示例
1 2 3 4 5 6 7 8 9 10 11 post /courseTeacher 请求参数: { "courseId": 75, "teacherName": "王老师", "position": "教师职位", "introduction": "教师简介" } 响应结果: {"id":24,"courseId":75,"teacherName":"王老师","position":"教师职位","introduction":"教师简介","photograph":null,"createDate":null}
3、修改教师
1 2 3 4 5 6 7 8 9 10 11 12 13 put /courseTeacher 请求参数: { "id": 24, "courseId": 75, "teacherName": "王老师", "position": "教师职位", "introduction": "教师简介", "photograph": null, "createDate": null } 响应: {"id":24,"courseId":75,"teacherName":"王老师","position":"教师职位","introduction":"教师简介","photograph":null,"createDate":null}
4、删除教师
1 2 3 4 5 6 7 delete /ourseTeacher/course/75/26 75:课程id 26:教师id,即course_teacher表的主键 请求参数:课程id、教师id 响应:状态码200,不返回信息
接口测试 1、添加教师
2、查询教师
3、修改教师
4、删除教师
删除课程 需求分析 课程的审核状态为未提交时方可删除。
删除课程需要删除课程相关的基本信息、营销信息、课程计划、课程教师信息。
接口定义 删除课程接口
1 2 3 4 delete /course/87 87为课程id 请求参数:课程id 响应:状态码200,不返回信息
接口测试 找到一门课程进行删除,删除后从数据库确认课程基本信息、课程营销信息、课程计划、课程计划关联信息、课程师资是否删除成功。