在线教育

  • 普通用户前台使用系统
  • 管理员后台管理系统
    • 讲师管理模块
      • 环境搭建
      • 讲师列表查询
      • 讲师删除(逻辑删除)
      • 整合swagger
      • 统一结果数据返回
      • 分页查询讲师
      • 新增讲师
      • 统一异常处理
      • 日志
      • 登录功能
      • 上传讲师头像
    • 课程分类管理模块
      • 环境搭建
      • 添加课程分类
      • 课程分类显示
    • 课程管理模块
      • 环境搭建
      • 添加课程
      • 课程大纲显示(章节和小节)
      • 修改课程基本信息
      • 章节的添加修改删除
      • 添加和删除小节
      • 课程信息确认和发布课程
      • 课程列表显示和删除课程
      • 添加小节上传视频
      • 视频删除
      • 删除小节并删除掉小节下的视频
      • 删除课程删除视频
      • 整合熔断器
    • 统计分析模块
      • 需求分析
      • 生成统计数据
      • 添加定时任务
      • 图表显示

普通用户前台使用系统

https://blog.csdn.net/weixin_45581692/article/details/127317141

管理员后台管理系统

讲师管理模块

环境搭建

1. 创建数据库表:讲师表

CREATE TABLE `edu_teacher` (`id` char(19) NOT NULL COMMENT '讲师ID',`name` varchar(20) NOT NULL COMMENT '讲师姓名',`intro` varchar(500) NOT NULL DEFAULT '' COMMENT '讲师简介',`career` varchar(500) DEFAULT NULL COMMENT '讲师资历,一句话说明讲师',`level` int(10) unsigned NOT NULL COMMENT '头衔 1高级讲师 2首席讲师',`avatar` varchar(255) DEFAULT NULL COMMENT '讲师头像',`sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序',`is_deleted` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',`gmt_create` datetime NOT NULL COMMENT '创建时间',`gmt_modified` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='讲师';

2. 创建项目,引入依赖

3. 编写配置文件(application.properties)

# 服务端口号
server.port=8001# 服务名
spring.application.name=service-edu# 环境设置:dev、test、prod
spring.profiles.active=dev# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guliedu?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=qwer`123# mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

4. 代码生成器生成代码
相关依赖

<dependency><groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId><version>2.0</version>
</dependency>
public class CodeGenerator {@Testpublic void run() {// 1、创建代码生成器AutoGenerator mpg = new AutoGenerator();// 2、全局配置GlobalConfig gc = new GlobalConfig();String projectPath = System.getProperty("user.dir");//(改)绝对路径gc.setOutputDir("D:\\my_items2\\guli_parent\\service\\service_edu" + "/src/main/java");gc.setAuthor("hxp");gc.setOpen(false); //生成后是否打开资源管理器gc.setFileOverride(false); //重新生成时文件是否覆盖gc.setServiceName("%sService");  //去掉Service接口的首字母Igc.setIdType(IdType.ID_WORKER_STR); //主键策略(id是数字类型改成ID_WORKER,字符串类型改成ID_WORKER_STR)gc.setDateType(DateType.ONLY_DATE);//定义生成的实体类中日期类型gc.setSwagger2(true);//开启Swagger2模式mpg.setGlobalConfig(gc);// 3、数据源配置(改)DataSourceConfig dsc = new DataSourceConfig();dsc.setUrl("jdbc:mysql://localhost:3306/guliedu?serverTimezone=GMT%2B8");dsc.setDriverName("com.mysql.cj.jdbc.Driver");dsc.setUsername("root");dsc.setPassword("qwer`123");dsc.setDbType(DbType.MYSQL); //数据库类型mpg.setDataSource(dsc);// 4、包配置PackageConfig pc = new PackageConfig();//com.hxp.eduservicepc.setParent("com.hxp");pc.setModuleName("eduservice"); //模块名(改)pc.setController("controller");pc.setEntity("entity");pc.setService("service");pc.setMapper("mapper");mpg.setPackageInfo(pc);// 5、策略配置StrategyConfig strategy = new StrategyConfig();strategy.setInclude("edu_course", "edu_chapter","edu_course_description","edu_video"); //(改)数据库表名,可以一次生成多个strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略strategy.setTablePrefix(pc.getModuleName() + "_"); //生成实体时去掉表前缀strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略strategy.setEntityLombokModel(true); // lombok 模型 @Accessors(chain = true) setter链式操作strategy.setRestControllerStyle(true); //restful api风格控制器strategy.setControllerMappingHyphenStyle(true); //url中驼峰转连字符mpg.setStrategy(strategy);// 6、执行mpg.execute();}
}

讲师列表查询

(1)编写接口
controller

@RestController
@RequestMapping("/eduservice/teacher")
public class EduTeacherController {@Autowiredprivate EduTeacherService teacherService;//查询讲师表所有数据@ApiOperation(value = "所有讲师列表") //作用在方法上@GetMapping("findAll")public R findAllTeacher() {return R.ok().data("items", teacherService.list(null));}
}

service

public interface EduTeacherService extends IService<EduTeacher> {}

(2)创建config包,包下创建配置类
@MapperScan可以放在主启动类上,但建议统一放在配置类中。

@Configuration
@MapperScan("com.hxp.eduservice.mapper") //扫描到mapper接口
public class EduConfig {}

细节处理
根据上面的代码查询出来的时间显示如下:

默认情况下json时间格式带有时区,并且是世界标准时间,和我们的时间相差8小时,解决:在application.properties配置文件中加上配置。

# 返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8

讲师删除(逻辑删除)

(1)在配置类加上一段配置

/*** 逻辑删除插件*/
@Bean
public ISqlInjector sqlInjector() {return new LogicSqlInjector();
}

(2)在实体类的逻辑删除属性上面添加注解

@TableLogic
private Boolean isDeleted;

(3)编写controller层代码

//逻辑删除讲师
@DeleteMapping("{id}")
public R removeTeacher(@PathVariable String id) { //获取路径中输入的idboolean flag = teacherService.removeById(id);if (flag) {return R.ok();} else {return R.error();}
}

整合swagger

在父工程下创建一个子模块common,作为公共模块,在common模块下创建一个子模块

加上相关依赖

<!--swagger-->
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><scope>provided </scope>
</dependency>
<dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><scope>provided </scope>
</dependency>
@Configuration //配置类
@EnableSwagger2    //swagger注解
public class SwaggerConfig {@Beanpublic Docket webApiConfig(){return new Docket(DocumentationType.SWAGGER_2).groupName("webApi").apiInfo(webApiInfo()).select()
//                .paths(Predicates.not(PathSelectors.regex("/admin/.*"))).paths(Predicates.not(PathSelectors.regex("/error.*"))).build();}private ApiInfo webApiInfo(){return new ApiInfoBuilder().title("网站-课程中心API文档").description("本文档描述了课程中心微服务接口定义").version("1.0").contact(new Contact("java", "http://atguigu.com", "1123@qq.com")).build();}
}

在service模块中引入公共模块的依赖

<dependency><groupId>com.hxp</groupId><artifactId>service_base</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>

为了让service模块能扫描到Swagger,需要在主启动类上加@ComponentScan注解

@SpringBootApplication
@ComponentScan(basePackages = {"com.hxp"}) //为了能扫描到其他模块
public class EduApplication {public static void main(String[] args) {SpringApplication.run(EduApplication.class, args);}
}

统一结果数据返回

项目中我们会将响应封装成json返回,一般我们会将所有接口的数据格式统一,使前端对数据的操作更一致、轻松。

在common模块下创建子模块common-utils

(1)创建interface,定义数据返回状态码

public interface ResultCode {public static Integer SUCCESS = 20000; //成功public static Integer ERROR = 20001; //失败
}

(2)定义返回格式

@Data
public class R {@ApiModelProperty(value = "是否成功")private Boolean success;@ApiModelProperty(value = "返回码")private Integer code;@ApiModelProperty(value = "返回消息")private String message;@ApiModelProperty(value = "返回数据")private Map<String, Object> data = new HashMap<String, Object>();//将构造方法私有,外部不能随便newprivate R() {}//成功的静态方法public static R ok() {R r = new R();r.setSuccess(true);r.setCode(ResultCode.SUCCESS);r.setMessage("成功");return r;}//失败的静态方法public static R error() {R r = new R();r.setSuccess(false);r.setCode(ResultCode.ERROR);r.setMessage("失败");return r;}public R success(Boolean success) {this.setSuccess(success);return this;}public R message(String message) {this.setMessage(message);return this;}public R code(Integer code) {this.setCode(code);return this;}public R data(String key, Object value) {this.data.put(key, value);return this;}public R data(Map<String,Object> map) {this.setData(map);return this;}
}

(3)在service中引入依赖

<dependency><groupId>com.hxp</groupId><artifactId>common_utils</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>

分页查询讲师

//分页查询讲师的方法
@GetMapping("pageTeacher/{current}/{limit}")
public R pageListTeacher(@PathVariable long current, @PathVariable long limit) {Page<EduTeacher> pageTeacher = new Page<>(current, limit);//把分页所有数据封装到pageTeacher对象里面teacherService.page(pageTeacher, null);long total = pageTeacher.getTotal(); //总记录数List<EduTeacher> records = pageTeacher.getRecords(); //数据list集合return R.ok().data("total",total).data("records", records);
}

多条件查询带分页

(1)把条件封装到对象(VO对象)里面,把对象传递到接口中

@Data
public class TeacherQuery {@ApiModelProperty(value = "教师名称,模糊查询")private String name;@ApiModelProperty(value = "头衔 1高级讲师 2首席讲师")private Integer level;@ApiModelProperty(value = "查询开始时间", example = "2019-01-01 10:10:10")private String begin; //注意,这里使用的是String类型,前端传过来的数据不需要进行类型转换@ApiModelProperty(value = "查询结束时间", example = "2019-12-01 10:10:10")private String end;
}

(2)编写接口,根据条件值进行判断,拼接条件
注意:参数加了@RequestBody,不能用GET方式提交数据

//条件查询带分页
@PostMapping("pageTeacherCondition/{current}/{limit}")
public R pageTeacherCondition(@PathVariable long current, @PathVariable long limit,@RequestBody(required = false) TeacherQuery teacherQuery) {Page<EduTeacher> pageTeacher = new Page<>(current,limit);QueryWrapper<EduTeacher> wrapper = new QueryWrapper<>();//多条件组合查询String name = teacherQuery.getName();Integer level = teacherQuery.getLevel();String begin = teacherQuery.getBegin();String end = teacherQuery.getEnd();//判断条件值是否为空,如果不为空拼接条件if (!StringUtils.isEmpty(name)) {wrapper.like("name", name);}if (!StringUtils.isEmpty(level)) {wrapper.eq("level", level);}if (!StringUtils.isEmpty(begin)) {wrapper.ge("gmt_create", begin); //大于等于}if (!StringUtils.isEmpty(end)) {wrapper.le("gmt_create", end); //小于等于}//排序wrapper.orderByDesc("gmt_create");//调用方法实现条件查询分页teacherService.page(pageTeacher, wrapper);long total = pageTeacher.getTotal();List<EduTeacher> records = pageTeacher.getRecords();return R.ok().data("total", total).data("records", records);
}

新增讲师

(1)在实体类的属性上加上注解

@TableField(fill = FieldFill.INSERT)
private Date gmtCreate;@TableField(fill = FieldFill.INSERT_UPDATE)
private Date gmtModified;

(2)创建自动填充类

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {@Overridepublic void insertFill(MetaObject metaObject) {this.setFieldValByName("gmtCreate", new Date(),metaObject);this.setFieldValByName("gmtModified", new Date(), metaObject);}@Overridepublic void updateFill(MetaObject metaObject) {this.setFieldValByName("gmtModified", new Date(), metaObject);}
}

(3)编写controller

//添加讲师
@PostMapping("addTeacher")
public R addTeacher(@RequestBody EduTeacher eduTeacher) {boolean save = teacherService.save(eduTeacher);if (save) {return R.ok();} else {return R.error();}
}

统一异常处理

(1)全局异常处理
在公共模块service-base中创建统一异常处理器

@ControllerAdvice
public class GlobalExceptionHandler {//指定出现什么异常执行这个方法@ExceptionHandler(Exception.class)@ResponseBody  //为了能返回数据public R error(Exception e) {e.printStackTrace();return R.error().message("执行了全局异常处理...");}
}

(2)特殊异常处理

@ExceptionHandler(ArithmeticException.class)
@ResponseBody  //为了能返回数据
public R error(ArithmeticException e) {e.printStackTrace();return R.error().message("执行了ArithmeticException异常处理...");
}

(3)自定义异常处理
创建自定义异常类继承RuntimeException

@Data
@AllArgsConstructor //有参构造
@NoArgsConstructor  //无参构造
public class GuliException extends RuntimeException{private Integer code;private String msg;
}

在统一异常类添加规则

@ControllerAdvice
public class GlobalExceptionHandler {//自定义异常@ExceptionHandler(GuliException.class)@ResponseBodypublic R error(GuliException e) {e.printStackTrace();return R.error().code(e.getCode()).message(e.getMsg());}
}

使用:在可能发送异常的地方加上 try-catch

try{int a = 1/0;
} catch (Exception e) {throw new GuliException(20001, "执行了自定义异常处理...");
}

日志

日志记录器(Logger)的行为是分等级的。
分为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL。
默认情况下,springboot从控制台打印出来的日志级别只有INFO及以上级别,可以设置日志级别。

# 设置日志级别
logging.level.root=WARN

把日志不仅输出到控制台,也可以输出到文件中,使用日志工具:Logback日志工具
第一步,删除application.properties日志配置,不然会有冲突
第二步,在resource下创建logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration  scan="true" scanPeriod="10 seconds"><!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 --><!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true --><!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 --><!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 --><contextName>logback</contextName><!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 --><property name="log.path" value="D:/guli_1010/edu" /><!-- 彩色日志 --><!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 --><!-- magenta:洋红 --><!-- boldMagenta:粗红--><!-- cyan:青色 --><!-- white:白色 --><!-- magenta:洋红 --><property name="CONSOLE_LOG_PATTERN"value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/><!--输出到控制台--><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息--><!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 --><filter class="ch.qos.logback.classic.filter.ThresholdFilter"><level>INFO</level></filter><encoder><Pattern>${CONSOLE_LOG_PATTERN}</Pattern><!-- 设置字符集 --><charset>UTF-8</charset></encoder></appender><!--输出到文件--><!-- 时间滚动输出 level为 INFO 日志 --><appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 正在记录的日志文件的路径及文件名 --><file>${log.path}/log_info.log</file><!--日志文件输出格式--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern><charset>UTF-8</charset></encoder><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 每天日志归档路径以及格式 --><fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文件保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文件只记录info级别的 --><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>INFO</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- 时间滚动输出 level为 WARN 日志 --><appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 正在记录的日志文件的路径及文件名 --><file>${log.path}/log_warn.log</file><!--日志文件输出格式--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern><charset>UTF-8</charset> <!-- 此处设置字符集 --></encoder><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文件保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文件只记录warn级别的 --><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>warn</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- 时间滚动输出 level为 ERROR 日志 --><appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 正在记录的日志文件的路径及文件名 --><file>${log.path}/log_error.log</file><!--日志文件输出格式--><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern><charset>UTF-8</charset> <!-- 此处设置字符集 --></encoder><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文件保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文件只记录ERROR级别的 --><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>ERROR</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!--<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。<logger>仅有一个name属性,一个可选的level和一个可选的addtivity属性。name:用来指定受此logger约束的某一个包或者具体的某一个类。level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,如果未设置此属性,那么当前logger将会继承上级的级别。--><!--使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:--><!--开发环境:打印控制台--><springProfile name="dev"><!--可以输出项目中的debug日志,包括mybatis的sql日志--><logger name="com.guli" level="INFO" /><!--root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG可以包含零个或多个appender元素。--><root level="INFO"><appender-ref ref="CONSOLE" /><appender-ref ref="INFO_FILE" /><appender-ref ref="WARN_FILE" /><appender-ref ref="ERROR_FILE" /></root></springProfile><!--生产环境:输出到文件--><springProfile name="pro"><root level="INFO"><appender-ref ref="CONSOLE" /><appender-ref ref="DEBUG_FILE" /><appender-ref ref="INFO_FILE" /><appender-ref ref="ERROR_FILE" /><appender-ref ref="WARN_FILE" /></root></springProfile></configuration>

将错误日志输出到文件
在GlobalExceptionHandler.java中,添加注解@Slf4j。

异常输出语句:log.error(e.getMessage());

登录功能

进行登录调用两个方法,login登录操作方法和info登录之后获取用户信息的方法。

@RestController
@RequestMapping("/eduservice/user")
public class EduLoginController {//登录@PostMapping("login")public R login() {return R.ok().data("token","admin");}//用户信息@GetMapping("info")public R info() {return R.ok().data("roles","[admin]").data("name","admin").data("avatar","https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif");}
}

可能产生跨域问题:通过一个地址去访问另外一个地址,这个过程中如果 “协议 ip 端口” 这三个有任何一个不一样,就会产生跨域问题。

解决:
(1)在后端接口controller添加注解:@CrossOrigin
(2)使用网关解决

上传讲师头像

(1)新建一个子模块

(2)引入依赖

<!--阿里云oss依赖-->
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!--日期工具栏依赖-->
<dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId>
</dependency>

(3)创建配置文件

server.port=8002spring.application.name=service-ossspring.profiles.active=dev#阿里云OSS
aliyun.oss.file.endpoint=oss-cn-hangzhou.aliyuncs.com
aliyun.oss.file.keyid=LTAI5tEz9ABsH5qFZsxXg5K2
aliyun.oss.file.keysecret=MHJngccYekVjxzp0YtKQU4HTJsr5Ty
#bucket可以在控制台创建,也可以使用java创建
aliyun.oss.file.bucketname=hxp-bucket

(4)遇到的问题:创建了启动类,启动报错了。
原因:依赖中引入了数据库相关的依赖,启动时会去找数据库配置,但这个模块不需要数据库也没有配置数据库,只是做上传到oss功能。
解决:1、添加上数据库配置;2、在启动类添加属性,默认不去加载数据库配置。

(5)常量类编写,让其他类能获取到OSS相关的常量。

//当项目已启动,spring加载之后,执行接口一个方法
@Component
public class ConstantPropertiesUtils implements InitializingBean {@Value("${aliyun.oss.file.endpoint}")private String endpoint;@Value("${aliyun.oss.file.keyid}")private String keyId;@Value("${aliyun.oss.file.keysecret}")private String keySecret;@Value("${aliyun.oss.file.bucketname}")private String bucketName;//定义公开静态常量public static String END_POINT;public static String ACCESS_KEY_ID;public static String ACCESS_KEY_SECRET;public static String BUCKET_NAME;@Overridepublic void afterPropertiesSet() throws Exception {END_POINT = endpoint;ACCESS_KEY_ID = keyId;ACCESS_KEY_SECRET = keySecret;BUCKET_NAME = bucketName;}
}

(6)编写接口

@RestController
@RequestMapping("/eduoss/fileoss")
public class OssController {@Autowiredprivate OssService ossService;@PostMappingpublic R uploadOssFile(MultipartFile file) {//MultipartFile 获取上传文件//返回上传到oss的路径String url = ossService.uploadFileAvatar(file);return R.ok().data("url",url);}
}
@Service
public class OssServiceImpl implements OssService {//上传头像到oss@Overridepublic String uploadFileAvatar(MultipartFile file) {//工具类获取值String endpoint = ConstantPropertiesUtils.END_POINT;String accessKeyId = ConstantPropertiesUtils.ACCESS_KEY_ID;String accessKeySecret = ConstantPropertiesUtils.ACCESS_KEY_SECRET;String bucketName = ConstantPropertiesUtils.BUCKET_NAME;try {//创建OSS实例OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);//获取上传文件输入流InputStream inputStream = file.getInputStream();//获取文件名称String fileName = file.getOriginalFilename();//1. 在文件名称中添加随机唯一的值String uuid = UUID.randomUUID().toString().replaceAll("-", "");fileName = uuid + fileName;//2. 把文件按照日期进行分类,获取当前日期String datePath = new DateTime().toString("yyyy/MM/dd");fileName = datePath + "/" + fileName;//调用oss方法实现上传//第一个参数 Bucket名称//第二个参数 上传到oss文件的文件名//第三个参数 上传文件输入流ossClient.putObject(bucketName, fileName, inputStream);//关闭OSSClientossClient.shutdown();//把上传之后文件路径返回//需要把上传到阿里云oss路径手动拼接出来String url = "https://"+bucketName+"."+endpoint+"/"+fileName;return url;} catch (Exception e) {e.printStackTrace();return null;}}
}

课程分类管理模块

环境搭建

1. 创建数据库表

CREATE TABLE `edu_subject` (`id` char(19) NOT NULL COMMENT '课程类别ID',`title` varchar(10) NOT NULL COMMENT '类别名称',`parent_id` char(19) NOT NULL DEFAULT '0' COMMENT '父ID',`sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序字段',`gmt_create` datetime NOT NULL COMMENT '创建时间',`gmt_modified` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='课程科目';

2. 引入easyExcel的依赖

<dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>2.1.1</version>
</dependency>
<!--xls-->
<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>3.1.7</version>
</dependency>
<!--xlsx-->
<dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>3.1.7</version>
</dependency>

3. 用代码生成器生成代码

添加课程分类

1. 创建excel实体类

@Data
public class SubjectData {@ExcelProperty(index = 0)private String oneSubjectName;@ExcelProperty(index = 1)private String twoSubjectName;
}

2. 编写excel的监听器
注意:监听器无法交给spring管理,所以不能用@Autowired注入对象,要用构造方法传递对象进行使用。

public class SubjectExcelListener extends AnalysisEventListener<SubjectData> {//因为SubjectExcelListener不能交给Spring进行管理,需要自己new,不能注入其他对象,不能实现数据库操作//使用构造器注入public EduSubjectService subjectService;public SubjectExcelListener() {}public SubjectExcelListener(EduSubjectService subjectService) {this.subjectService = subjectService;}@Overridepublic void invoke(SubjectData subjectData, AnalysisContext analysisContext) {if (subjectData == null) {throw new GuliException(20001, "文件数据为空");}//一行一行读取,每次读取有两个值,第一个值一级分类,第二个值二级分类EduSubject existOneSubject = this.existOneSubject(subjectService,subjectData.getOneSubjectName());if (existOneSubject == null) {  //没有相同一级分类,进行添加existOneSubject = new EduSubject();existOneSubject.setParentId("0");existOneSubject.setTitle(subjectData.getOneSubjectName()); //一级分类名称subjectService.save(existOneSubject);}//获取一级分类id值String pid = existOneSubject.getId();//添加二级分类//判断二级分类是否重复EduSubject existTwoSubject = this.existTwoSubject(subjectService, subjectData.getTwoSubjectName(),pid);if (existTwoSubject == null) {existTwoSubject = new EduSubject();existTwoSubject.setParentId(pid);existTwoSubject.setTitle(subjectData.getTwoSubjectName()); //二级分类名称subjectService.save(existTwoSubject);}}//判断一级分类不能重复添加private EduSubject existOneSubject(EduSubjectService subjectService, String name) {QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();wrapper.eq("title", name);wrapper.eq("parent_id","0");    //一级分类的parent_id为0EduSubject oneSubject = subjectService.getOne(wrapper);return oneSubject;}//判断二级分类不能重复添加private EduSubject existTwoSubject(EduSubjectService subjectService, String name, String pid) {QueryWrapper<EduSubject> wrapper = new QueryWrapper<>();wrapper.eq("title", name);wrapper.eq("parent_id",pid);    //一级分类的parent_id为0EduSubject twoSubject = subjectService.getOne(wrapper);return twoSubject;}@Overridepublic void doAfterAllAnalysed(AnalysisContext analysisContext) {}
}

3. 编写接口

@RestController
@RequestMapping("/eduservice/subject")
public class EduSubjectController {@Autowiredprivate EduSubjectService subjectService;//添加课程分类@PostMapping("addSubject")public R addSubject(MultipartFile file) {subjectService.saveSubject(file,subjectService);return R.ok();}
}
@Service
public class EduSubjectServiceImpl extends ServiceImpl<EduSubjectMapper, EduSubject> implements EduSubjectService {//添加课程分类@Overridepublic void saveSubject(MultipartFile file,EduSubjectService subjectService) {try {//文件输入流InputStream in = file.getInputStream();//调用方法进行读取EasyExcel.read(in, SubjectData.class, new SubjectExcelListener(subjectService)).sheet().doRead();} catch (Exception e) {}}
}

课程分类显示


1. 针对返回数据创建对应的实体类(一级分类和二级分类),给两个实体类建立关系

@Data
public class OneSubject {private String id;private String title;private List<TwoSubject> children = new ArrayList<>();
}
@Data
public class TwoSubject {private String id;private String title;
}

2. 编写接口

//课程分类列表展示(树形)
@GetMapping("getAllSubject")
public R getAllSubject() {List<OneSubject> list = subjectService.getAllOneTwoSubject();return R.ok().data("list",list);
}
//课程分类列表(树形)
@Override
public List<OneSubject> getAllOneTwoSubject() {//1 查询所有一级分类 parentId=0QueryWrapper<EduSubject> wrapperOne = new QueryWrapper<>();wrapperOne.eq("parent_id", "0");List<EduSubject> oneSubjectList = baseMapper.selectList(wrapperOne);//2 查询所有二级分类 parentId != 0QueryWrapper<EduSubject> wrapperTwo = new QueryWrapper<>();wrapperTwo.ne("parent_id", "0"); //不等于List<EduSubject> twoSubjectList = baseMapper.selectList(wrapperTwo);//创建list集合,用于存储最终封装的数据List<OneSubject> finalSubjectList = new ArrayList<>();//3 封装一级分类for (int i = 0; i < oneSubjectList.size(); i++) {//得到oneSubjectList的每个eduSubject对象EduSubject eduSubject = oneSubjectList.get(i);//把eduSubject里面值获取出来,放到OneSubject对象里面OneSubject oneSubject = new OneSubject();
//            oneSubject.setId(eduSubject.getId());
//            oneSubject.setTitle(eduSubject.getTitle());BeanUtils.copyProperties(eduSubject,oneSubject); //用BeanUtils替换上面两句//多个OneSubject放到finalSubjectList里面finalSubjectList.add(oneSubject);//4 封装二级分类,在一级分类循环遍历查询所有的二级分类List<TwoSubject> twoFinalSubjectList = new ArrayList<>();for (int j = 0; j < twoSubjectList.size(); j++) {//获取每个二级分类EduSubject tSubject = twoSubjectList.get(j);//判断二级分类parentId和一级分类id是否一样if (tSubject.getParentId().equals(eduSubject.getId())) {TwoSubject twoSubject = new TwoSubject();BeanUtils.copyProperties(tSubject, twoSubject);twoFinalSubjectList.add(twoSubject);}}//把一级下面所有二级分类放到一级分类中oneSubject.setChildren(twoFinalSubjectList);}return finalSubjectList;
}

课程管理模块

环境搭建

1. 创建数据库表

CREATE TABLE `edu_chapter` (`id` char(19) NOT NULL COMMENT '章节ID',`course_id` char(19) NOT NULL COMMENT '课程ID',`title` varchar(50) NOT NULL COMMENT '章节名称',`sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '显示排序',`gmt_create` datetime NOT NULL COMMENT '创建时间',`gmt_modified` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_course_id` (`course_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='课程章节';
CREATE TABLE `edu_course` (`id` char(19) NOT NULL COMMENT '课程ID',`teacher_id` char(19) NOT NULL COMMENT '课程讲师ID',`subject_id` char(19) NOT NULL COMMENT '课程专业ID',`subject_parent_id` char(19) NOT NULL DEFAULT '' COMMENT '课程专业父级ID',`title` varchar(50) NOT NULL COMMENT '课程标题',`price` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '课程销售价格,设置为0则可免费观看',`lesson_num` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '总课时',`cover` varchar(255) CHARACTER SET utf8 NOT NULL COMMENT '课程封面图片路径',`buy_count` bigint(10) unsigned NOT NULL DEFAULT '0' COMMENT '销售数量',`view_count` bigint(10) unsigned NOT NULL DEFAULT '0' COMMENT '浏览数量',`version` bigint(20) unsigned NOT NULL DEFAULT '1' COMMENT '乐观锁',`status` varchar(10) NOT NULL DEFAULT 'Draft' COMMENT '课程状态 Draft未发布  Normal已发布',`is_deleted` tinyint(3) DEFAULT NULL COMMENT '逻辑删除 1(true)已删除, 0(false)未删除',`gmt_create` datetime NOT NULL COMMENT '创建时间',`gmt_modified` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_title` (`title`),KEY `idx_subject_id` (`subject_id`),KEY `idx_teacher_id` (`teacher_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='课程基本信息';
CREATE TABLE `edu_video` (`id` char(19) NOT NULL COMMENT '视频ID',`course_id` char(19) NOT NULL COMMENT '课程ID',`chapter_id` char(19) NOT NULL COMMENT '章节ID',`title` varchar(50) NOT NULL COMMENT '节点名称',`video_source_id` varchar(100) DEFAULT NULL COMMENT '云端视频资源',`video_original_name` varchar(100) DEFAULT NULL COMMENT '原始文件名称',`sort` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '排序字段',`play_count` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '播放次数',`is_free` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否可以试听:0收费 1免费',`duration` float NOT NULL DEFAULT '0' COMMENT '视频时长(秒)',`status` varchar(20) NOT NULL DEFAULT 'Empty' COMMENT 'Empty未上传 Transcoding转码中  Normal正常',`size` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '视频源文件大小(字节)',`version` bigint(20) unsigned NOT NULL DEFAULT '1' COMMENT '乐观锁',`gmt_create` datetime NOT NULL COMMENT '创建时间',`gmt_modified` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),KEY `idx_course_id` (`course_id`),KEY `idx_chapter_id` (`chapter_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT COMMENT='课程视频';
CREATE TABLE `edu_course_description` (`id` char(19) NOT NULL COMMENT '课程ID',`description` text COMMENT '课程简介',`gmt_create` datetime NOT NULL COMMENT '创建时间',`gmt_modified` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程简介';

还要用到 edu_teacher 和 edu_subject 表。
表与表之间的关系:

2. 代码生成器生成代码

添加课程



1. 创建VO类封装表单提交的数据

@Data
public class CourseInfoVo {@ApiModelProperty(value = "课程ID")private String id;@ApiModelProperty(value = "课程讲师ID")private String teacherId;@ApiModelProperty(value = "课程专业ID")private String subjectId;@ApiModelProperty(value = "课程专业父级ID")private String subjectParentId;@ApiModelProperty(value = "课程标题")private String title;@ApiModelProperty(value = "课程销售价格,设置为0则可免费观看")private BigDecimal price;@ApiModelProperty(value = "总课时")private Integer lessonNum;@ApiModelProperty(value = "课程封面图片路径")private String cover;@ApiModelProperty(value = "课程简介")private String description;
}

2. 编写controller和service部分
注意:课程和描述是一对一的关系,添加之后,id值一样。
修改 EduCourseDescription 课程描述类的实体类id的类型为手动设置。

//添加课程基本信息的方法
@PostMapping("addCourseInfo")
public R addCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {String id= courseService.saveCourseInfo(courseInfoVo);return R.ok().data("courseId", id);
}
//课程描述注入
@Autowired
private EduCourseDescriptionService courseDescriptionService;//添加课程基本信息
@Override
public String saveCourseInfo(CourseInfoVo courseInfoVo) {//1 向课程表添加课程基本信息EduCourse eduCourse = new EduCourse();BeanUtils.copyProperties(courseInfoVo, eduCourse);int insert = baseMapper.insert(eduCourse);if (insert <= 0) {throw new GuliException(20001, "添加课程信息失败");}//2 向课程简介表添加课程简介EduCourseDescription courseDescription = new EduCourseDescription();courseDescription.setDescription(courseInfoVo.getDescription());//获取添加之后课程id,然后设置进描述id,让它们产生一对一的关系String cid = eduCourse.getId();courseDescription.setId(cid);courseDescriptionService.save(courseDescription);return cid;
}

课程大纲显示(章节和小节)


1. 创建两个实体类,章节和小节,在章节实体类使用list表示小节

@Data
public class ChapterVo {private String id;private String title;//表示小节private List<VideoVo> children = new ArrayList<>();
}
@Data
public class VideoVo {private String id;private String title;private String videoSourceId; //视频id
}

2. 编写接口

@RestController
@RequestMapping("/eduservice/chapter")
public class EduChapterController {@Autowiredprivate EduChapterService chapterService;//课程大纲列表,根据课程id查询@GetMapping("getChapterVideo/{courseId}")public R getChapterVideo(@PathVariable String courseId) {List<ChapterVo> list = chapterService.getChapterVideoByCourseId(courseId);return R.ok().data("allChapterVideo", list);}
}
@Service
public class EduChapterServiceImpl extends ServiceImpl<EduChapterMapper, EduChapter> implements EduChapterService {@Autowiredprivate EduVideoService videoService;   //注入小节service//课程大纲列表@Overridepublic List<ChapterVo> getChapterVideoByCourseId(String courseId) {//1 根据课程id查询课程里面所有的章节QueryWrapper<EduChapter> wrapperChapter = new QueryWrapper<>();wrapperChapter.eq("course_id", courseId);List<EduChapter> eduChapterList = baseMapper.selectList(wrapperChapter);//2 根据课程id查询课程里面所有的小节QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();wrapperVideo.eq("course_id", courseId);List<EduVideo> eduVideoList = videoService.list(wrapperVideo);//创建list集合,用于最终封装数据List<ChapterVo> finalList = new ArrayList<>();//3 遍历查询章节list集合进行封装for (int i = 0; i < eduChapterList.size(); i++) {//每个章节EduChapter eduChapter = eduChapterList.get(i);//将eduChapter对象复制到ChapterVo中ChapterVo chapterVo = new ChapterVo();BeanUtils.copyProperties(eduChapter, chapterVo);//把chapterVo放到最终的集合中finalList.add(chapterVo);//创建集合,用于封装章节的小节List<VideoVo> videoVoList = new ArrayList<>();//4 遍历查询小节list集合,进行封装for (int j = 0; j < eduVideoList.size(); j++) {EduVideo eduVideo = eduVideoList.get(j);//判断:小节里面chapterId和章节里面id是否一样if (eduVideo.getChapterId().equals(eduChapter.getId())) {//封装VideoVo videoVo = new VideoVo();BeanUtils.copyProperties(eduVideo, videoVo);videoVoList.add(videoVo);}}//将封装之后小节的list,设置到章节对象中chapterVo.setChildren(videoVoList);}return finalList;}
}

修改课程基本信息

在课程大纲页面,点击“上一步”,回到第一步,将数据回显在页面中,修改信息内容,然后保存。
1. 根据课程id查询课程基本信息接口

//根据课程id查询课程基本信息
@GetMapping("getCourseInfo/{courseId}")
public R getCourseInfo(@PathVariable String courseId) {CourseInfoVo courseInfoVo = courseService.getCourseInfo(courseId);return R.ok().data("courseInfoVo", courseInfoVo);
}
//根据课程id查询课程基本信息
@Override
public CourseInfoVo getCourseInfo(String courseId) {//1 查询课程表EduCourse eduCourse = baseMapper.selectById(courseId);CourseInfoVo courseInfoVo = new CourseInfoVo();BeanUtils.copyProperties(eduCourse, courseInfoVo);//2 查询描述表EduCourseDescription courseDescription = courseDescriptionService.getById(courseId);courseInfoVo.setDescription(courseDescription.getDescription());return courseInfoVo;
}

2. 修改课程信息接口

//修改课程信息
@PostMapping("updateCourseInfo")
public R updateCourseInfo(@RequestBody CourseInfoVo courseInfoVo) {courseService.updateCourseInfo(courseInfoVo);return R.ok();
}
@Override
public void updateCourseInfo(CourseInfoVo courseInfoVo) {//1 修改课程表EduCourse eduCourse = new EduCourse();BeanUtils.copyProperties(courseInfoVo, eduCourse);int update = baseMapper.updateById(eduCourse);if (update == 0) {throw new GuliException(20001, "修改课程信息失败");}//2 修改描述表EduCourseDescription description = new EduCourseDescription();description.setId(courseInfoVo.getId());description.setDescription(courseInfoVo.getDescription());courseDescriptionService.updateById(description);
}

章节的添加修改删除

1. 添加和修改

//根据章节id查询
@GetMapping("getChapterInfo/{chapterId}")
public R getCharpterInfo(@PathVariable String chapterId) {EduChapter eduChapter = chapterService.getById(chapterId);return R.ok().data("chapter", eduChapter);
}//修改章节
@PostMapping("updateChapter")
public R updateChapter(@RequestBody EduChapter eduChapter) {chapterService.updateById(eduChapter);return R.ok();
}

2. 删除
如果删除的章节下面有小节,不让删除

//删除的方法
@DeleteMapping("{chapterId}")
public R deleteChapter(@PathVariable String chapterId) {boolean flag = chapterService.deleteChapter(chapterId);if (flag) {return R.ok();} else {return R.error();}
}
//删除章节的方法
@Override
public boolean deleteChapter(String chapterId) {//根据chapterId章节id,查询小节表,如果查询出小节数据,不进行删除QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();wrapper.eq("chapter_id", chapterId);int count = videoService.count(wrapper);if (count > 0) {throw new GuliException(20001, "不能删除");} else {//删除章节int result = baseMapper.deleteById(chapterId);return result>0;}
}

添加和删除小节

其中删除小节需要删除小节中对应的视频,这里暂时还没写视频相关的接口,所以删除小节直接调用removeById方法删除就行。

@RestController
@RequestMapping("/eduservice/video")
public class EduVideoController {@Autowiredprivate EduVideoService videoService;//添加小节@PostMapping("addVideo")public R addVideo(@RequestBody EduVideo eduVideo) {videoService.save(eduVideo);return R.ok();}//删除小节// 删除小节的时候,同时把里面视频删掉@DeleteMapping("{id}")public R deleteVideo(@PathVariable String id) {//删除小节videoService.removeById(id);return R.ok();}
}

课程信息确认和发布课程


通过页面可以看出,需要查询的数据有:课程名称、课程价格、课程简介、课程分类、课程讲师、课程封面。
而这些数据分布在多张表中,所以建议使用sql连表查询,查询出这些数据进行封装。

1. 定义课程发布信息的VO对象

@Data
public class CoursePublishVo {private String id;private String title;private String cover;private String lessonNum;private String subjectLevelOne;private String subjectLevelTwo;private String teacherName;private String price;
}

2. 在mapper中定义查询的方法,并且在xml中实现

@Mapper
public interface EduCourseMapper extends BaseMapper<EduCourse> {public CoursePublishVo getPublishCourseInfo(String courseId);
}
<select id="getPublishCourseInfo" resultType="com.hxp.eduservice.entity.vo.CoursePublishVo">SELECT ec.id, ec.title, ec.price, ec.lesson_num AS lessonNum, ec.cover,et.name AS teacherName,es1.title AS subjectLevelOne,es2.title AS subjectLevelTwoFROM edu_course ec LEFT JOIN edu_course_description ecd ON ec.id=ecd.idLEFT JOIN edu_teacher et ON ec.teacher_id=et.idLEFT JOIN edu_subject es1 ON ec.subject_parent_id=es1.idLEFT JOIN edu_subject es2 ON ec.subject_id=es2.idWHERE ec.id=#{courseId};
</select>

3. 编写接口

//根据课程id查询课程确认信息
@GetMapping("getPublishCourseInfo/{id}")
public R getPublishCourseInfo(@PathVariable String id) {CoursePublishVo coursePublishVo = courseService.publishCourseInfo(id);return R.ok().data("publishCourse", coursePublishVo);
}
@Override
public CoursePublishVo publishCourseInfo(String id) {CoursePublishVo publishCourseInfo = baseMapper.getPublishCourseInfo(id);return publishCourseInfo;
}

4. 可能出现的问题
可能出现如下错误,原因是没有找到xml文件。maven加载的时候,把java文件夹里面的.java类型文件进行编译,如果有其他类型文件,不会加载。

解决:通过配置实现。
(1)pom.xml

<build><resources><resource><directory>src/main/java</directory><includes><include>**/*.xml</include></includes><filtering>false</filtering></resource></resources>
</build>

(2)application.properties

# 配置mapper.xml文件的路径
mybatis-plus.mapper-locations=classpath:com/hxp/eduservice/mapper/xml/*.xml

5. 最终发布
做完前面的操作,已经把课程的信息存到数据库了,但此时用户在前台还看不到课程,所以需要实现最终的发布。

实现:点击“发布课程”,将这个字段的值改成“Normal”。

//课程最终发布,修改课程状态
@PostMapping("publishCourse/{id}")
public R publishCourse(@PathVariable String id) {EduCourse eduCourse = new EduCourse();eduCourse.setId(id);eduCourse.setStatus("Normal");  //设置课程发布状态courseService.updateById(eduCourse);return R.ok();
}

课程列表显示和删除课程

1. 课程列表

@Autowired
private EduCourseService courseService;@GetMapping
public R getCourseList() {List<EduCourse> list = courseService.list(null);return R.ok().data("list", list);
}

2. 删除课程
课程里面有:课程描述、章节、小节、视频。
删除课程把里面的内容都删除,按照顺序进行删除,视频 --> 小节 --> 章节 --> 描述 --> 课程本身。

//删除课程
@DeleteMapping("{courseId}")
public R deleteCourse(@PathVariable String courseId) {courseService.removeCourse(courseId);return R.ok();
}
//删除课程
@Override
public void removeCourse(String courseId) {//1 根据课程id删除小节eduVideoService.removeVideoByCourseId(courseId);//2 根据课程id删除章节chapterService.removeChapterByCourseId(courseId);//3 根据课程id删除描述courseDescriptionService.removeById(courseId);//4 根据课程id删除课程本身int result = baseMapper.deleteById(courseId);if (result == 0) {throw new GuliException(20001, "删除失败");}
}

在小节的Service中编写删除小节的方法。

@Service
public class EduVideoServiceImpl extends ServiceImpl<EduVideoMapper, EduVideo> implements EduVideoService {@Autowiredprivate VodClient vodClient;//1 根据课程id删除小节// 删除小节,同时删除对应视频文件@Overridepublic void removeVideoByCourseId(String courseId) {//TODO 这里删除视频还未写//删除小节QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();wrapper.eq("course_id", courseId);baseMapper.delete(wrapper);}
}

在章节的service中编写删除章节的方法

//根据课程id删除章节
@Override
public void removeChapterByCourseId(String courseId) {QueryWrapper<EduChapter> wrapper = new QueryWrapper<>();wrapper.eq("course_id", courseId);baseMapper.delete(wrapper);
}

添加小节上传视频



1. 搭建环境,引入依赖

application.properties

server.port=8003
spring.application.name=service-vod
spring.profiles.active=dev#阿里云视频点播
aliyun.vod.file.keyid=LTAI5tEz9ABsH5qFZsxXg5K2
aliyun.vod.file.keysecret=MHJngccYekVjxzp0YtKQU4HTJsr5Ty
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@ComponentScan(basePackages = {"com.hxp"})
public class VodApplication {public static void main(String[] args) {SpringApplication.run(VodApplication.class, args);}
}
<dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId><version>4.3.3</version>
</dependency>
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.1.0</version>
</dependency>
<dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-vod</artifactId><version>2.15.5</version>
</dependency>
<dependency><groupId>com.aliyun</groupId><artifactId>aliyun-sdk-vod-upload</artifactId><version>1.4.11</version>
</dependency>
<dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId><version>2.8.2</version>
</dependency>

2. 编写常量类

@Component
public class ConstantVodUtils implements InitializingBean {@Value("${aliyun.vod.file.keyid}")private String keyid;@Value("${aliyun.vod.file.keysecret}")private String keysecret;public static String ACCESS_KEY_SECRET;public static String ACCESS_KEY_ID;@Overridepublic void afterPropertiesSet() throws Exception {ACCESS_KEY_SECRET = keysecret;ACCESS_KEY_ID = keyid;}
}

3. 编写接口

@RestController
@RequestMapping("/eduvod/video")
public class VodController {@Autowiredprivate VodService vodService;@PostMapping("uploadAlyiVideo")public R uploadAlyiVideo(MultipartFile file) {String videoId = vodService.uploadVideoAly(file);return R.ok().data("videoId", videoId);}
}
@Override
public String uploadVideoAly(MultipartFile file) {try {//accessKeyId, accessKeySecret//fileName:上传文件原始名称// 01.03.09.mp4String fileName = file.getOriginalFilename();//title:上传之后显示名称,01.01.mp4 => 01.01String title = fileName.substring(0, fileName.lastIndexOf("."));//inputStream:上传文件输入流InputStream inputStream = file.getInputStream();UploadStreamRequest request = new UploadStreamRequest(ConstantVodUtils.ACCESS_KEY_ID,ConstantVodUtils.ACCESS_KEY_SECRET, title, fileName, inputStream);UploadVideoImpl uploader = new UploadVideoImpl();UploadStreamResponse response = uploader.uploadStream(request);String videoId = null;if (response.isSuccess()) {videoId = response.getVideoId();} else { //如果设置回调URL无效,不影响视频上传,可以返回VideoId同时会返回错误码。其他情况上传失败时,VideoId为空,此时需要根据返回错误码分析具体错误原因videoId = response.getVideoId();}return videoId;} catch (Exception e) {e.printStackTrace();return null;}
}

注意:用tomcat传输文件有大小限制,超出会报错。
解决:在application.properties中加上配置。

# 最大上传单个文件大小:默认1M
spring.servlet.multipart.max-file-size=1024MB
# 最大置总上传的数据大小:默认10M
spring.servlet.multipart.max-request-size=1024MB

视频删除

点击×,可以删除视频

1. 初始化视频对象

public class InitVodClient {public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {String regionId = "cn-shanghai";  // 点播服务接入区域DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);DefaultAcsClient client = new DefaultAcsClient(profile);return client;}
}

2. 删除阿里云视频接口

//根据视频id删除阿里云视频
@DeleteMapping("removeAlyVideo/{id}")
public R removeAlyVideo(@PathVariable String id) {try {//初始化对象DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);//创建删除视频request对象DeleteVideoRequest request = new DeleteVideoRequest();//向request设置视频idrequest.setVideoIds(id);//调用初始化对象的方法实现删除client.getAcsResponse(request);return R.ok();} catch (Exception e) {e.printStackTrace();throw new GuliException(20001, "删除视频失败");}
}

删除小节并删除掉小节下的视频

前提条件,把相互调用服务在nacos进行注册。

1. 创建包client,引入依赖,配置nacos

<!--服务注册-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>
<!--服务调用-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>

2. 在调用端service-edu服务启动类添加注解

3. 在调用端,创建interface,使用注解指定调用服务名称,定义调用的方法路径。
@FeignClient注解用于指定从哪个服务中调用功能,名称与被调用的服务名保持一致。
@DeleteMapping注解用于被调用的微服务进行地址映射,要写全路径。
@PathVariable注解要指定参数名称,否则报错。
@Component注解,防止在其他位置注入VodClient时idea报错。

写接口方法时,就把之前写的删除视频的方法复制过来,@DeleteMapping写全路径,@PathVariable加上名称即可。

@FeignClient("service-vod") //调用的服务名称
@Component
public interface VodClient {//根据视频id删除阿里云视频@DeleteMapping("/eduvod/video/removeAlyVideo/{id}")public R removeAlyVideo(@PathVariable("id") String id);
}

4. 在删除小节的方法中调用方法删除视频

@Autowired
private VodClient vodClient; //注入vodClient
//删除小节
// 删除小节的时候,同时把阿里云的视频删掉
@DeleteMapping("{id}")
public R deleteVideo(@PathVariable String id) {//根据小节id获取视频id,调用方法实现视频删除EduVideo eduVideo = videoService.getById(id);String videoSourceId = eduVideo.getVideoSourceId();//根据视频id,远程调用实现视频删除if (!StringUtils.isEmpty(videoSourceId)) {vodClient.removeAlyVideo(videoSourceId);}//删除小节videoService.removeById(id);return R.ok();
}

启动其他模块可能出现的问题:

原因是引入了nacos依赖,而在这个模块没有做相关的配置。解决:

删除课程删除视频

一个课程有多个章节,一个章节有多个小节,每个小节有视频。
1. 在service-vod创建接口,实现删除多个视频
这里用org.apache.commons.lang下的StringUtils的join()方法,将list集合转成字符串

//删除多个阿里云视频
@DeleteMapping("delete-batch")
public R deleteBatch(@RequestParam("videoIdList") List<String> videoIdList) {vodService.removeMoreAlyVideo(videoIdList);return R.ok();
}
//删除多个阿里云视频
@Override
public void removeMoreAlyVideo(List videoIdList) {try {//初始化对象DefaultAcsClient client = InitVodClient.initVodClient(ConstantVodUtils.ACCESS_KEY_ID, ConstantVodUtils.ACCESS_KEY_SECRET);//创建删除视频request对象DeleteVideoRequest request = new DeleteVideoRequest();//videoList值 [1,2,3] 转换成 1,2,3String videoIds = StringUtils.join(videoIdList.toArray(), ",");//向request设置视频id,setVideoIds参数是以逗号分开传递的,不能直接传递集合request.setVideoIds(videoIds);//调用初始化对象的方法实现删除client.getAcsResponse(request);} catch (Exception e) {e.printStackTrace();throw new GuliException(20001, "删除视频失败");}
}

2. 在service-edu调用service-vod接口实现删除多个视频的功能。

//删除多个阿里云视频
@DeleteMapping("/eduvod/video/delete-batch")
public R deleteBatch(@RequestParam("videoIdList") List<String> videoIdList);

@Autowired
private VodClient vodClient;//1 根据课程id删除小节
// 删除小节,同时删除对应视频文件
@Override
public void removeVideoByCourseId(String courseId) {// 根据课程id查询课程所有的视频idQueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();wrapperVideo.eq("course_id", courseId);wrapperVideo.select("video_source_id");List<EduVideo> eduVideoList = baseMapper.selectList(wrapperVideo);// List<EduVideo> 变成 List<String>List<String> videoIds = new ArrayList<>();for (int i = 0; i < eduVideoList.size(); i++) {EduVideo eduVideo = eduVideoList.get(i);String videoSourceId = eduVideo.getVideoSourceId();if (!StringUtils.isEmpty(videoSourceId)) {videoIds.add(videoSourceId);}}//根据多个视频id删除多个视频if (videoIds.size() > 0) {vodClient.deleteBatch(videoIds);}//删除小节QueryWrapper<EduVideo> wrapper = new QueryWrapper<>();wrapper.eq("course_id", courseId);baseMapper.delete(wrapper);
}

整合熔断器

给远程调用的服务,设置熔断机制。

1. 添加依赖

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency><!--hystrix依赖,主要是用  @HystrixCommand -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

2. 在application.properties进行配置

# 开启熔断机制
feign.hystrix.enabled=true

3. 在创建interface之后,创建interface的实现类,调用出错了则会执行实现类的代码

@Component
public class VodFileDegradeFeignClient implements VodClient{//出错之后执行@Overridepublic R removeAlyVideo(String id) {return R.error().message("删除视频出错了");}@Overridepublic R deleteBatch(List<String> videoIdList) {return R.error().message("删除多个视频出错了");}
}

4. 在接口上的@FeignClient注解,指定实现类的class

统计分析模块

需求分析

统计在线教育项目中,每天有多少注册人数,把统计出来的注册人数,使用图表显示出来。

统计某一天的注册人数:查询用户表得到需要的数据。
注:date函数获取日期时间格式里面的日期部分,去掉时分秒。

select count(*) from ucenter_member uc where date(uc.gmt_create)='2020-03-09';


生成统计数据

1. 数据库表

CREATE TABLE `statistics_daily` (`id` char(19) NOT NULL COMMENT '主键',`date_calculated` varchar(20) NOT NULL COMMENT '统计日期',`register_num` int(11) NOT NULL DEFAULT '0' COMMENT '注册人数',`login_num` int(11) NOT NULL DEFAULT '0' COMMENT '登录人数',`video_view_num` int(11) NOT NULL DEFAULT '0' COMMENT '每日播放视频数',`course_num` int(11) NOT NULL DEFAULT '0' COMMENT '每日新增课程数',`gmt_create` datetime NOT NULL COMMENT '创建时间',`gmt_modified` datetime NOT NULL COMMENT '更新时间',PRIMARY KEY (`id`),KEY `statistics_day` (`date_calculated`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='网站统计日数据';

2. 配置文件

# 服务端口
server.port=8008
# 服务名
spring.application.name=service-statistics# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/guliedu?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=qwer`123spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0
#最小空闲#请求处理的超时时间
ribbon.ReadTimeout=120000
#请求连接的超时时间
ribbon.ConnectTimeout=30000#返回json的全局时间格式
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8# 配置mapper.xml文件的路径
mybatis-plus.mapper-locations=classpath:com/hxp/staservice/mapper/xml/*.xml# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848#mybatis日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

3. 代码生成器生成代码

4. 在service-ucenter查询某一天的注册人数

//查询某一天注册人数
@GetMapping("countRegister/{day}")
public R countRegister(@PathVariable String day) {Integer count = memberService.countRegisterDay(day);return R.ok().data("countRegister", count);
}
//查询某一天注册人数
@Override
public Integer countRegisterDay(String day) {return baseMapper.countRegisterDay(day);
}
<!--查询某一天的注册人数-->
<select id="countRegisterDay" resultType="java.lang.Integer">SELECT COUNT(*) FROM ucenter_member uc WHERE DATE(uc.gmt_create)=#{day}
</select>

5. 在service_statistics模块,远程调用方法查询注册人数

@Component
@FeignClient("service-ucenter")
public interface UcenterClient {//查询某一天注册人数@GetMapping("/educenter/member/countRegister/{day}")public R countRegister(@PathVariable("day") String day);
}
@RestController
@RequestMapping("/staservice/sta")
public class StatisticsDailyController {@Autowiredprivate StatisticsDailyService statisticsService;//统计某一天注册人数,生成统计数据@PostMapping("registerCount/{day}")public R registerCount(@PathVariable String day) {statisticsService.registerCount(day);return R.ok();}
}
@Service
public class StatisticsDailyServiceImpl extends ServiceImpl<StatisticsDailyMapper, StatisticsDaily> implements StatisticsDailyService {@Autowiredprivate UcenterClient ucenterClient;//统计某一天注册人数,生成统计数据@Overridepublic void registerCount(String day) {//添加记录之前删除表相同日期的数据QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();wrapper.eq("date_calculated",day);baseMapper.delete(wrapper);//远程调用得到某一天注册人数R registerR = ucenterClient.countRegister(day);Integer countRegister = (Integer) registerR.getData().get("countRegister");System.out.println(countRegister);//把获取到的数据添加到统计分析表里面StatisticsDaily sta = new StatisticsDaily();sta.setRegisterNum(countRegister);sta.setDateCalculated(day);sta.setVideoViewNum(11);sta.setLoginNum(22);sta.setCourseNum(33);baseMapper.insert(sta);}
}

问题:如果是查询同一天,每次都会新增一条记录,并且每条记录的注册人数都不同。
所以在添加之前,需要先删除原来的记录,再做添加。

添加定时任务

1. 在启动类添加注解

2. 创建定时任务类
在这个类里面使用表达式设置什么时候执行。
cron表达式:设置执行规则

@Component
public class ScheduledTask {@Autowiredprivate StatisticsDailyService staService;//在每天凌晨1点,执行方法,把前一天的数据查询进行添加@Scheduled(cron = "0 0 1 * * ?")public void task() {staService.registerCount(DateUtil.formatDate(DateUtil.addDays(new Date(), -1)));}
}

生成cron表达式工具:https://www.pppet.net/

图表显示

//图表显示,返回两部分数据,日期json数组,数量json数组
@GetMapping("showData/{type}/{begin}/{end}")
public R showData(@PathVariable String type, @PathVariable String begin,@PathVariable String end) {Map<String, Object> map = statisticsService.getShowData(type,begin,end);return R.ok().data(map);
}
//图表显示,返回两部分数据,日期json数组,数量json数组
@Override
public Map<String, Object> getShowData(String type, String begin, String end) {//根据条件查询对应数据QueryWrapper<StatisticsDaily> wrapper = new QueryWrapper<>();wrapper.between("date_calculated",begin,end);wrapper.select("date_calculated",type);List<StatisticsDaily> staList = baseMapper.selectList(wrapper);//因为返回有两部分数据:日期 和 日期对应数量//前端要求数组json结构,对应后端java代码是list集合//创建两个list集合,一个日期list,一个数量listList<String> date_calculatedList = new ArrayList<>();List<Integer> numDataList = new ArrayList<>();//遍历查询所有数据list集合,进行封装for (int i = 0; i < staList.size(); i++) {StatisticsDaily daily = staList.get(i);//封装日期list集合date_calculatedList.add(daily.getDateCalculated());//封装对应数量switch (type) {case "login_num":numDataList.add(daily.getLoginNum());break;case "register_num":numDataList.add(daily.getRegisterNum());break;case "video_view_num":numDataList.add(daily.getVideoViewNum());break;case "course_num":numDataList.add(daily.getCourseNum());break;default:break;}}//把封装之后两个list集合放到map集合,进行返回Map<String, Object> map = new HashMap<>();map.put("date_calculatedList",date_calculatedList);map.put("numDataList",numDataList);return map;
}

谷粒学院——后台管理系统功能模块相关推荐

  1. 谷粒学院订单管理 server-order 模块

    谷粒学院订单管理 server-order 模块 模块介绍 主要实现前台页面购买课程后,生成订单,并实现微信支付的功能. ![]](https://img-blog.csdnimg.cn/7b821f ...

  2. 【zblog后台管理】模块编辑之导航条设置

    首次使用zblog的朋友,很多人可能不知道该如何修改网站导航条,那么,今天就给大家介绍一下导航条的修改 首先:zblog安装成功后,默认的导航栏只有一个"首页"和一个"留 ...

  3. MES管理系统功能模块之质量管理

    MES管理系统想要赢得市场.赢得客户就必须提高自身的质量.成本各方面的水平.飞速发展的信息技术,使得MES生产管理系统成为了为企业提高竞争力的有力工具.今天讲一下MES管理系统众多功能模块中的质量管理 ...

  4. 完成后台管理系统功能(八)商品的修改以及删除

    1.有关商品的修改功能. 业务流程:用户在item-list页面只能选择一条数据(若选择多条或者没选,则跳出提示信息),然后点击编辑,跳到用户编辑的页面,这时候页面中已经有了原本的数据,在用户对原有的 ...

  5. mysql 商品规格表_完成后台管理系统功能(九)商品规格列表的展示,添加以及删除...

    商品规格的实现图: 规律: 1.同一类商品的规格项分组相同. 2.同一类商品的规格项目是相同的.规格项目是跟商品关联. 3.不同商品规格参数的值是不同的 1 页面效果展示: 2.规格列表的展示功能 用 ...

  6. java毕业设计 springboo影视播放在线视频点播系统 springboot毕业设计题目课题选题 springboot毕业设计项目作品源码(4)后台管理系统功能和界面

  7. 信贷系统之小额信贷管理系统功能

    转自:https://www.zhihu.com/question/31075470 先来一个小额信贷管理系统业务架构图 <img src="https://pic2.zhimg.co ...

  8. Day206.课程分类【导入功能】前端实现、 课程分类列表【显示功能】、课程管理【模块需求、添加课程功能】 -谷粒学院

    谷粒学院 [课程分类]-导入前端实现 一.配置路由 1.添加路由 src/router/index.js //课程分类{path: 'subject',component: Layout,redire ...

  9. Day210.服务端渲染技术NUXT、整合前台主页面、名师、课程静态页面、首页整合banner数据后端部分【创建banner微服务、接口、banner后台前端实现】 -谷粒学院

    谷粒学院 服务端渲染技术NUXT 一.服务端渲染技术NUXT 1.什么是服务端渲染 服务端渲染又称SSR (Server Side Render)是在服务端完成页面的内容,而不是在客户端通过AJAX获 ...

最新文章

  1. 用Python分析《红楼梦》:见证了贾府的兴衰,你是否还能“笑道”世事无常
  2. JQuery变量数字相加的研究
  3. Java 自定义 ClassLoader 实现 JVM 类加载
  4. 庐山真面-Oxite的HelloWorld
  5. [蓝桥杯2015决赛]密文搜索
  6. 902. 最短编辑距离
  7. 分布式系统:一致性模型
  8. 人脸检测(三)--Haar特征原理及实现
  9. linux 重定向命令
  10. 服务器server操作系统吗,windows server 2008 R2 操作系统
  11. f-stack 编译测试笔记
  12. qq音乐for linux,[Linux] QQ音乐官方上线 For Linux V1.0.5-1 [2020.12.2]
  13. 服务器08系统操作日志在哪看,怎么查看服务器操作系统日志
  14. 《强化学习周刊》第3期:深度强化学习如何提升鲁棒性和性能
  15. 六度分离(Floyd算法)
  16. 程序员如何提升自己的代码质量?
  17. 【1.6万字】连续抓屏保存为Gif动图 【keyboard库、PIL库、imageio库和pygifsicle库 探索】
  18. html word 批注,word怎么批注修改文章
  19. Shopee招聘主页下面一排建筑简笔画代表哪里
  20. Axios实现zip文件下载

热门文章

  1. enq: HW - contention
  2. 盘点程序员情人节的表白,前端程序员最浪漫,后端不服来战
  3. 关于Intellij IDEA中文注释字体不统一的解决办法
  4. 全栈合一:万物互联下的智能安全运维
  5. 家电行业售后服务管理解决方案
  6. 社区发现算法python视频_社区发现FN算法Python实现
  7. WindowsAPI 窗口
  8. 【图灵杯 J】简单的变位词
  9. 微信接入指南进入开发者模式
  10. 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。