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.velocitygroupId>
<artifactId>velocity-engine-coreartifactId>
<version>2.0version>
dependency>
public class CodeGenerator {
@Test
public 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接口的首字母I
gc.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.eduservice
pc.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 {
@Autowired
private 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) { //获取路径中输入的id
boolean flag = teacherService.removeById(id);
if (flag) {
return R.ok();
} else {
return R.error();
}
}
在父工程下创建一个子模块common,作为公共模块,在common模块下创建一个子模块
加上相关依赖
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<scope>provided scope>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<scope>provided scope>
dependency>
@Configuration //配置类
@EnableSwagger2 //swagger注解
public class SwaggerConfig {
@Bean
public 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.hxpgroupId>
<artifactId>service_baseartifactId>
<version>0.0.1-SNAPSHOTversion>
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>();
//将构造方法私有,外部不能随便new
private 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.hxpgroupId>
<artifactId>common_utilsartifactId>
<version>0.0.1-SNAPSHOTversion>
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 {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("gmtCreate", new Date(),metaObject);
this.setFieldValByName("gmtModified", new Date(), metaObject);
}
@Override
public 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)
@ResponseBody
public 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
<configuration scan="true" scanPeriod="10 seconds">
<contextName>logbackcontextName>
<property name="log.path" value="D:/guli_1010/edu" />
<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">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFOlevel>
filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}Pattern>
<charset>UTF-8charset>
encoder>
appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/log_info.logfile>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
<charset>UTF-8charset>
encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15maxHistory>
rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFOlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/log_warn.logfile>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
<charset>UTF-8charset>
encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15maxHistory>
rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warnlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/log_error.logfile>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%npattern>
<charset>UTF-8charset>
encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15maxHistory>
rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERRORlevel>
<onMatch>ACCEPTonMatch>
<onMismatch>DENYonMismatch>
filter>
appender>
<springProfile name="dev">
<logger name="com.guli" level="INFO" />
<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)引入依赖
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
dependency>
(3)创建配置文件
server.port=8002
spring.application.name=service-oss
spring.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;
@Override
public 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 {
@Autowired
private OssService ossService;
@PostMapping
public R uploadOssFile(MultipartFile file) {
//MultipartFile 获取上传文件
//返回上传到oss的路径
String url = ossService.uploadFileAvatar(file);
return R.ok().data("url",url);
}
}
@Service
public class OssServiceImpl implements OssService {
//上传头像到oss
@Override
public 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);
//关闭OSSClient
ossClient.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.alibabagroupId>
<artifactId>easyexcelartifactId>
<version>2.1.1version>
dependency>
<dependency>
<groupId>org.apache.poigroupId>
<artifactId>poiartifactId>
<version>3.1.7version>
dependency>
<dependency>
<groupId>org.apache.poigroupId>
<artifactId>poi-ooxmlartifactId>
<version>3.1.7version>
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;
}
@Override
public 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为0
EduSubject 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为0
EduSubject twoSubject = subjectService.getOne(wrapper);
return twoSubject;
}
@Override
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
}
}
3. 编写接口
@RestController
@RequestMapping("/eduservice/subject")
public class EduSubjectController {
@Autowired
private 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 {
//添加课程分类
@Override
public 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=0
QueryWrapper<EduSubject> wrapperOne = new QueryWrapper<>();
wrapperOne.eq("parent_id", "0");
List<EduSubject> oneSubjectList = baseMapper.selectList(wrapperOne);
//2 查询所有二级分类 parentId != 0
QueryWrapper<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 {
@Autowired
private 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 {
@Autowired
private EduVideoService videoService; //注入小节service
//课程大纲列表
@Override
public 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 {
@Autowired
private 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 subjectLevelTwo
FROM edu_course ec LEFT JOIN edu_course_description ecd ON ec.id=ecd.id
LEFT JOIN edu_teacher et ON ec.teacher_id=et.id
LEFT JOIN edu_subject es1 ON ec.subject_parent_id=es1.id
LEFT JOIN edu_subject es2 ON ec.subject_id=es2.id
WHERE 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/javadirectory>
<includes>
<include>**/*.xmlinclude>
includes>
<filtering>falsefiltering>
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 {
@Autowired
private VodClient vodClient;
//1 根据课程id删除小节
// 删除小节,同时删除对应视频文件
@Override
public 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.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>4.3.3version>
dependency>
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-vodartifactId>
<version>2.15.5version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-sdk-vod-uploadartifactId>
<version>1.4.11version>
dependency>
<dependency>
<groupId>com.google.code.gsongroupId>
<artifactId>gsonartifactId>
<version>2.8.2version>
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;
@Override
public void afterPropertiesSet() throws Exception {
ACCESS_KEY_SECRET = keysecret;
ACCESS_KEY_ID = keyid;
}
}
3. 编写接口
@RestController
@RequestMapping("/eduvod/video")
public class VodController {
@Autowired
private 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.mp4
String fileName = file.getOriginalFilename();
//title:上传之后显示名称,01.01.mp4 => 01.01
String 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设置视频id
request.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.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
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,3
String 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查询课程所有的视频id
QueryWrapper<EduVideo> wrapperVideo = new QueryWrapper<>();
wrapperVideo.eq("course_id", courseId);
wrapperVideo.select("video_source_id");
List<EduVideo> eduVideoList = baseMapper.selectList(wrapperVideo);
// List 变成 List
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.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
2. 在application.properties进行配置
# 开启熔断机制
feign.hystrix.enabled=true
3. 在创建interface之后,创建interface的实现类,调用出错了则会执行实现类的代码
@Component
public class VodFileDegradeFeignClient implements VodClient{
//出错之后执行
@Override
public R removeAlyVideo(String id) {
return R.error().message("删除视频出错了");
}
@Override
public 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`123
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000
spring.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 {
@Autowired
private 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 {
@Autowired
private UcenterClient ucenterClient;
//统计某一天注册人数,生成统计数据
@Override
public 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 {
@Autowired
private 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,一个数量list
List<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;
}