文章目录
谷粒学院项目总结
1.项目介绍
1.1 采用的商业模式
B2C模式(Business To Customer 会员模式)
商家到用户,这种模式是自己制作大量自有版权的视频,放在自有平台上,让用户按月付费或者按年付费。 这种模式简单,快速,只要专心录制大量视频即可快速发展,其曾因为 lynda 的天价融资而大热。 但在中国由于版权保护意识不强,教育内容易于复制,有海量的免费资源的竞争对手众多等原因,难以取得像样的现金流
1.2 功能模块
谷粒学院,是一个B2C模式的职业技能在线教育系统,分为前台用户系统和后台运营平台
1.3 采用技术
2.Mybatis-Plus相关配置
2.1 配置分页插件
可以在
config
包下新建一个Mybatis-Plus的配置类
MyBatisPlusConfig
统一管理:
//使其成为配置类@Configuration//开启事务管理@EnableTransactionManagement//指定要变成实现类的接口所在的包,然后包下面的所有接口在编译之后都会生成相应的实现类(和在每个类加@Mapper作用相同)@MapperScan("com.atguigu.eduservice.mapper")publicclassMyBatisPlusConfig{//配置分页插件@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptor interceptor =newMybatisPlusInterceptor();
interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.H2));return interceptor;}}
2.2 自动填充
新建一个
MyMetaObjectHandler
类实现
MetaObjectHandler
接口:
//注入到spring@ComponentpublicclassMyMetaObjectHandlerimplementsMetaObjectHandler{//插入时自动填充@OverridepublicvoidinsertFill(MetaObject metaObject){//属性名称,不是字段名称this.setFieldValByName("gmtCreate",newDate(), metaObject);this.setFieldValByName("gmtModified",newDate(), metaObject);}//更新时自动填充@OverridepublicvoidupdateFill(MetaObject metaObject){this.setFieldValByName("gmtModified",newDate(), metaObject);}}
在需要自动填充的字段加上注解:
2.3 代码生成器
publicclassCodeGenerator{@Testpublicvoidrun(){// 1、创建代码生成器AutoGenerator mpg =newAutoGenerator();// 2、全局配置GlobalConfig gc =newGlobalConfig();String projectPath =System.getProperty("user.dir");//项目路径
gc.setOutputDir("D:\\guli_parent\\service\\service_edu"+"/src/main/java");
gc.setAuthor("xppll");//生成后是否打开资源管理器
gc.setOpen(false);//重新生成时文件是否覆盖
gc.setFileOverride(false);//UserServie
gc.setServiceName("%sService");//去掉Service接口的首字母I//主键策略
gc.setIdType(IdType.ID_WORKER_STR);//定义生成的实体类中日期类型
gc.setDateType(DateType.ONLY_DATE);//开启Swagger2模式
gc.setSwagger2(true);
mpg.setGlobalConfig(gc);// 3、数据源配置DataSourceConfig dsc =newDataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("root");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);// 4、包配置PackageConfig pc =newPackageConfig();//模块名
pc.setModuleName("eduservice");//包 com.atguigu.eduservice
pc.setParent("com.atguigu");//包 com.atguigu.eduservice.controller
pc.setController("controller");
pc.setEntity("entity");
pc.setService("service");
pc.setMapper("mapper");
mpg.setPackageInfo(pc);// 5、策略配置StrategyConfig strategy =newStrategyConfig();
strategy.setInclude("edu_course","edu_course_description","edu_chapter","edu_video");//数据库表映射到实体的命名策略
strategy.setNaming(NamingStrategy.underline_to_camel);//生成实体时去掉表前缀
strategy.setTablePrefix(pc.getModuleName()+"_");//数据库表字段映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);// lombok 模型 @Accessors(chain = true) setter链式操作
strategy.setEntityLombokModel(true);//restful api风格控制器
strategy.setRestControllerStyle(true);//url中驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
mpg.setStrategy(strategy);// 6、执行
mpg.execute();}}
3.Swagger配置
引入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
可以在
config
包下新建一个Swagger的配置类
SwaggerConfig
统一管理:
/**
* @author xppll
* @date 2021/11/29 14:56
*/@Configuration//配置类@EnableSwagger2//swagger注解publicclassSwaggerConfig{@BeanpublicDocketwebApiConfig(){returnnewDocket(DocumentationType.SWAGGER_2).groupName("webApi").apiInfo(webApiInfo()).select().paths(Predicates.not(PathSelectors.regex("/admin/.*"))).paths(Predicates.not(PathSelectors.regex("/error.*"))).build();}privateApiInfowebApiInfo(){returnnewApiInfoBuilder().title("网站-课程中心API文档").description("本文档描述了课程中心微服务接口定义").version("1.0").contact(newContact("Helen","http://atguigu.com","[email protected]")).build();}}
访问edu模块,可以看到:
4.统一返回数据格式
项目中我们会将响应封装成
json
返回,一般我们会将所有接口的数据格式统一, 使前端(iOS Android, Web)对数据的操作更一致、轻松。 一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含状态码、返回消息、数据这几部分内容
4.1 统一结果返回类
在
commonutils
(公共工具类包)包下创建统一结果返回类
R
/**
* 定义统一返回结果的类
*/@DatapublicclassR{//swagger的注解@ApiModelProperty(value ="是否成功")privateBoolean success;@ApiModelProperty(value ="返回码")privateInteger code;@ApiModelProperty(value ="返回消息")privateString message;@ApiModelProperty(value ="返回数据")privateMap<String,Object> data =newHashMap<String,Object>();//构造方法私有publicR(){}//成功静态方法publicstaticRok(){R r =newR();
r.setSuccess(true);
r.setCode(ResultCode.SUCCESS);
r.setMessage("成功");return r;}//失败静态方法publicstaticRerror(){R r =newR();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("失败");return r;}//返回this是为了链式编程,例如 R.ok().code().message()publicRsuccess(Boolean success){this.setSuccess(success);returnthis;}publicRmessage(String message){this.setMessage(message);returnthis;}publicRcode(Integer code){this.setCode(code);returnthis;}publicRdata(String key,Object value){this.data.put(key, value);returnthis;}publicRdata(Map<String,Object> map){this.setData(map);returnthis;}}
4.2 统一定义返回码
这里又许多种方式,这里列举两种:
1.创建接口定义返回码
publicinterfaceResultCode{publicstaticInteger SUCCESS =20000;publicstaticInteger ERROR =20001;}
2.创建枚举类定义返回码
publicenumErrorCode{PARAMS_ERROR(10001,"参数有误"),ACCOUNT_PWD_NOT_EXIST(10002,"用户名或密码不存在"),TOKEN_ERROR(10003,"token不合法"),ACCOUNT_EXIST(10004,"账户已存在"),NO_PERMISSION(70001,"无访问权限"),SESSION_TIME_OUT(90001,"会话超时"),NO_LOGIN(90002,"未登录");privateint code;privateString msg;ErrorCode(int code,String msg){this.code = code;this.msg = msg;}//get,set方法...}
5.统一异常处理
5.1 创建统一异常处理器
在
handler
包下创建统一异常处理类
GlobalExceptionHandler
:
/**
* 统一异常处理类
*
* @author xppll
* @date 2021/11/29 19:11
*///对加了@Controller的方法进行拦截处理,AOP的实现@ControllerAdvice@Slf4jpublicclassGlobalExceptionHandler{//进行一次处理,处理Exception.class的异常@ExceptionHandler(Exception.class)//返回json数据,不加的话直接返回页面@ResponseBodypublicRerror(Exception e){
e.printStackTrace();//将信息写到日志文件中去
log.error(e.getMessage());returnR.error().message("执行了全局异常处理...");}}
还可以处理特定异常:
//添加特定异常方法@ExceptionHandler(ArithmeticException.class)@ResponseBodypublicRerror(ArithmeticException e){
e.printStackTrace();returnR.error().message("执行了特定异常");}
5.2 自定义异常处理
在
handler
包下创建自定义异常类
GuliException
:
/**
* 自定义异常
* 需要继承RuntimeException
* @author xppll
* @date 2021/11/29 20:09
*/@Data@AllArgsConstructor@NoArgsConstructorpublicclassGuliExceptionextendsRuntimeException{//状态码privateInteger code;//异常信息privateString msg;}
处理自定义异常:
//添加自定义异常//需要自己手动抛出@ExceptionHandler(GuliException.class)@ResponseBodypublicRerror(GuliException e){
log.error(e.getMessage());
e.printStackTrace();//传入自己定义的参数returnR.error().code(e.getCode()).message(e.getMsg());}
栗子:自己手动抛出
@GetMapping("findAll")publicRlist(){try{int a =10/0;}catch(Exception e){thrownewGuliException(20003,"出现自定义异常");}List<EduTeacher> list = teacherService.list(null);returnR.ok().data("items",list);}
6.统一日志处理
6.1 配置日志级别
日志记录器(
Logger
)的行为是分等级的。如下表所示: 分为:
OFF
、
FATAL
、
ERROR
、
WARN
、
INFO
、
DEBUG
、
ALL
默认情况下,spring boot从控制台打印出来的日志级别只有
INFO
及以上级别,可以配置日志级别:
# 设置日志级别
logging.level.root=WARN
这种配置方式只能将日志打印在控制台上
6.2 Logback日志
spring boot内部使用Logback作为日志实现的框架
配置
logback
日志
注意:需要删除
application.properties
中的其它日志配置
在
resources
中创建
logback-spring.xml
(名字必须一模一样!)
<?xml version="1.0" encoding="UTF-8"?><configurationscan="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上下文中。定义变量后,可以使“${}”来使用变量。 --><propertyname="log.path"value="D:/guli_1010/edu"/><!-- 彩色日志 --><!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 --><!-- magenta:洋红 --><!-- boldMagenta:粗红--><!-- cyan:青色 --><!-- white:白色 --><!-- magenta:洋红 --><propertyname="CONSOLE_LOG_PATTERN"value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/><!--输出到控制台--><appendername="CONSOLE"class="ch.qos.logback.core.ConsoleAppender"><!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息--><!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 --><filterclass="ch.qos.logback.classic.filter.ThresholdFilter"><level>INFO</level></filter><encoder><Pattern>${CONSOLE_LOG_PATTERN}</Pattern><!-- 设置字符集 --><charset>UTF-8</charset></encoder></appender><!--输出到文件--><!-- 时间滚动输出 level为 INFO 日志 --><appendername="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><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicyclass="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!-- 每天日志归档路径以及格式 --><fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicyclass="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文件保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文件只记录info级别的 --><filterclass="ch.qos.logback.classic.filter.LevelFilter"><level>INFO</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- 时间滚动输出 level为 WARN 日志 --><appendername="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><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicyclass="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicyclass="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文件保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文件只记录warn级别的 --><filterclass="ch.qos.logback.classic.filter.LevelFilter"><level>warn</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter></appender><!-- 时间滚动输出 level为 ERROR 日志 --><appendername="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><!-- 日志记录器的滚动策略,按日期,按大小记录 --><rollingPolicyclass="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern><timeBasedFileNamingAndTriggeringPolicyclass="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>100MB</maxFileSize></timeBasedFileNamingAndTriggeringPolicy><!--日志文件保留天数--><maxHistory>15</maxHistory></rollingPolicy><!-- 此日志文件只记录ERROR级别的 --><filterclass="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级别:
--><!--开发环境:打印控制台--><springProfilename="dev"><!--可以输出项目中的debug日志,包括mybatis的sql日志--><loggername="com.guli"level="INFO"/><!--
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
可以包含零个或多个appender元素。
--><rootlevel="INFO"><appender-refref="CONSOLE"/><appender-refref="INFO_FILE"/><appender-refref="WARN_FILE"/><appender-refref="ERROR_FILE"/></root></springProfile><!--生产环境:输出到文件--><springProfilename="pro"><rootlevel="INFO"><appender-refref="CONSOLE"/><appender-refref="DEBUG_FILE"/><appender-refref="INFO_FILE"/><appender-refref="ERROR_FILE"/><appender-refref="WARN_FILE"/></root></springProfile></configuration>
6.3 将错误日志输出到文件
举个例子:
GlobalExceptionHandler
中类上添加注解@Slf4j
- 异常输出语句:
log.error(e.getMessage());
7.整合阿里云OSS
SpringBoot整合阿里云OSS
8.整合EasyExcel
SpringBoot整合EasyExcel
9.整合阿里云视频点播
SpringBoot整合阿里云视频点播
10.整合JWT单点登录
关于JWT的详细知识可以参考:JWT整合Springboot
10.1 单点登录
单点登录三种常见方式:
- session广播机制实现
- 使用cookie+reids实现
- 使用token实现
10.2 引入依赖
<dependencies><!-- JWT--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId></dependency></dependencies>
10.3 创建JWT工具类
/**
* @author xppll
* @date 2021/12/8 13:49
*/publicclassJwtUtils{//token过期时间publicstaticfinallong EXPIRE =1000*60*60*24;//秘钥publicstaticfinalString APP_SECRET ="ukc8BDbRigUDaY6pZFfWus2jZWLPHO";/**
* 获得token
*
* @param id 用户id
* @param nickname 用户昵称
* @return
*/publicstaticStringgetJwtToken(String id,String nickname){StringJwtToken=Jwts.builder()//设置jwt头信息.setHeaderParam("typ","JWT").setHeaderParam("alg","HS256")//设置分类.setSubject("guli-user")//设置签发时间.setIssuedAt(newDate())//设置过期时间=当前时间+过多久过期的时间.setExpiration(newDate(System.currentTimeMillis()+ EXPIRE))//设置token主体部分,存储用户信息.claim("id", id).claim("nickname", nickname)//设置签发算法+秘钥.signWith(SignatureAlgorithm.HS256, APP_SECRET).compact();returnJwtToken;}/**
* 判断token是否存在与有效
*
* @param jwtToken
* @return
*/publicstaticbooleancheckToken(String jwtToken){if(StringUtils.isEmpty(jwtToken))returnfalse;try{//验证token是否是有效的tokenJwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);}catch(Exception e){
e.printStackTrace();returnfalse;}returntrue;}/**
* 判断token是否存在与有效
*
* @param request
* @return
*/publicstaticbooleancheckToken(HttpServletRequest request){try{String jwtToken = request.getHeader("token");if(StringUtils.isEmpty(jwtToken))returnfalse;Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);}catch(Exception e){
e.printStackTrace();returnfalse;}returntrue;}/**
* 根据token获取用户id
*
* @param request
* @return
*/publicstaticStringgetMemberIdByJwtToken(HttpServletRequest request){String jwtToken = request.getHeader("token");if(StringUtils.isEmpty(jwtToken))return"";Jws<Claims> claimsJws =Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);Claims claims = claimsJws.getBody();return(String) claims.get("id");}}
10.4 封装前端接受和传来的信息
登录信息:
@DatapublicclassUcentMemberVo{@ApiModelProperty(value ="手机号")privateString mobile;@ApiModelProperty(value ="密码")privateString password;}
注册信息:
@DatapublicclassRegisterVo{privateString nickname;privateString mobile;privateString password;privateString code;}
10.5 controller层
主要有三个接口:
- 登录
- 注册
- 登录成功后,根据token获取用户信息,用于前端显示
/**
* 会员表 前端控制器
*
* @author xppll
* @since 2021-12-08
*/@CrossOrigin@RestController@RequestMapping("/educenter/member")publicclassUcenterMemberController{@AutowiredprivateUcenterMemberService memberService;/**
* 登录
*
* @param member 接收前端登录传来的数据
* @return 返回R
*/@PostMapping("login")publicRloginUser(@RequestBodyUcentMemberVo member){//返回token,使用jwt生成String token = memberService.login(member);returnR.ok().data("token", token);}/**
* 注册
*
* @param registerVo 接收前端注册传来的数据
* @return 返回R
*/@PostMapping("register")publicRregisterUser(@RequestBodyRegisterVo registerVo){
memberService.register(registerVo);returnR.ok();}/**
* 根据token获取用户信息,用于前端显示
*
* @param request
* @return
*/@GetMapping("getMemberInfo")publicRgetMemberInfo(HttpServletRequest request){//调用jwt工具类,根据request对象获取头信息,返回用户idString memberId =JwtUtils.getMemberIdByJwtToken(request);UcentMemberVo member = memberService.getLoginInfo(memberId);returnR.ok().data("userInfo", member);}}
10.6 service层
/**
* 会员表 服务实现类
*
* @author xppll
* @since 2021-12-08
*/@ServicepublicclassUcenterMemberServiceImplextendsServiceImpl<UcenterMemberMapper,UcenterMember>implementsUcenterMemberService{@AutowiredprivateRedisTemplate<String,String> redisTemplate;/**
* 登录
*
* @param member 前端传的参数
* @return 返回token
*/@OverridepublicStringlogin(UcentMemberVo member){//获取登录手机号和密码String mobile = member.getMobile();String password = member.getPassword();//1.两个有一个为空,登录失败!if(StringUtils.isBlank(mobile)||StringUtils.isBlank(password)){thrownewGuliException(20001,"手机号和密码不能为空,登录失败!");}//2.判断手机号是否存在LambdaQueryWrapper<UcenterMember> queryWrapper =newLambdaQueryWrapper<>();
queryWrapper.eq(UcenterMember::getMobile, mobile);UcenterMember mobileMember = baseMapper.selectOne(queryWrapper);if(mobileMember ==null){thrownewGuliException(20001,"手机号不存在,登录失败!");}//3.判断密码是否正确//数据库的密码加了密//需要把输入密码加密在比较if(!MD5.encrypt(password).equals(mobileMember.getPassword())){thrownewGuliException(20001,"密码错误,登录失败!");}//4.判断用户是否被禁(封号)if(mobileMember.getIsDisabled()){thrownewGuliException(20001,"用户已被禁止登录,登录失败!");}//调用JWT工具类生成token//传入id,nicknamereturnJwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());}//注册@Overridepublicvoidregister(RegisterVo registerVo){//验证码String code = registerVo.getCode();//手机号String mobile = registerVo.getMobile();//昵称String nickname = registerVo.getNickname();//密码String password = registerVo.getPassword();if(StringUtils.isBlank(mobile)||StringUtils.isBlank(code)||StringUtils.isBlank(nickname)||StringUtils.isBlank(password)){thrownewGuliException(20001,"传入参数不能为空!,注册失败");}//从redis取出验证码String redisCode = redisTemplate.opsForValue().get(mobile);//判断验证码是否失效if(StringUtils.isBlank(redisCode)){thrownewGuliException(20001,"验证码失效!,注册失败");}//判断验证码是否正确if(!code.equals(redisCode)){thrownewGuliException(20001,"验证码错误!,注册失败");}//判断手机号是否已经注册过LambdaQueryWrapper<UcenterMember> queryWrapper =newLambdaQueryWrapper<>();
queryWrapper.eq(UcenterMember::getMobile, mobile);Integer count = baseMapper.selectCount(queryWrapper);if(count >0){thrownewGuliException(20001,"该手机号已经被注册!注册失败");}//添加到数据库UcenterMember member =newUcenterMember();
member.setMobile(mobile);
member.setNickname(nickname);//密码需要加密
member.setPassword(MD5.encrypt(password));
member.setIsDisabled(false);
member.setAvatar("https://xppll.oss-cn-beijing.aliyuncs.com/2021/12/08/dde5b98fe9dca6b6076file.png");
baseMapper.insert(member);}//根据id获取信息传给前端@OverridepublicUcentMemberVogetLoginInfo(String memberId){UcenterMember member = baseMapper.selectById(memberId);UcentMemberVo ucentMemberVo =newUcentMemberVo();BeanUtils.copyProperties(member, ucentMemberVo);return ucentMemberVo;}}
11.整合阿里云短信
这里实现短信功能为了完成用户的注册
11.1 准备工作
首先需要开通阿里云短信服务
在导入依赖:
<dependencies><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId></dependency><dependency><groupId>com.aliyun</groupId><artifactId>aliyun-java-sdk-core</artifactId></dependency></dependencies>
11.2 具体实现
controller
层:
/**
* @author xppll
* @date 2021/12/8 19:52
*/@RestController@RequestMapping("/edumsm/msm")publicclassMsmController{@AutowiredprivateMsmService msmService;@AutowiredprivateRedisTemplate<String,String> reditemplate;//通过手机号发送短信的方法@GetMapping("send/{phone}")publicRsendMsm(@PathVariable("phone")String phone){//1.从redis获取验证码,如果获取到直接返回String code = reditemplate.opsForValue().get(phone);if(!StringUtils.isEmpty(code)){returnR.ok();}//2.调用工具类生成四位随机数,传递给阿里云进行发送
code =RandomUtil.getFourBitRandom();Map<String,Object> param =newHashMap<>();
param.put("code", code);//3.调用service里的方法实现短信发送boolean isSend = msmService.send(param, phone);if(isSend){//4.发送成功,把发送成功的验证码放到redis中去并设置有效时间
reditemplate.opsForValue().set(phone, code,5,TimeUnit.MINUTES);returnR.ok();}else{//5.发送失败,返回失败信息returnR.error().message("短信发送失败!");}}}
service
层:
/**
* @author xppll
* @date 2021/12/8 19:53
*/@ServicepublicclassMsmServiceImplimplementsMsmService{/**
* 发送短信
* @param param 需要阿里云发送的验证码
* @param phone 手机号
* @return
*/@Overridepublicbooleansend(Map<String,Object> param,String phone){//手机号为空,返回falseif(StringUtils.isEmpty(phone))returnfalse;//地域节点,id,密钥DefaultProfile profile =DefaultProfile.getProfile("default","xxx","xxx");IAcsClient client =newDefaultAcsClient(profile);//设置相关参数CommonRequest request =newCommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");//设置发送相关的参数//手机号
request.putQueryParameter("PhoneNumbers", phone);//阿里云中申请的 ”签名名称“
request.putQueryParameter("SignName","我的谷粒在线教育网站");//阿里云申请的 “模板CODE”
request.putQueryParameter("TemplateCode","SMS_xxxxx");//验证码
request.putQueryParameter("TemplateParam",JSONObject.toJSONString(param));//发送try{CommonResponse response = client.getCommonResponse(request);System.out.println(response.getData());return response.getHttpResponse().isSuccess();}catch(ClientException e){
e.printStackTrace();}returnfalse;}}
12.整合微信扫描登录
SpringBoot整合微信登录
13.定时统计每天的注册人数
13.1 数据库表和实体类
数据库表
statistics_daily
:
对应的实体类:
/**
* 网站统计日数据
*
* @author xppll
* @since 2021-12-16
*/@Data@EqualsAndHashCode(callSuper =false)@Accessors(chain =true)@ApiModel(value="StatisticsDaily对象", description="网站统计日数据")publicclassStatisticsDailyimplementsSerializable{privatestaticfinallong serialVersionUID =1L;@ApiModelProperty(value ="主键")@TableId(value ="id", type =IdType.ID_WORKER_STR)privateString id;@ApiModelProperty(value ="统计日期")privateString dateCalculated;@ApiModelProperty(value ="注册人数")privateInteger registerNum;@ApiModelProperty(value ="登录人数")privateInteger loginNum;@ApiModelProperty(value ="每日播放视频数")privateInteger videoViewNum;@ApiModelProperty(value ="每日新增课程数")privateInteger courseNum;@ApiModelProperty(value ="创建时间")@TableField(fill =FieldFill.INSERT)privateDate gmtCreate;@ApiModelProperty(value ="更新时间")@TableField(fill =FieldFill.INSERT_UPDATE)privateDate gmtModified;}
13.2 实现接口
在
service_ucenter
模块创建接口,统计某一天的注册人数:
controller
层:
//查询某一天的注册人数@GetMapping("countRegister/{day}")publicRcountRegister(@PathVariable("day")String day){Integer count=memberService.countRegisterDay(day);returnR.ok().data("countRegister",count);}
service
层:
@OverridepublicIntegercountRegisterDay(String day){return baseMapper.countRegisterDay(day);}
mapper
层:
<!--查询某一天的注册人数--><selectid="countRegisterDay"resultType="java.lang.Integer">
SELECT COUNT(*)
FROM ucenter_member uc
WHERE DATE(uc.gmt_create) = #{day}
</select>
13.3 远程调用
在
service_statistics
模块创建远程调用接口:
在client包下
UcenterClient
接口:
/**
* @author xppll
* @date 2021/12/16 22:38
*/@Component@FeignClient("service-ucenter")publicinterfaceUcenterClient{//查询某一天的注册人数@GetMapping("/educenter/member/countRegister/{day}")publicRcountRegister(@PathVariable("day")String day);}
controller
层:
//统计某一天的注册人数,生成统计数据@PostMapping("registerCount/{day}")publicRregisterCount(@PathVariable("day")String day){
staService.registerCount(day);returnR.ok();}
service
层:
@ServicepublicclassStatisticsDailyServiceImplextendsServiceImpl<StatisticsDailyMapper,StatisticsDaily>implementsStatisticsDailyService{@AutowiredprivateUcenterClient ucenterClient;@OverridepublicvoidregisterCount(String day){//先删除数据库该天的记录,然后重写添加,防止添加多个LambdaQueryWrapper<StatisticsDaily> queryWrapper =newLambdaQueryWrapper<>();
queryWrapper.eq(StatisticsDaily::getDateCalculated, day);
baseMapper.delete(queryWrapper);//远程调用得到某一天的注册人数R register = ucenterClient.countRegister(day);Integer count =(Integer) register.getData().get("countRegister");//把获取的数据添加到数据库,统计分析表里StatisticsDaily sta =newStatisticsDaily();//注册人数
sta.setRegisterNum(count);//统计日期
sta.setDateCalculated(day);//每日视频播放数
sta.setVideoViewNum(RandomUtils.nextInt(100,200));//每日登录人数
sta.setLoginNum(RandomUtils.nextInt(100,200));//每日新增课程数
sta.setCourseNum(RandomUtils.nextInt(100,200));
baseMapper.insert(sta);}}
13.4 定时任务
推荐一个网站,可以生成所需定时任务的cron表达式:在线Cron表达式生成器 (qqe2.com)
创建定时任务类,使用
cron
表达式:
/**
* @author xppll
* @date 2021/12/17 13:31
*/@ComponentpublicclassScheduledTask{@AutowiredprivateStatisticsDailyService staService;/**
* 在每天凌晨一点,把前一天的数据进行查询添加
*/@Scheduled(cron ="0 0 1 * * ?")publicvoidtask(){
staService.registerCount(DateUtil.formatDate(DateUtil.addDays(newDate(),-1)));}}
这里的日期转换工具类
DateUtil
:
/**
* 日期操作工具类
*
* @author qy
* @since 1.0
*/publicclassDateUtil{privatestaticfinalString dateFormat ="yyyy-MM-dd";/**
* 格式化日期
*
* @param date
* @return
*/publicstaticStringformatDate(Date date){SimpleDateFormat sdf =newSimpleDateFormat(dateFormat);return sdf.format(date);}/**
* 在日期date上增加amount天 。
*
* @param date 处理的日期,非null
* @param amount 要加的天数,可能为负数
*/publicstaticDateaddDays(Date date,int amount){Calendar now =Calendar.getInstance();
now.setTime(date);
now.set(Calendar.DATE, now.get(Calendar.DATE)+ amount);return now.getTime();}publicstaticvoidmain(String[] args){System.out.println(DateUtil.formatDate(newDate()));System.out.println(DateUtil.formatDate(DateUtil.addDays(newDate(),-1)));}}
14.整合微信支付
SpringBoot整合微信支付
15.权限管理模块
待补充…
16.网关gateway
详细的关于微服务中网关gateway的使用可以参考:【SpringCloud】学习笔记-p4(Gateway服务网关)
16.1 准备工作
创建一个
api-gateway
模块(网关服务):
引入相关依赖:
<dependencies><dependency><groupId>com.atguigu</groupId><artifactId>common_utils</artifactId><version>0.0.1-SNAPSHOT</version></dependency><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-gateway</artifactId></dependency><!--gson--><dependency><groupId>com.google.code.gson</groupId><artifactId>gson</artifactId></dependency><!--服务调用--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency></dependencies>
16.2 编写基础配置和路由规则
# 服务端口
server.port=8222
# 服务名
spring.application.name=service-gateway
# nacos服务地址
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
#使用服务发现路由
spring.cloud.gateway.discovery.locator.enabled=true
#服务路由名小写
#spring.cloud.gateway.discovery.locator.lower-case-service-id=true
#路由id,自定义,只要唯一即可
spring.cloud.gateway.routes[0].id=service-acl
#路由的目标地址 lb就是负载均衡,后面跟服务名称
spring.cloud.gateway.routes[0].uri=lb://service-acl
#路由断言,也就是判断请求是否符合路由规则的条件
spring.cloud.gateway.routes[0].predicates= Path=/*/acl/**
#配置service-edu服务
spring.cloud.gateway.routes[1].id=service-edu
spring.cloud.gateway.routes[1].uri=lb://service-edu
spring.cloud.gateway.routes[1].predicates= Path=/eduservice/**
#配置service-ucenter服务
spring.cloud.gateway.routes[2].id=service-ucenter
spring.cloud.gateway.routes[2].uri=lb://service-ucenter
spring.cloud.gateway.routes[2].predicates= Path=/ucenterservice/**
#配置service-ucenter服务
spring.cloud.gateway.routes[3].id=service-cms
spring.cloud.gateway.routes[3].uri=lb://service-cms
spring.cloud.gateway.routes[3].predicates= Path=/cmsservice/**
spring.cloud.gateway.routes[4].id=service-msm
spring.cloud.gateway.routes[4].uri=lb://service-msm
spring.cloud.gateway.routes[4].predicates= Path=/edumsm/**
spring.cloud.gateway.routes[5].id=service-order
spring.cloud.gateway.routes[5].uri=lb://service-order
spring.cloud.gateway.routes[5].predicates= Path=/orderservice/**
spring.cloud.gateway.routes[6].id=service-order
spring.cloud.gateway.routes[6].uri=lb://service-order
spring.cloud.gateway.routes[6].predicates= Path=/orderservice/**
spring.cloud.gateway.routes[7].id=service-oss
spring.cloud.gateway.routes[7].uri=lb://service-oss
spring.cloud.gateway.routes[7].predicates= Path=/eduoss/**
spring.cloud.gateway.routes[8].id=service-statistic
spring.cloud.gateway.routes[8].uri=lb://service-statistic
spring.cloud.gateway.routes[8].predicates= Path=/staservice/**
spring.cloud.gateway.routes[9].id=service-vod
spring.cloud.gateway.routes[9].uri=lb://service-vod
spring.cloud.gateway.routes[9].predicates= Path=/eduvod/**
spring.cloud.gateway.routes[10].id=service-edu
spring.cloud.gateway.routes[10].uri=lb://service-edu
spring.cloud.gateway.routes[10].predicates= Path=/eduuser/**
spring.redis.host=192.168.75.130
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
16.3 网关解决跨域问题
这里我们用网关解决跨域问题,就不用在用
nginx+@CrossOrigin
解决跨域了:
@ConfigurationpublicclassCorsConfig{@BeanpublicCorsWebFiltercorsFilter(){CorsConfiguration config =newCorsConfiguration();//允许的跨域ajax的请求方式
config.addAllowedMethod("*");//允许哪些网站的跨域请求 ,这里*就是全部
config.addAllowedOrigin("*");//允许在请求中携带的头信
config.addAllowedHeader("*");UrlBasedCorsConfigurationSource source =newUrlBasedCorsConfigurationSource(newPathPatternParser());
source.registerCorsConfiguration("/**", config);returnnewCorsWebFilter(source);}}
16.4 Filter使用
创建全局
Filter
类,需要实现
GlobalFilter
接口,统一处理会员登录与外部不允许访问的服务:
/**
* 全局Filter,统一处理会员登录与外部不允许访问的服务
*
* @author qy
* @since 2019-11-21
*/@ComponentpublicclassAuthGlobalFilterimplementsGlobalFilter,Ordered{privateAntPathMatcher antPathMatcher =newAntPathMatcher();@OverridepublicMono<Void>filter(ServerWebExchange exchange,GatewayFilterChain chain){//获取请求参数ServerHttpRequest request = exchange.getRequest();String path = request.getURI().getPath();//谷粒学院api接口,校验用户必须登录if(antPathMatcher.match("/api/**/auth/**", path)){List<String> tokenList = request.getHeaders().get("token");//token为空,未登录,拦截请求if(null== tokenList){ServerHttpResponse response = exchange.getResponse();returnout(response);}else{//拦截请求ServerHttpResponse response = exchange.getResponse();returnout(response);}}//内部服务接口,不允许外部访问if(antPathMatcher.match("/**/inner/**", path)){//拦截请求ServerHttpResponse response = exchange.getResponse();returnout(response);}//放行return chain.filter(exchange);}@OverridepublicintgetOrder(){return0;}privateMono<Void>out(ServerHttpResponse response){JsonObject message =newJsonObject();
message.addProperty("success",false);
message.addProperty("code",28004);
message.addProperty("data","鉴权失败");byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);DataBuffer buffer = response.bufferFactory().wrap(bits);//response.setStatusCode(HttpStatus.UNAUTHORIZED);//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type","application/json;charset=UTF-8");return response.writeWith(Mono.just(buffer));}}
16.5 自定义异常处理
服务网关调用服务时可能会有一些异常或服务不可用,它返回错误信息不友好,需要我们覆盖处理,创建异常处理类
ErrorHandlerConfig
:
@Configuration@EnableConfigurationProperties({ServerProperties.class,ResourceProperties.class})publicclassErrorHandlerConfig{privatefinalServerProperties serverProperties;privatefinalApplicationContext applicationContext;privatefinalResourceProperties resourceProperties;privatefinalList<ViewResolver> viewResolvers;privatefinalServerCodecConfigurer serverCodecConfigurer;publicErrorHandlerConfig(ServerProperties serverProperties,ResourceProperties resourceProperties,ObjectProvider<List<ViewResolver>> viewResolversProvider,ServerCodecConfigurer serverCodecConfigurer,ApplicationContext applicationContext){this.serverProperties = serverProperties;this.applicationContext = applicationContext;this.resourceProperties = resourceProperties;this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);this.serverCodecConfigurer = serverCodecConfigurer;}@Bean@Order(Ordered.HIGHEST_PRECEDENCE)publicErrorWebExceptionHandlererrorWebExceptionHandler(ErrorAttributes errorAttributes){JsonExceptionHandler exceptionHandler =newJsonExceptionHandler(
errorAttributes,this.resourceProperties,this.serverProperties.getError(),this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());return exceptionHandler;}}
JsonExceptionHandler
:
/**
* 自定义异常处理
*
* <p>异常时用JSON代替HTML异常信息<p>
/
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
/**
* 获取异常属性
*/@OverrideprotectedMap<String,Object>getErrorAttributes(ServerRequest request,boolean includeStackTrace){Map<String,Object> map =newHashMap<>();
map.put("success",false);
map.put("code",20005);
map.put("message","网关失败");
map.put("data",null);return map;}/**
* 指定响应处理方法为JSON处理的方法
*
* @param errorAttributes
*/@OverrideprotectedRouterFunction<ServerResponse>getRoutingFunction(ErrorAttributes errorAttributes){returnRouterFunctions.route(RequestPredicates.all(),this::renderErrorResponse);}/**
* 根据code获取对应的HttpStatus
*
* @param errorAttributes
*/@OverrideprotectedintgetHttpStatus(Map<String,Object> errorAttributes){return200;}}
17.Redis进行缓存
首页数据通过Redis进行缓存,Redis缓存配置类
RedisConfig
:
//开启缓存@EnableCaching@ConfigurationpublicclassRedisConfigextendsCachingConfigurerSupport{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactory
factory){RedisTemplate<String,Object> template =newRedisTemplate<>();RedisSerializer<String> redisSerializer =newStringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =newJackson2JsonRedisSerializer(Object.class);ObjectMapper om =newObjectMapper();
om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);//key序列化方式
template.setKeySerializer(redisSerializer);//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);return template;}@BeanpublicCacheManagercacheManager(RedisConnectionFactory factory){RedisSerializer<String> redisSerializer =newStringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer =newJackson2JsonRedisSerializer(Object.class);//解决查询缓存转换异常的问题ObjectMapper om =newObjectMapper();
om.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);// 配置序列化(解决乱码的问题),过期时间600秒RedisCacheConfiguration config =RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(600)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).disableCachingNullValues();RedisCacheManager cacheManager =RedisCacheManager.builder(factory).cacheDefaults(config).build();return cacheManager;}}
用在查询教师的方法上:
18.项目总结
在线教育系统,分为前台网站系统和后台运营平台,
B2C
模式。
使用了微服务技术架构,前后端分离开发:
- 后端的主要技术架构是:SpringBoot + SpringCloud + MyBatis-Plus + HttpClient + MySQL + Maven+EasyExcel+ nginx
- 前端的架构是:Node.js + Vue.js +element-ui+NUXT+ECharts
- 其他涉及到的中间件包括Redis、阿里云OSS、阿里云视频点播
- 业务中使用了ECharts做图表展示,使用EasyExcel完成分类批量添加、注册分布式单点登录使用了JWT
系统分为前台用户系统和后台管理系统两部分:
- 前台用户系统包括:首页、课程、名师、问答、文章
- 后台管理系统包括:讲师管理、课程分类管理、课程管理、统计分析、Banner管理、订单管理、权限管理等功能
版权归原作者 LL.LEBRON 所有, 如有侵权,请联系我们删除。