0


谷粒学院项目总结

文章目录

谷粒学院项目总结

1.项目介绍

1.1 采用的商业模式

B2C模式(Business To Customer 会员模式)

商家到用户,这种模式是自己制作大量自有版权的视频,放在自有平台上,让用户按月付费或者按年付费。 这种模式简单,快速,只要专心录制大量视频即可快速发展,其曾因为 lynda 的天价融资而大热。 但在中国由于版权保护意识不强,教育内容易于复制,有海量的免费资源的竞争对手众多等原因,难以取得像样的现金流

1.2 功能模块

谷粒学院,是一个B2C模式的职业技能在线教育系统,分为前台用户系统和后台运营平台

image-20211127155545776

image-20211127160336413

1.3 采用技术

image-20211127161302350

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);}}

在需要自动填充的字段加上注解:

image-20211224142307610

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模块,可以看到:

image-20211224142747999

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 将错误日志输出到文件

举个例子:

  1. GlobalExceptionHandler 中类上添加注解@Slf4j
  2. 异常输出语句:log.error(e.getMessage());

image-20211130172602775

7.整合阿里云OSS

SpringBoot整合阿里云OSS

8.整合EasyExcel

SpringBoot整合EasyExcel

9.整合阿里云视频点播

SpringBoot整合阿里云视频点播

10.整合JWT单点登录

关于JWT的详细知识可以参考:JWT整合Springboot

10.1 单点登录

单点登录三种常见方式:

  1. session广播机制实现
  2. 使用cookie+reids实现
  3. 使用token实现

image-20211224160956995

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层

主要有三个接口:

  1. 登录
  2. 注册
  3. 登录成功后,根据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

image-20211224182907514

对应的实体类:

/**
 * 网站统计日数据
 *
 * @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

模块(网关服务):

image-20211224192553004

引入相关依赖:

<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;}}

用在查询教师的方法上:

image-20220123132915901

18.项目总结

在线教育系统,分为前台网站系统和后台运营平台,

B2C

模式。

使用了微服务技术架构,前后端分离开发:

  1. 后端的主要技术架构是:SpringBoot + SpringCloud + MyBatis-Plus + HttpClient + MySQL + Maven+EasyExcel+ nginx
  2. 前端的架构是:Node.js + Vue.js +element-ui+NUXT+ECharts
  3. 其他涉及到的中间件包括Redis、阿里云OSS、阿里云视频点播
  4. 业务中使用了ECharts做图表展示,使用EasyExcel完成分类批量添加、注册分布式单点登录使用了JWT

系统分为前台用户系统和后台管理系统两部分:

  1. 前台用户系统包括:首页、课程、名师、问答、文章
  2. 后台管理系统包括:讲师管理、课程分类管理、课程管理、统计分析、Banner管理、订单管理、权限管理等功能

在这里插入图片描述

标签: springboot 项目 java

本文转载自: https://blog.csdn.net/qq_45966440/article/details/122650467
版权归原作者 LL.LEBRON 所有, 如有侵权,请联系我们删除。

“谷粒学院项目总结”的评论:

还没有评论