完整版请移步至我的个人博客查看:https://cyborg2077.github.io/
学成在线–项目环境搭建
学成在线–内容管理模块
学成在线–媒资管理模块
学成在线–课程发布模块
学成在线–认证授权模块
学成在线–选课学习模块
学成在线–项目优化
Git仓库:https://github.com/Cyborg2077/xuecheng-plus
需求分析也称为软件需求分析、系统需求分析或需求分析工程等,是开发人员经过深入细致的调研和分析,准确理解用户和项目的功能、性能、可靠性等具体需求,将用户非形式的需求表叔转化为完整的需求定义,从而确定系统必须做什么的过程
简单理解就是要高清问题域,问题域就是用户的需求,软件要为用户解决什么问题,实现哪些业务功能,满足什么样的性能要求
那么如何做需求分析?
项目 | 添加课程 |
---|---|
功能名称 | 添加课程 |
功能描述 | 添加课程基本信息 |
参与者 | 教学机构管理员 |
前置条件 | 教学机构管理只允许向自己机构添加课程 拥有添加课程的权限 |
基本事件流程 | 1、登录教学机构平台 2、进入课程列表页面 3、点击添加课程按钮进入添加课程界面 4、填写课程基本信息 5、点击提交。 |
可选事件流程 | 成功:提示添加成功,跳转到课程营销信息添加界面 失败:提示具体的失败信息,用户根据失败信息进行修改。 |
数据描述 | 课程基本信息:课程id、课程名称、课程介绍、课程大分类、课程小分类、课程等级、课程图片、所属机构、课程创建时间、课程修改时间、课程状态 |
后置条件 | 向课程基本信息插入一条记录 |
补充说明 |
那下面我们继续来创建内容管理模块的工程结构。本项目是一个前后端分离项目,前端与后端开发人员之间主要依据接口进行开发,前后端交互流程如下
流程分为前端、接口层、业务层三部分,所以模块工程结构如下图所示
结合项目父工程、项目基础工程后,如下图
4.0.0
com.xuecheng
xuecheng-plus-parent
0.0.1-SNAPSHOT
../xuecheng-plus-parent
com.xuecheng
xuecheng-plus-content
0.0.1-SNAPSHOT
xuecheng-plus-content
xuecheng-plus-content
pom
1.8
xuecheng-plus-content-api
xuecheng-plus-content-model
xuecheng-plus-content-service
xuecheng-plus-content
,按照上图中的依赖关系修改pom文件
4.0.0
com.xuecheng
xuecheng-plus-content
0.0.1-SNAPSHOT
xuecheng-plus-content-api
0.0.1-SNAPSHOT
xuecheng-plus-content-api
xuecheng-plus-content-api
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
com.xuecheng
xuecheng-plus-content-service
0.0.1-SNAPSHOT
com.xuecheng
xuecheng-plus-content-model
0.0.1-SNAPSHOT
org.springframework.boot
spring-boot-maven-plugin
xuecheng-plus-content
,按照上图中的依赖关系修改pom文件
4.0.0
com.xuecheng
xuecheng-plus-content
0.0.1-SNAPSHOT
xuecheng-plus-content-model
0.0.1-SNAPSHOT
xuecheng-plus-content-model
xuecheng-plus-content-model
1.8
com.xuecheng
xuecheng-plus-base
0.0.1-SNAPSHOT
org.springframework.boot
spring-boot-maven-plugin
xuecheng-plus-content
,按照上图中的依赖关系修改pom文件
4.0.0
com.xuecheng
xuecheng-plus-content
0.0.1-SNAPSHOT
xuecheng-plus-content-service
0.0.1-SNAPSHOT
xuecheng-plus-content-service
xuecheng-plus-content-service
1.8
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-test
test
com.xuecheng
xuecheng-plus-content-model
0.0.1-SNAPSHOT
org.springframework.boot
spring-boot-maven-plugin
PO即持久对象(Persistent Object),它们是由一组属性和属性的get/set方法组成的。PO对应于数据库的表
在开发持久层代码需要根据数据表编写PO类,在实际开发中通常使用代码生成器工具来生成PO类代码(人人开源、MP代码生成器等)
这里是使用的MP的generator工程生成PO类,详细操作可以参考我这篇文章
{% link MyBatisPlus, https://cyborg2077.github.io/2022/09/20/MyBatisPlus/, https://pic.imgdb.cn/item/6335135c16f2c2beb100182d.jpg %}
将生成好的PO类拷贝至xuecheng-plus-content-model工程的com.xuecheng.content.model.po包下,同时在model工程的pom.xml中添加MP的依赖,版本控制在父工程已经完成了
com.baomidou
mybatis-plus-annotation
${mybatis-plus-boot-starter.version}
定义一个接口需要包括以下几个方面
接口请求示例
POST /content/course/list?pageNo=2&pageSize=1
Content-Type: application/json
{
"auditStatus": "202002",
"courseName": ""
}
//成功响应结果
{
"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
}
定义请求模型类
package com.xuecheng.base.model;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class PageParams {
// 默认起始页码
public static final long DEFAULT_PAGE_CURRENT = 1L;
// 默认每页记录数
public static final long DEFAULT_PAGE_SIZE = 10L;
// 当前页码
private Long pageNo = DEFAULT_PAGE_CURRENT;
// 当前每页记录数
private Long pageSize = DEFAULT_PAGE_SIZE;
}
package com.xuecheng.content.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class QueryCourseParamDto {
// 审核状态
private String auditStatus;
// 课程名称
private String courseName;
// 发布状态
private String publishStatus;
}
多样化的模型类
不一样
,那此时就需要定义两个Controller课程查询接口,每个接口定义VO对象与前端传输数据
定义响应模型类
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
@AllArgsConstructor
public class PageResult implements Serializable {
// 数据列表
private List items;
// 总记录数
private long counts;
// 当前页码
private long page;
// 每页记录数
private long pageSize;
}
定义接口
com.xuecheng
xuecheng-plus-content-model
0.0.1-SNAPSHOT
com.xuecheng
xuecheng-plus-content-service
0.0.1-SNAPSHOT
org.springframework.cloud
spring-cloud-context
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-log4j2
com.spring4all
swagger-spring-boot-starter
1.9.0.RELEASE
@RestController
@Api(value = "课程信息编辑接口", tags = "课程信息编辑接口")
public class CourseBaseInfoController {
@PostMapping("/course/list")
@ApiOperation("课程查询接口")
public PageResult list(PageParams pageParams, @RequestBody QueryCourseParamDto queryCourseParams) {
CourseBase courseBase = new CourseBase();
courseBase.setId(15L);
courseBase.setDescription("测试课程");
PageResult result = new PageResult<>();
result.setItems(Arrays.asList(courseBase));
result.setPage(1);
result.setPageSize(10);
result.setCounts(1);
return result;
}
}
logs
%date{YYYY-MM-dd HH:mm:ss,SSS} %level [%thread][%file:%line] - %msg%n%throwable
server:
servlet:
context-path: /content
port: 63040
# 微服务配置
spring:
application:
name: content-api
# 日志文件配置路径
logging:
config: classpath:log4j2-dev.xml
# swagger 文档配置
swagger:
title: "学成在线内容管理系统"
description: "内容系统管理系统对课程相关信息进行业务管理数据"
base-package: com.xuecheng.content
enabled: true
version: 1.0.0
{% endtabs %}
com.spring4all
swagger-spring-boot-starter
swagger:
title: "学成在线内容管理系统"
description: "内容系统管理系统对课程相关信息进行业务管理数据"
base-package: com.xuecheng.content
enabled: true
version: 1.0.0
{% endtabs %}@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageParams {
// 默认起始页码
public static final long DEFAULT_PAGE_CURRENT = 1L;
// 默认每页记录数
public static final long DEFAULT_PAGE_SIZE = 10L;
// 当前页码
+ @ApiModelProperty("当前页码")
private Long pageNo = DEFAULT_PAGE_CURRENT;
// 当前每页记录数
+ @ApiModelProperty("每页记录数")
private Long pageSize = DEFAULT_PAGE_SIZE;
}
@Api | 修饰整个类,描述Controller的作用 |
---|---|
@ApiOperation | 描述一个类的一个方法,或者说一个接口 |
@ApiParam | 单个参数描述 |
@ApiModel | 用对象来接收参数 |
@ApiModelProperty | 用对象接收参数时,描述对象的一个字段 |
@ApiResponse | HTTP响应其中1个描述 |
@ApiResponses | HTTP响应整体描述 |
@ApiIgnore | 使用该注解忽略这个API |
@ApiError | 发生错误返回的信息 |
@ApiImplicitParam | 一个请求参数 |
@ApiImplicitParams | 多个请求参数 |
com.xuecheng
xuecheng-plus-content-model
0.0.1-SNAPSHOT
mysql
mysql-connector-java
runtime
com.baomidou
mybatis-plus-boot-starter
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-logging
org.springframework.boot
spring-boot-starter-log4j2
/**
* MybatisPlus配置
*/
@Configuration
@MapperScan("com.xuecheng.content.mapper")
public class MybatisPlusConfig {
/**
* 定义分页拦截器
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
spring:
application:
name: content-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xc_content?serverTimezone=UTC&userUnicode=true&useSSL=false
username: root
password: root
logging:
config: classpath:log4j2-dev.xml
@Slf4j
@SpringBootTest(classes = XuechengPlusContentServiceApplication.class)
class XuechengPlusContentServiceApplicationTests {
@Autowired
CourseBaseMapper courseBaseMapper;
@Test
void contextLoads() {
CourseBase courseBase = courseBaseMapper.selectById(22);
log.info("查询到数据:{}", courseBase);
Assertions.assertNotNull(courseBase);
}
}
数据字典表
审核未通过
这几个大字,合适吗?
审核未通过
这五个字记录在课程基本信息表中,查询出来的状态就是审核未通过
这几个字,那么如果有一天客户想把审核未通过
改为未通过
,怎么办?审核未通过
更新为未通过
。看起来解决了问题,但是万一后期客户抽风又想改呢?真实情况就是这样,但是这一类数据也有共同点:它有一些分类项,且这些分类项比较固定,大致的意思都是一样的,只是表述方式不一样。[
{"code":"202001","desc":"审核未通过"},
{"code":"202002","desc":"未审核"},
{"code":"202003","desc":"审核通过"}
]
审核未通过
的显示内容,直接在数据字典中修改就好了,无需修改课程基本信息表Service开发
public interface CourseBaseInfoService {
/**
* 课程查询接口
* @param pageParams 分页参数
* @param queryCourseParams 查询条件
* @return
*/
PageResult queryCourseBaseList(PageParams pageParams, QueryCourseParamDto queryCourseParams);
}
@Service
public class CourseBaseInfoServiceImpl implements CourseBaseInfoService {
@Resource
CourseBaseMapper courseBaseMapper;
@Override
public PageResult queryCourseBaseList(PageParams pageParams, QueryCourseParamDto queryCourseParams) {
// 构建条件查询器
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
// 构建查询条件:按照课程名称模糊查询
queryWrapper.like(StringUtils.isNotEmpty(queryCourseParams.getCourseName()), CourseBase::getCompanyName, queryCourseParams.getCourseName());
// 构建查询条件,按照课程审核状态查询
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParams.getAuditStatus()), CourseBase::getAuditStatus, queryCourseParams.getAuditStatus());
// 构建查询条件,按照课程发布状态查询
queryWrapper.eq(StringUtils.isNotEmpty(queryCourseParams.getPublishStatus()), CourseBase::getStatus, queryCourseParams.getPublishStatus());
// 分页对象
Page page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
// 查询数据内容获得结果
Page pageInfo = courseBaseMapper.selectPage(page, queryWrapper);
// 获取数据列表
List items = pageInfo.getRecords();
// 获取数据总条数
long counts = pageInfo.getTotal();
// 构建结果集
return new PageResult<>(items, counts, pageParams.getPageNo(), pageParams.getPageSize());
}
}
@Resource
CourseBaseInfoService courseBaseInfoService;
@Test
void contextQueryCourseTest() {
PageResult result = courseBaseInfoService.queryCourseBaseList(new PageParams(1L, 10L), new QueryCourseParamDto());
log.info("查询到数据:{}", result);
}
@Resource
CourseBaseInfoService courseBaseInfoService;
@ApiOperation("课程查询接口")
@PostMapping("/course/list")
public PageResult list(PageParams pageParams, @RequestBody QueryCourseParamDto queryCourseParams) {
PageResult result = courseBaseInfoService.queryCourseBaseList(pageParams, queryCourseParams);
return result;
}
在HTTP客户端中生成请求
,即可生成一个测试用例,IDEA会为我们生成一个.http结尾的文件,我们可以添加请求参数进行测试### 课程查询列表
POST http://localhost:63040/content/course/list?pageNo=1&pageSize=10
Content-Type: application/json
{
"auditStatus": "",
"courseName": "",
"publishStatus": ""
}
{
"dev": {
"host": "localhost:63010",
"content_host": "localhost:63040",
"system_host": "localhost:63110",
"media_host": "localhost:63050",
"cache_host": "localhost:63035"
}
}
Access to XMLHttpRequest at 'http://harib-eir.info/xuecheng-plus.com?adTagId=dbb6a410-0bec-11ec-8010-0a70670a1f67&fallbackUrl=ww87.xuecheng-plus.com' (redirected from 'http://localhost:8601/api/content/course/list?pageNo=1&pageSize=10') from origin 'http://localhost:8601' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
提示:从http://localhost:8601访问http://localhost:63110/system/dictionary/all被CORS policy阻止,因为没有Access-Control-Allow-Origin 头信息。CORS全称是 cross origin resource share 表示跨域资源共享。
比如:
{% note warning no-icon %}
注意:服务器之间不存在跨域请求。
{% endnote %}
GET / HTTP/1.1
Origin: http://localhost:8601
Access-Control-Allow-Origin:http://localhost:8601
Access-Control-Allow-Origin:*
解决跨域的方法
Access-Control-Allow-Origin: *
这里采用添加请求头的方式解决跨域问题。在xuecheng-plus-system-api模块下新建配置类GlobalCorsConfig
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter getCorsFilter() {
CorsConfiguration configuration = new CorsConfiguration();
//添加哪些http方法可以跨域,比如:GET,Post,(多个方法中间以逗号分隔),*号表示所有
configuration.addAllowedMethod("*");
//添加允许哪个请求进行跨域,*表示所有,可以具体指定http://localhost:8601 表示只允许http://localhost:8601/跨域
configuration.addAllowedOrigin("*");
//所有头信息全部放行
configuration.addAllowedHeader("*");
//允许跨域发送cookie
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", configuration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
Access-Control-Allow-Origin
# 前台管理页面-端口
VUE_APP_CLIENT_MANAGE_PORT=8601
# 首页、列表、学习
VUE_APP_CLIENT_PORTAL_URL=http://www.xuecheng-plus.com
# 后台服务网关
#VUE_APP_SERVER_API_URL=http://www.xuecheng-plus.com/api
#VUE_APP_SERVER_API_URL=http://localhost:63010
#VUE_APP_SERVER_API_URL=http://172.16.63.20:63010
VUE_APP_SERVER_API_URL=http://localhost:63040
# 权限认证
#VUE_APP_SERVER_AUTHORIZATION=ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9
VUE_APP_SERVER_AUTHORIZATION=
#ewogICAgImF1ZCI6IFsKICAgICAgICAieHVlY2hlbmctcmVzb3VyY2UiCiAgICBdLAogICAgInBheWxvYWQiOiB7CiAgICAgICAgIjExNzcxNDQyMDk0NjMxMjgxMjUiOiB7CiAgICAgICAgICAgICJyZXNvdXJjZXMiOiBbCiAgICAgICAgICAgIF0sCiAgICAgICAgICAgICJ1c2VyX2F1dGhvcml0aWVzIjogewogICAgICAgICAgICAgICAgInJfMDAxIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb21wYW55X21vZGlmeSIsCgkJCQkJInhjX2NvbXBhbnlfdmlldyIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2RlbCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2VkaXQiLAoJCQkJCSJ4Y19jb3Vyc2VfYmFzZV9saXN0IiwKCQkJCQkieGNfY291cnNlX2Jhc2Vfc2F2ZSIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX3ZpZXciLAoJCQkJCSJ4Y19jb3Vyc2VfcHVibGlzaCIsCgkJCQkJInhjX21hcmtldF9zYXZlX21vZGlmeSIsCgkJCQkJInhjX21hcmtldF92aWV3IiwKCQkJCQkieGNfbWVkaWFfZGVsIiwKCQkJCQkieGNfbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX3ByZXZpZXciLAoJCQkJCSJ4Y19tZWRpYV9zYXZlIiwKCQkJCQkieGNfdGVhY2hlcl9saXN0IiwKCQkJCQkieGNfdGVhY2hlcl9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaGVyX3NhdmUiLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2NvcnJlY3Rpb24iLAoJCQkJCSJ4Y193b3JrcmVjb3JkX2xpc3QiLAoJCQkJCSJ4Y190ZWFjaHBsYW53b3JrX2RlbCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfbGlzdCIsCgkJCQkJInhjX3RlYWNocGxhbndvcmtfc2F2ZV9tb2RpZnkiLAoJCQkJCSJ4Y190ZWFjaHBsYW5fZGVsIiwKCQkJCQkieGNfdGVhY2hwbGFuX3NhdmVfbW9kaWZ5IiwKCQkJCQkieGNfdGVhY2hwbGFuX3ZpZXciCiAgICAgICAgICAgICAgICBdLAogICAgICAgICAgICAgICAgInJfMDAyIjogWwogICAgICAgICAgICAgICAgICAgICJ4Y19jb3Vyc2VfYWRtaW5fbGlzdCIsCgkJCQkJInhjX2NvdXJzZV9iYXNlX2NvbW1pdCIsCgkJCQkJInhjX3N5c3RlbV9jYXRlZ29yeSIsCgkJCQkJInhjX21fbWVkaWFfbGlzdCIsCgkJCQkJInhjX21lZGlhX2F1ZGl0IgogICAgICAgICAgICAgICAgXQogICAgICAgICAgICB9CiAgICAgICAgfQogICAgfSwKICAgICJ1c2VyX25hbWUiOiAieGMtdXNlci1maXJzdCIsCiAgICAic2NvcGUiOiBbCiAgICAgICAgInJlYWQiCiAgICBdLAogICAgIm1vYmlsZSI6ICIxNTAxMjM0NTY3OCIsCiAgICAiZXhwIjogMTYwNjUyNTEyMiwKICAgICJjbGllbnRfYXV0aG9yaXRpZXMiOiBbCiAgICAgICAgIlJPTEVfVVNFUiIKICAgIF0sCiAgICAianRpIjogIjFlYjdlOTg3LWQ3YzItNDBmNS1iMGQ2LWNkNjEzOWNiMThlMCIsCiAgICAiY2xpZW50X2lkIjogInhjLWNvbS1wbGF0Zm9ybSIsCiAgICAiY29tcGFueUlkIjogMTIzMjE0MTQyNQp9
# Cookie域
VUE_APP_SERVER_DOMAIN=xuecheng-plus.com
# 七牛云静态页
VUE_APP_SERVER_QINIU_URL=http://localhost11
# 图片服务器地址
VUE_APP_SERVER_PICSERVER_URL=http://192.168.101.65:9000
请求网址: http://localhost:8601/api/content/course-category/tree-nodes
请求方法: GET
状态代码: 404 Not Found
远程地址: 127.0.0.1:8601
请求网址: http://localhost:8601/api/content/course-category/tree-nodes
请求方法: GET
状态代码: 404 Not Found
远程地址: 127.0.0.1:8601
[
{
"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"
}
],
"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" : 8,
"parentid" : "1-2"
}
],
"id" : "1-2",
"isLeaf" : null,
"isShow" : null,
"label" : "移动开发",
"name" : "移动开发",
"orderby" : 2,
"parentid" : "1"
}
]
"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"
},
{
"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" : 8,
"parentid" : "1-2"
}
]
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CourseCategoryTreeDto extends CourseCategory {
List childrenTreeNodes;
}
@Slf4j
@RestController("/course-category/tree-nodes")
@Api(value = "课程分类相关接口", tags = "课程分类相关接口")
public class CourseCategoryController {
@ApiOperation("课程分类相关接口")
public List queryTreeNodes() {
return null;
}
}
two.parentid = one.id
(二级分类的父节点为一级分类),同时one.parentid = 1
(一级分类的父节点为根节点),这样就可以查询出所有的数据SELECT * FROM
course_category one
JOIN course_category two
ON two.parentid = one.id
WHERE one.parentid = '1'
SELECT * FROM
course_category one
JOIN course_category two
ON two.parentid = one.id
JOIN course_category three
+ ON three.parentid = two.id
WHERE one.parentid = '1'
WITH RECURSIVE t1 AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM t1 WHERE n < 5
)
SELECCT * FROM t1;
SELECT 1 AS n
,就是给一个递归的初始值,WHERE n < 5
则是终止条件WITH RECURSIVE t1 AS (
SELECT p.* FROM course_category p WHERE p.id = '1'
UNION ALL
SELECT c.* FROM course_category c JOIN t1 WHERE c.parentid = t1.id
)
SELECT * FROM t1;
public interface CourseCategoryMapper extends BaseMapper {
List selectTreeNodes();
}
public interface CourseCategoryService {
/**
* 课程分类查询
* @param id 根节点id
* @return 根节点下面的所有子节点
*/
List queryTreeNodes(String id);
}
@Slf4j
@Service
public class CourseCategoryServiceImpl implements CourseCategoryService {
@Autowired
private CourseCategoryMapper courseCategoryMapper;
@Override
public List queryTreeNodes(String id) {
// 获取所有的子节点
List categoryTreeDtos = courseCategoryMapper.selectTreeNodes(id);
// 定义一个List,作为最终返回的数据
List result = new ArrayList<>();
// 为了方便找子节点的父节点,这里定义一个HashMap,key是节点的id,value是节点本身
HashMap nodeMap = new HashMap<>();
// 将数据封装到List中,只包括根节点的下属节点(1-1、1-2 ···),这里遍历所有节点
categoryTreeDtos.stream().forEach(item -> {
// 这里寻找父节点的直接下属节点(1-1、1-2 ···)
if (item.getParentid().equals(id)) {
nodeMap.put(item.getId(), item);
result.add(item);
}
// 获取每个子节点的父节点
String parentid = item.getParentid();
CourseCategoryTreeDto parentNode = nodeMap.get(parentid);
// 判断HashMap中是否存在该父节点(按理说必定存在,以防万一)
if (parentNode != null) {
// 为父节点设置子节点(将1-1-1设为1-1的子节点)
List childrenTreeNodes = parentNode.getChildrenTreeNodes();
// 如果子节点暂时为null,则初始化一下父节点的子节点(给个空集合就行)
if (childrenTreeNodes == null) {
parentNode.setChildrenTreeNodes(new ArrayList());
}
// 将子节点设置给父节点
parentNode.getChildrenTreeNodes().add(item);
}
});
// 返回根节点的直接下属节点(1-1、1-2 ···)
return result;
}
}
@Resource
CourseCategoryService courseCategoryService;
@Test
void contextCourseCategoryTest() {
List courseCategoryTreeDtos = courseCategoryService.queryTreeNodes("1");
System.out.println(courseCategoryTreeDtos);
}
@Slf4j
@RestController
@Api(value = "课程分类相关接口", tags = "课程分类相关接口")
public class CourseCategoryController {
@Resource
private CourseCategoryService courseCategoryService;
@ApiOperation("课程分类相关接口")
@GetMapping("/course-category/tree-nodes")
public List queryTreeNodes() {
return courseCategoryService.queryTreeNodes("1");
}
}
public interface CourseCategoryMapper extends BaseMapper {
List selectTreeNodes();
}
{% endtabs %}
请求网址: http://localhost:8601/api/content/course
请求方法: POST
状态代码: 404 Not Found
远程地址: 127.0.0.1:8601
### 创建课程
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
}
package com.xuecheng.content.model.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
/**
* @version 1.0
* @description 添加课程dto
*/
@Data
@ApiModel(value = "AddCourseDto", description = "新增课程基本信息")
public class AddCourseDto {
@ApiModelProperty(value = "课程名称", required = true)
private String name;
@ApiModelProperty(value = "适用人群", required = true)
private String users;
@ApiModelProperty(value = "课程标签")
private String tags;
@ApiModelProperty(value = "大分类", required = true)
private String mt;
@ApiModelProperty(value = "小分类", required = true)
private String st;
@ApiModelProperty(value = "课程等级", required = true)
private String grade;
@ApiModelProperty(value = "教学模式(普通,录播,直播等)", required = true)
private String teachmode;
@ApiModelProperty(value = "课程介绍")
private String description;
@ApiModelProperty(value = "课程图片", required = true)
private String pic;
@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;
}
package com.xuecheng.content.model.dto;
import com.xuecheng.content.model.po.CourseBase;
import lombok.Data;
/**
* @version 1.0
* @description 课程基本信息dto
*/
@Data
public class CourseBaseInfoDto extends CourseBase {
/**
* 收费规则,对应数据字典
*/
private String charge;
/**
* 价格
*/
private Float price;
/**
* 原价
*/
private Float originalPrice;
/**
* 咨询qq
*/
private String qq;
/**
* 微信
*/
private String wechat;
/**
* 电话
*/
private String phone;
/**
* 有效期天数
*/
private Integer validDays;
/**
* 大分类名称
*/
private String mtName;
/**
* 小分类名称
*/
private String stName;
}
{% endtabs %}
@ApiOperation("新增课程基础信息接口")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto) {
return null;
}
/**
* 新增课程基本信息
* @param companyId 教学机构id
* @param addCourseDto 课程基本信息
* @return
*/
CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto addCourseDto);
@Resource
CourseBaseMapper courseBaseMapper;
@Resource
CourseMarketMapper courseMarketMapper;
@Resource
CourseCategoryMapper courseCategoryMapper;
@Override
@Transactional
public CourseBaseInfoDto createCourseBase(Long companyId, AddCourseDto addCourseDto) {
// 1. 合法性校验
if (StringUtils.isBlank(addCourseDto.getName())) {
throw new RuntimeException("课程名称为空");
}
if (StringUtils.isBlank(addCourseDto.getMt())) {
throw new RuntimeException("课程分类为空");
}
if (StringUtils.isBlank(addCourseDto.getSt())) {
throw new RuntimeException("课程分类为空");
}
if (StringUtils.isBlank(addCourseDto.getGrade())) {
throw new RuntimeException("课程等级为空");
}
if (StringUtils.isBlank(addCourseDto.getTeachmode())) {
throw new RuntimeException("教育模式为空");
}
if (StringUtils.isBlank(addCourseDto.getUsers())) {
throw new RuntimeException("适应人群为空");
}
if (StringUtils.isBlank(addCourseDto.getCharge())) {
throw new RuntimeException("收费规则为空");
}
// 2. 封装请求参数
// 封装课程基本信息
CourseBase courseBase = new CourseBase();
BeanUtils.copyProperties(addCourseDto, courseBase);
// 2.1 设置默认审核状态(去数据字典表中查询状态码)
courseBase.setAuditStatus("202002");
// 2.2 设置默认发布状态
courseBase.setStatus("203001");
// 2.3 设置机构id
courseBase.setCompanyId(companyId);
// 2.4 设置添加时间
courseBase.setCreateDate(LocalDateTime.now());
// 2.5 插入课程基本信息表
int baseInsert = courseBaseMapper.insert(courseBase);
Long courseId = courseBase.getId();
// 封装课程营销信息
CourseMarket courseMarket = new CourseMarket();
BeanUtils.copyProperties(addCourseDto, courseMarket);
courseMarket.setId(courseId);
// 2.6 判断收费规则,若课程收费,则价格必须大于0
String charge = courseMarket.getCharge();
if ("201001".equals(charge)) {
Float price = addCourseDto.getPrice();
if (price == null || price.floatValue() <= 0) {
throw new RuntimeException("课程设置了收费,价格不能为空,且必须大于0");
}
}
// 2.7 插入课程营销信息表
int marketInsert = courseMarketMapper.insert(courseMarket);
if (baseInsert <= 0 || marketInsert <= 0) {
throw new RuntimeException("新增课程基本信息失败");
}
// 3. 返回添加的课程信息
return getCourseBaseInfo(courseId);
}
private CourseBaseInfoDto getCourseBaseInfo(Long courseId) {
CourseBaseInfoDto courseBaseInfoDto = new CourseBaseInfoDto();
// 1. 根据课程id查询课程基本信息
CourseBase courseBase = courseBaseMapper.selectById(courseId);
if (courseBase == null)
return null;
// 1.1 拷贝属性
BeanUtils.copyProperties(courseBase, courseBaseInfoDto);
// 2. 根据课程id查询课程营销信息
CourseMarket courseMarket = courseMarketMapper.selectById(courseId);
// 2.1 拷贝属性
if (courseMarket != null)
BeanUtils.copyProperties(courseMarket, courseBaseInfoDto);
// 3. 查询课程分类名称,并设置属性
// 3.1 根据小分类id查询课程分类对象
CourseCategory courseCategoryBySt = courseCategoryMapper.selectById(courseBase.getSt());
// 3.2 设置课程的小分类名称
courseBaseInfoDto.setStName(courseCategoryBySt.getName());
// 3.3 根据大分类id查询课程分类对象
CourseCategory courseCategoryByMt = courseCategoryMapper.selectById(courseBase.getMt());
// 3.4 设置课程大分类名称
courseBaseInfoDto.setMtName(courseCategoryByMt.getName());
return courseBaseInfoDto;
}
@ApiOperation("新增课程基础信息接口")
@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto addCourseDto) {
// 机构id,暂时硬编码模拟假数据
Long companyId = 22L;
return courseBaseInfoService.createCourseBase(companyId, addCourseDto);
}
学成在线–内容管理模块