企业级微服务大项目实战《学成在线》【二】(课程相关接口)
下面正式开始开发!
对了我的笔记肯定不会把全部代码都打上去,我会挑一些技术点进行阐述。
补充下为啥要叫DTO,PO啥的:
DTO:前端给后端传递的数据
VO:后端给前端传递的数据
DO:数据库表结构
PO:数据库表结构到JAVA的映射类
课程信息
查询
开发习惯从底层开始,所以就从DAO层(mapper层)开始写,再写service。
先在content-service写个测试类,配置和包看黑马的去。介绍下以前学过的分页查询插件courseBaseMapper,实质上就是在sql语句上加上limit等语句,可以看下测试类的代码:
@SpringBootTest
public class CourseBaseMapperTests {
@Autowired
CourseBaseMapper courseBaseMapper;
@Test
public void testCourse(){
CourseBase courseBase = courseBaseMapper.selectById(18);
Assertions.assertNotNull(courseBase);
}
}
ctrl点击selectById就可以看到这个插件的底层实现原理。然后是分页查询的全部代码:
@Test
public void testCourse(){
CourseBase courseBase = courseBaseMapper.selectById(18);
Assertions.assertNotNull(courseBase);
//分页查询
PageParams pageParams =new PageParams();
pageParams.setPageNo(1L);
pageParams.setPageSize(10L);
QueryCourseParamsDto courseParamsDto =new QueryCourseParamsDto();
courseParamsDto.setCourseName("java");
LambdaQueryWrapper<CourseBase> queryWrapper = new LambdaQueryWrapper<>();
//在sql中拼接course_base.name like '%值%'
queryWrapper.like(StringUtils.isNotEmpty(courseParamsDto.getCourseName()),CourseBase::getName,courseParamsDto.getCourseName());
//根据课程审核状态查询
queryWrapper.eq(StringUtils.isNotEmpty(courseParamsDto.getAuditStatus()),CourseBase::getAuditStatus,courseParamsDto.getAuditStatus());
Page<CourseBase> page = new Page<>(1,10);
Page<CourseBase> pageResult = courseBaseMapper.selectPage(page, queryWrapper);
PageResult<CourseBase> tPageResult = new PageResult<>(pageResult.getRecords(),pageResult.getTotal(),pageParams.getPageNo(),pageParams.getPageSize());
System.out.println(tPageResult);
}
运行出来拿到断点有结果就成了:
大概流程就是利用LambdaQueryWrapper那个插件然后一直利用参数查询所需要的结果,一步一步装填数据,最终以PageResult的形式返回就行。
然后说一下根据课程状态进行查询,在实际开发中一般会以代码来代替课程状态的中文,也是为了方便修改和规范,所以有个xc2.0_system的dictionary的数据库表就拿来表示对应关系:
直接在刚刚的代码下加一个条件:courseParamsDto.setAuditStatus("202004");
接下来就来测试接口,在默认文件里api模块下有个Controller类里面接口是写好的,然后就可以运行ContentApplication的springboot启动项,然后浏览器输入localhost:63040/content/swagger-ui.html,然后测试一下有数据就成功。还有个HttpClient插件接口测试,可以去下载搜一下,postman也可以。像我的就比较推荐httpclient直接就在idea里测试:
它还可以设置环境,在api-test下创建一个env.json文件,里面编写:
{
"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"
}
}
然后再在http里将localhost:63040换成{{content-host}}
前后端联调
解压打开前端文件这里用idea打开,然后去设置里配置npm和node的位置,打开根目录下的package.json ,运行serve的(idea它有),或者右键package.json选择查看npm脚本找到serve运行。对了如果运行失败,大概率是你的node版本与该项目的node文件版本不一致,两种办法,一是卸载项目里的node_moudels,再cnpm install安装自己版本的node文件,二是你自己更换node版本,这里是16.17.0,但是我用了第一种方法发现运行是没问题,但是一直报错,可能版本不同有些语法也不同吧哈哈哈,还是用第二种吧。
这里推荐另一个插件nvm,可以管理node的版本:windows下node更换版本(简单操作)_node版本更改-CSDN博客,有了它想切换哪个版本就切换。推荐我这样做,算是第三种方法吧qwq
将资料的system文件导入java项目中,复制api的resourse到service并修改下数据库的连接地址。
解决跨域问题
在浏览器通过http://localhost:8601/地址访问前端工程。
chrome浏览器报错如下:
Access to XMLHttpRequest at 'http://localhost:63110/system/dictionary/all' from origin 'http://localhost:8601' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. |
firefox浏览器报错如下:
已拦截跨源请求:同源策略禁止读取位于 http://localhost:63110/system/dictionary/all 的远程资源。(原因:CORS 头缺少 'Access-Control-Allow-Origin')。状态码:200。 |
提示:从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,表示这个请求来源哪里。
比如:
GET / HTTP/1.1 |
服务器收到请求判断这个Origin是否允许跨域,如果允许则在响应头中说明允许该来源的跨域请求,如下:
Access-Control-Allow-Origin:http://localhost:8601 |
如果允许任何域名来源的跨域请求,则响应如下:
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,
或直接从课程资料/项目工程下拷贝,
代码如下:
package com.xuecheng.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* @description 跨域过虑器
* @author Mr.M
* @date 2022/9/7 11:04
* @version 1.0
*/
@Configuration
public class GlobalCorsConfig {
/**
* 允许跨域调用的过滤器
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//允许白名单域名进行跨域调用
config.addAllowedOrigin("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
此配置类实现了跨域过虑器,在响应头添加Access-Control-Allow-Origin。
重启系统管理服务,前端工程可以正常进入http://localhost:8601,观察浏览器记录,成功解决跨域。
课程分类查询
先来看看树状查询的sql语句例子:
select one.id one_id,
one.label one_lable,
two.id two_id,
two.label two_lable
from course_category one
inner join course_category two on two.parentid = one.id
where one.parentid ='1'
树状数据结构的样子就是数据结构里的树:parentid就是它的父节点
为什么选择在mysql中递归而不在java中递归:因为Java每递归一次就要连接一次数据库,性能不好。
还是根据接口文档给的json参数来写接口,不然传参都不知道传啥。
这是mapper的sql语句:
<select id="selectTreeNodes" resultType="com.xuecheng.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 t1.id = t.parentid
)
select * from t1 order by t1.id, t1.orderby
</select>
主要就是写写sql,这里比较难,其它的都是那三层架构。,然后是最难的service将查的数据整理并返回:
@Service
@Slf4j
public class CourseCategoryServiceImpl implements CourseCategoryService {
@Autowired
CourseCategoryMapper courseCategoryMapper;
@Override
public List<CourseCategoryTreeDto> queryTreeNodes(String id) {
// 递归查询分类信息
List<CourseCategoryTreeDto> treeNodes = courseCategoryMapper.selectTreeNodes(id);
//将list转map,以备使用,排除根节点
Map<String, CourseCategoryTreeDto> mapTemp = treeNodes.stream().filter(item->!id.equals(item.getId())).collect(Collectors.toMap(key -> key.getId(), value -> value, (key1, key2) -> key2));
//最终返回的list
List<CourseCategoryTreeDto> categoryTreeDtos = new ArrayList<>();
//依次遍历每个元素,排除根节点
treeNodes.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>());
}
//下边开始往ChildrenTreeNodes属性中放子节点
courseCategoryTreeDto.getChildrenTreeNodes().add(item);
}
});
return categoryTreeDtos;
}
逻辑性比较强,大概流程就是遍历每个节点,如果有父节点则添加到父节点的list去,有一步的if是判断它的子节点是否为空,为空则创建一个该节点的孩子节点的list,因为默认childrenNodes为null,比如1-1走的流程就是先走过滤器,然后添加到1的子节点上,1-1-1则是看到它的父节点1-1的childrennodes为空先创建一个list,然后再添加到该list下。
新增课程
来看看service层的接口吧:
/**
* 添加课程
* @param companyId
* @param dto
* @return
*/
@Override
@Transactional
public CourseBaseInfoDto createCourseBase(Long companyId,AddCourseDto dto) {
/*1.参数合法性校验*/
//合法性校验
if (StringUtils.isEmpty(dto.getName())) {
throw new RuntimeException("课程名称为空");
}
if (StringUtils.isEmpty(dto.getMt())) {
throw new RuntimeException("课程分类为空");
}
if (StringUtils.isEmpty(dto.getSt())) {
throw new RuntimeException("课程分类为空");
}
if (StringUtils.isEmpty(dto.getGrade())) {
throw new RuntimeException("课程等级为空");
}
if (StringUtils.isEmpty(dto.getTeachmode())) {
throw new RuntimeException("教育模式为空");
}
if (StringUtils.isEmpty(dto.getUsers())) {
throw new RuntimeException("适应人群为空");
}
if (StringUtils.isEmpty(dto.getCharge())) {
throw new RuntimeException("收费规则为空");
}
/*2.数据封装调用mapper持久化数据*/
CourseBase insertCourseBase = new CourseBase();
//数据拷贝
BeanUtils.copyProperties(dto,insertCourseBase);
//设置默认审核状态
insertCourseBase.setAuditStatus("202002");
//设置默认发布状态
insertCourseBase.setStatus("203001");
//机构id
insertCourseBase.setCompanyId(companyId);
//添加时间
insertCourseBase.setCreateDate(LocalDateTime.now());
//插入课程基本信息表
int insert = courseBaseMapper.insert(insertCourseBase);
if(insert<=0){
throw new RuntimeException("新增课程基本信息失败");
}
//todo:向课程营销表保存课程营销信息course_market
CourseMarket courseMarket =new CourseMarket();
//将页面输入的数据拷贝到courseMarket
BeanUtils.copyProperties(dto,courseMarket);
//课程id
Long courseId = insertCourseBase.getId();
courseMarket.setId(courseId);
//保存营销信息
saveCourseMarket(courseMarket);
//todo:查询课程基本信息及营销信息并返回
//从数据库查出来
CourseBaseInfoDto courseBaseInfo = getCourseBaseInfo(courseId);
return courseBaseInfo;
}
//单独写一个方法保存营销信息
public int saveCourseMarket(CourseMarket courseMarket){
//参数的合法性校验
String charge = courseMarket.getCharge();
if (StringUtils.isEmpty(charge)){
throw new RuntimeException("收费规则不能为空");
}
if("201001".equals(charge)) //如果为收费课程,扣费规则不能为空
if(courseMarket.getPrice() == null || courseMarket.getPrice().floatValue() <= 0){
throw new RuntimeException("收费课程价格为空");
}
Long id =courseMarket.getId();
CourseMarket courseMarket1 =courseMarketMapper.selectById(id);
if (courseMarket1 == null){
//插入数据库
courseMarketMapper.insert(courseMarket);
}else {
BeanUtils.copyProperties(courseMarket,courseMarket1);
courseMarket1.setId(courseMarket.getId());
//更新
courseMarketMapper.updateById(courseMarket1);
}
return 1;
}
public CourseBaseInfoDto getCourseBaseInfo(long courseId){
/*查询课程*/
CourseBase courseBase = courseBaseMapper.selectById(courseId);
if(courseBase==null)
return null;
/*查询课程营销信息*/
CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
/*创建返回的dto*/
CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
BeanUtils.copyProperties(courseBase,courseBaseInfoDto);
if(courseMarket!=null)
BeanUtils.copyProperties(courseMarket,courseBaseInfoDto);
CourseCategory courseCategoryMt = courseCategoryMapper.selectById(courseBaseInfoDto.getMt());
CourseCategory courseCategorySt = courseCategoryMapper.selectById(courseBaseInfoDto.getSt());
courseBaseInfoDto.setMtName(courseCategoryMt.getName());
courseBaseInfoDto.setStName(courseCategorySt.getName());
return courseBaseInfoDto;
}
}
步骤就是先排除有没有必填信息没填,然后再是添加默认信息,以及营销表的检验和插入,最后将插入的课程的基本信息和营销信息并返回,测试参数(记得设置运行环境~):
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": "斤斤计较急急急急急急急急急急急急急急急",
"pic": "",
"teachmode": "200002",
"users": "初级人员",
"tags": "",
"grade": "204001",
"description": "怕怕怕怕怕怕怕怕怕怕怕怕怕怕",
"objectives": ""
}
调试成功的截图:
异常处理
全局异常处理器
从 Spring 3.0 - Spring 3.2 版本之间,对 Spring 架构和 SpringMVC 的Controller 的异常捕获提供了相应的异常处理。
-
@ExceptionHandler
Spring3.0提供的标识在方法上或类上的注解,用来表明方法的处理异常类型。
-
@ControllerAdvice
Spring3.2提供的新注解,从名字上可以看出大体意思是控制器增强, 在项目中来增强SpringMVC中的Controller。通常和**
@ExceptionHandler
** 结合使用,来处理SpringMVC的异常信息。 -
@ResponseStatus
Spring3.0提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。 调用处理程序方法时,状态代码将应用于HTTP响应。
通过上面的两个注解便可实现微服务端全局异常处理,具体代码如下:
package com.xuecheng.base.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
/**
* @author woldier
* @version 1.0
* @description 全局异常处理器
* @date 2023/3/6 15:18
**/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* @description XueChengPlusException.class 异常处理,提取异常信息并且返回
* @param e
* @return com.xuecheng.base.exception.RestErrorResponse
* @author: woldier
* @date: 2023/3/6 15:22
*/
@ExceptionHandler(XueChengPlusException.class)
@ResponseBody
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "业务模块抛出异常")
public RestErrorResponse doXueChengPlusException(XueChengPlusException e){
return new RestErrorResponse(e.getErrMessage());
}
/**
* @description Exception 异常处理,此异常为未知异常
* @param e
* @return com.xuecheng.base.exception.RestErrorResponse
* @author: woldier
* @date: 2023/3/6 15:27
*/
@ExceptionHandler(Exception.class)
@ResponseBody
@ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR, reason = "服务器未知异常")
public RestErrorResponse doXueChengPlusException(Exception e){
log.error(e.getMessage());
e.printStackTrace();
return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());
}
}
修改课程
介绍下EditCourseDto这个Dto:
/**
* @author woldier
* @version 1.0
* @description 修改课程基本信息所要使用的请求参数dto
* @date 2023/3/6 19:33
**/
@Data
@ApiModel(value = "EditCourseDto", description = "修改课程基本信息")
public class EditCourseDto extends AddCourseDto{
@NotNull(groups = {ValidationGroups.Update.class},message = "修改课程时id不能为空")
@ApiModelProperty(value = "课程名称", required = true)
private Long id;
}
继承了AddCourseDto的所有信息并且加了个id属性,大概意思就是修改课程时它的属性跟添加课程时的属性一样不能为空,并且加了个修改课程的id。
service层:
@Override
@Transactional
public CourseBaseInfoDto updateCourseBase(Long companyId, EditCourseDto editCourseDto) throws XueChengPlusException {
//先查数据库
Long id =editCourseDto.getId();
CourseBase courseBase =courseBaseMapper.selectById(id);
//为空抛异常
if (courseBase==null){
XueChengPlusException.cast("课程不存在");
}
//检查组织人id是否一样
if(!courseBase.getCompanyId().equals(companyId))
XueChengPlusException.cast("companyID不一致");
/*更新courseBase信息*/
/*拷贝*/
BeanUtils.copyProperties(editCourseDto,courseBase);
courseBase.setChangeDate(LocalDateTime.now());
int i =courseBaseMapper.updateById(courseBase);
if (i<=0){
XueChengPlusException.cast("更新失败");
}
/*更新营销信息*/
// CourseMarket courseMarket =new CourseMarket();
// BeanUtils.copyProperties(editCourseDto,courseMarket);
// courseMarket.setOriginalPrice(editCourseDto.getOriginalPrice().floatValue());
// courseMarket.setPrice(editCourseDto.getPrice().floatValue());
// checkCharge(courseMarket.getPrice(),editCourseDto.getCharge());
// courseMarketService.saveOrUpdate(courseMarket);
/*查询封装返回*/
return getCourseBaseInfo(editCourseDto.getId());
}
很好理解,就是检验companyId和一些属性后再修改。
课程计划
查询
对于录播课程,可以先看看播放列表,它有层级关系,第几章第几节啥的,还有对应视频或文档的信息。
可以运行下下面的sql查看对应关系就知道大概关系了:
one.pname one_pname,
two.id two_id,
two.pname two_pname,
tm.media_fileName,
tm.media_id
from teachplan one
inner join teachplan two on two.parentid=one.id
left join teachplan_media tm on two.id = tm.teachplan_id
where one.parentid=0 and one.course_id=117
order by one.id, two.id;
<!-- 课程分类树型结构查询映射结果 -->
<resultMap id="treeNodeResultMap" type="com.xuecheng.content.model.dto.TeachplanDto">
<!-- 一级数据映射 -->
<id column="one_id" property="id"/>
<result column="one_pname" property="pname"/>
<result column="one_parentid" property="parentid"/>
<result column="one_grade" property="grade"/>
<result column="one_mediaType" property="mediaType"/>
<result column="one_stratTime" property="stratTime"/>
<result column="one_endTime" property="endTime"/>
<result column="one_orderby" property="orderby"/>
<result column="one_courseId" property="courseId"/>
<result column="one_coursePubId" property="coursePubId"/>
<!-- 一级中包含多个二级数据 -->
<collection property="teachPlanTreeNodes" ofType="com.xuecheng.content.model.dto.TeachplanDto">
<!-- 二级数据映射 -->
<id column="two_id" property="id"/>
<result column="two_pname" property="pname"/>
<result column="two_parentid" property="parentid"/>
<result column="two_grade" property="grade"/>
<result column="two_mediaType" property="mediaType"/>
<result column="two_stratTime" property="stratTime"/>
<result column="two_endTime" property="endTime"/>
<result column="two_orderby" property="orderby"/>
<result column="two_courseId" property="courseId"/>
<result column="two_coursePubId" property="coursePubId"/>
<!-- 每一个二级课程计划对应一个媒资信息 -->
<association property="teachplanMedia" javaType="com.xuecheng.content.model.po.TeachplanMedia">
<result column="teachplanMeidaId" property="id"/>
<result column="mediaFilename" property="mediaFilename"/>
<result column="mediaId" property="mediaId"/>
<result column="two_id" property="teachplanId"/>
<result column="two_courseId" property="courseId"/>
<result column="two_coursePubId" property="coursePubId"/>
</association>
</collection>
</resultMap>
<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_startTime,
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
LEFT JOIN teachplan two ON one.id = two.parentid
LEFT JOIN teachplan_media m1 on two.id = m1.teachplan_id
WHERE one.grade="1" AND one.course_id = #{courseId}
ORDER BY one.orderby, two.orderby
</select>
新增/修改
补充下关于LambdaQueryWrapper的用法:MyBatis-Plus LambdaQueryWrapper使用说明_mybatisplus lambdaquerywrapper-CSDN博客
数据模型
1、新增第一级课程计划
名称默认为:新章名称 [点击修改]
grade:1
orderby: 所属课程中同级别下排在最后
2、新增第二级课程计划
名称默认为:新小节名称 [点击修改]
grade:2
orderby: 所属课程计划中排在最后
3、修改第一级、第二级课程计划的名称,修改第二级课程计划是否免费
service:自己看吧何平安懒得写了,有注释的看得懂
/**
* 添加更新课程计划
* @param dto
*/
@Override
@Transactional
public void saveOrUpdateTeachPlan(SaveTeachplanDto dto) {
//判断是更新或者添加节点
Long teachplanId =dto.getId();
if (teachplanId == null){
//新增
TeachplanDto teachplanDto = new TeachplanDto();
BeanUtils.copyProperties(dto,teachplanDto);
//确定排序字段,找到它的同级节点个数,排序字段个数加1 select count(1) from teachplan where course_id =17 and parentid = 268
Long parentid = dto.getParentid();
Long courseid = dto.getCourseId();
LambdaQueryWrapper<Teachplan> teachplanLambdaQueryWrapper = new LambdaQueryWrapper<>();
LambdaQueryWrapper<Teachplan> eq = teachplanLambdaQueryWrapper.eq(Teachplan::getCourseId, courseid).eq(Teachplan::getParentid, parentid);
Integer count = teachplanMapper.selectCount(eq);
teachplanDto.setOrderby(count+1);
teachplanMapper.insert(teachplanDto);
}else {
//更新
Teachplan teachplan = teachplanMapper.selectById(teachplanId);
BeanUtils.copyProperties(dto,teachplan);
teachplanMapper.updateById(teachplan);
}
}
删除课程计划
service流程:
* 1.查询数据库中课程计划
* 查看是否存在不存在抛出异常
* 2.获取课程计划的等级
* 若为章则递归删除其子节点
* 若为节,直接删除,查询是否有媒体信息,有则删除
service总代码:
Teachplan teachplan = teachplanMapper.selectById(id);
if (teachplan == null){
XueChengPlusException.cast("课程不存在");
}
Integer grade = teachplan.getGrade();
if (grade == 2){
this.deleteGrade2(id);
}else {
List<TeachplanDto> teachplanDtos =teachplanMapper.selectTreeNodes(id);
List<TeachplanDto> teachPlanTreeNodes = teachplanDtos.stream().filter(e -> e.getId().equals(teachplan.getId())).collect(Collectors.toList()).get(0).getTeachPlanTreeNodes();
teachplanMapper.deleteById(id);
teachPlanTreeNodes.forEach(e->this.deleteGrade2(e.getId()));
}
上下移动课程计划
/**
* 移动课程计划
* @param id
* @param moveWay
* @throws XueChengPlusException
*/
@Override
@Transactional
public void move(Long id, Boolean moveWay) throws XueChengPlusException {
/*
1.根据id查询,若查询为空抛出异常
2.根据查询得到的courseId以及grade查询到所有的计划根据moveDown选择Asc还是Desc 为True选择Desc
3.通过filter进行流过滤,过滤条件是元素的order大于/小于本order ,moveDown为True时是大于,为False小于
4.检查filter后收集的list是否为空empty说明无法移动抛出异常,不为空取出list尾部元素两者交换order然后更新
*/
/*1.根据id查询,若查询为空抛出异常*/
Teachplan teachplan = teachplanMapper.selectById(id);
if (teachplan == null) XueChengPlusException.cast("课程计划不存在");
/*2.根据查询得到的courseId以及grade查询到所有的计划根据moveDown选择Asc还是Desc 为True选择Desc*/
Long courseId = teachplan.getCourseId();
Integer grade = teachplan.getGrade();
Integer orderby = teachplan.getOrderby();
LambdaQueryWrapper<Teachplan> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper
.eq(Teachplan::getCourseId, courseId)
.eq(Teachplan::getGrade, grade);
if (moveWay)
lambdaQueryWrapper.orderByDesc(Teachplan::getOrderby);
else
lambdaQueryWrapper.orderByAsc(Teachplan::getOrderby);
/*通过filter进行流过滤,过滤条件是元素的order大于/小于本order ,moveDown为True时是大于,为False小于 */
List<Teachplan> teachplanList = teachplanMapper.selectList(lambdaQueryWrapper);
List<Teachplan> teachplanList1 = teachplanList.stream().filter(e -> moveWay ? e.getOrderby() > orderby : e.getOrderby() < orderby).collect(Collectors.toList());
/*检查filter后收集的list是否为空empty说明无法移动抛出异常,不为空取出list尾部元素两者交换order然后更新*/
if (teachplanList1.isEmpty()) XueChengPlusException.cast("无法完成该操作");
Teachplan teachplan1 = teachplanList1.get(teachplanList1.size()-1);
teachplan.setOrderby(teachplan1.getOrderby());
teachplan1.setOrderby(orderby);
teachplanMapper.updateById(teachplan);
teachplanMapper.updateById(teachplan1);
}
教师管理
进入教师设置页面后可以点击修改教师信息,或者点击新增教师信息
这两个业务共用一个接口,通过查看是否传入id来进行区分
- 修改时如下
- 新增时如下
- 课程教师信息删除
service层:
package com.xuecheng.content.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.content.mapper.CourseBaseMapper;
import com.xuecheng.content.mapper.CourseTeacherMapper;
import com.xuecheng.content.model.dto.TeacherSaveOrUpdateDto;
import com.xuecheng.content.model.po.CourseTeacher;
import com.xuecheng.content.service.CourseTeacherService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
@Service
@Slf4j
public class CourseTeacherServiceImpl extends ServiceImpl<CourseTeacherMapper, CourseTeacher> implements CourseTeacherService{
@Autowired
CourseBaseMapper courseBaseMapper;
/**
* 根据课程id查询教师
* @param courseId 课程id
* @return
* @throws XueChengPlusException
*/
@Override
public List<CourseTeacher> listTeacherByCourseId(Long courseId) throws XueChengPlusException {
if (courseId == null || courseId<0){
XueChengPlusException.cast("课程id不存在");
}
LambdaQueryWrapper<CourseTeacher> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(CourseTeacher::getCourseId,courseId);
//根据课程id查询课程教师信息
List<CourseTeacher> courseTeacherList = this.list(lambdaQueryWrapper);
if(courseTeacherList.isEmpty()) return null;
return courseTeacherList;
}
/**
* 新增/修改教师
* @param dto
*/
@Override
@Transactional
public void saveOrUpdateTeacher(TeacherSaveOrUpdateDto dto) throws XueChengPlusException {
/*
1. 判断dto中是否有id
2. 有表明是修改,先查询数据看,看对应id是否存在,不存在保存,存在则更新
3. 无表明是新增,直接update
*/
Long id =dto.getId();
Long courseId =dto.getCourseId();
//创建数据库对象
CourseTeacher courseTeacher = new CourseTeacher();
BeanUtils.copyProperties(dto,courseTeacher);
if (id != null){
//修改
LambdaQueryWrapper<CourseTeacher> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(CourseTeacher::getCourseId,courseId);
CourseTeacher one = this.getOne(lambdaQueryWrapper);
if (one == null){
XueChengPlusException.cast("教师不存在");
}
this.updateById(courseTeacher);
}else {
//新增
if (courseBaseMapper.selectById(courseId)==null)
XueChengPlusException.cast("课程不存在");
courseTeacher.setCreateDate(LocalDateTime.now());
if (!this.save(courseTeacher)) XueChengPlusException.cast("新增失败");
}
}
/**
* 删除教师id
* @param courseId 课程id
* @param id id
* @throws XueChengPlusException
*/
@Override
@Transactional
public void deleteTeacher(Long courseId, Long id) throws XueChengPlusException {
if (courseId == null || id == null)
XueChengPlusException.cast("id或者课程id为空");
/*查询课程id是否存在于数据库*/
if (courseBaseMapper.selectById(id) == null)
XueChengPlusException.cast("课程id不合法");
LambdaQueryWrapper<CourseTeacher> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(CourseTeacher::getCourseId,courseId);
lambdaQueryWrapper.eq(CourseTeacher::getId,id);
CourseTeacher one = this.getOne(lambdaQueryWrapper);
if (one == null)
XueChengPlusException.cast("教师不存在");
this.removeById(id);
}
}
删除课程
在课程管理界面,点击删除按钮删除课程
service层:
/**
* 删除课程
* @param id
*/
@Override
@Transactional
public void deleteCourseById(Long id) throws XueChengPlusException {
/*
0.验证此课程id是否合法
1.根据id删除课程教师信息
2.根据courseId删除课程计划信息
2.1查询课程计划信息
2.2调用删除章目录的服务
3.删除课程营销信息和课程基本信息
*/
/*获取课程基本信息*/
CourseBase courseBase = courseBaseMapper.selectById(id);
CourseMarket courseMarket =new CourseMarket();
BeanUtils.copyProperties(courseBase, courseMarket);
if (courseBase == null){
XueChengPlusException.cast("id不合法");
}
//删除课程教师
List<CourseTeacher> list = courseTeacherService.listTeacherByCourseId(id);
if (list != null&&!list.isEmpty()){
/*list非空说明有教师数据,通过主键remove*/
list.forEach(e-> {
try {
courseTeacherService.deleteTeacher(id, e.getId());
} catch (XueChengPlusException ex) {
throw new RuntimeException(ex);
}
});
}
/*根据courseId删除课程计划信息*/
//查询课程计划信息
List<TeachplanDto> teachplanList = teachPlanService.findTeachplanList(id);
if (teachplanList != null && !teachplanList.isEmpty()){
teachplanList.forEach(e->{
/*获取二级节点*/
List<TeachplanDto> teachPlanTreeNodes = e.getTeachPlanTreeNodes();
/*若二级节点不空,遍历删除*/
if (teachPlanTreeNodes != null && !teachPlanTreeNodes.isEmpty()){
teachPlanTreeNodes.forEach(kid->teachPlanService.deleteGrade2(kid.getId()));
}
try {
teachPlanService.deleteTeachPlan(e.getId());
} catch (XueChengPlusException ex) {
throw new RuntimeException(ex);
}
});
}
/*删除课程营销信息和课程基本信息*/
LambdaQueryWrapper<CourseMarket> courseMarketLambdaQueryWrapper=new LambdaQueryWrapper<>();
courseMarketLambdaQueryWrapper.eq(CourseMarket::getId,id);
if ("201001".equals(courseMarket.getCharge())){
courseMarketMapper.deleteById(id);
}
courseBaseMapper.deleteById(id);
}
Nacos远程服务器搭建
先拉取1.4.1的镜像并启动:
docker pull nacos/nacos-server:1.4.1
docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server:1.4.1
进入nacos配置文件:
docker exec -it nacos bash
配置MySQL:
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://xxxxxxxx:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456