谷粒学院项目总结

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")
public class MyBatisPlusConfig {
    //配置分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
        return interceptor;
    }
}

2.2 自动填充

新建一个MyMetaObjectHandler类实现MetaObjectHandler接口:

//注入到spring
@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);
    }
}

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

image-20211224142307610

2.3 代码生成器

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:\\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 = new DataSourceConfig();
        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 = new PackageConfig();
        //模块名
        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 = new StrategyConfig();

        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注解
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("Helen", "http://atguigu.com",
                                 "55317332@qq.com"))
            .build();
    }

}

访问edu模块,可以看到:

image-20211224142747999

4.统一返回数据格式

项目中我们会将响应封装成json返回,一般我们会将所有接口的数据格式统一, 使前端(iOS Android, Web)对数据的操作更一致、轻松。 一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含状态码、返回消息、数据这几部分内容

4.1 统一结果返回类

commonutils(公共工具类包)包下创建统一结果返回类R

/**
 * 定义统一返回结果的类
 */
@Data
public class R {
    //swagger的注解
    @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>();

    //构造方法私有
    public 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;
    }

    //返回this是为了链式编程,例如 R.ok().code().message()
    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;
    }

}

4.2 统一定义返回码

这里又许多种方式,这里列举两种:

1.创建接口定义返回码

public interface ResultCode {
    public static Integer SUCCESS = 20000;
    public static Integer ERROR = 20001;
}

2.创建枚举类定义返回码

public enum ErrorCode {

    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, "未登录");

    private int code;
    private String 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
@Slf4j
public class GlobalExceptionHandler {
    //进行一次处理,处理Exception.class的异常
    @ExceptionHandler(Exception.class)
    //返回json数据,不加的话直接返回页面
    @ResponseBody
    public R error(Exception e) {
        e.printStackTrace();
        //将信息写到日志文件中去
        log.error(e.getMessage());
        return R.error().message("执行了全局异常处理...");
    }

}

还可以处理特定异常:

//添加特定异常方法
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public R error(ArithmeticException e){
    e.printStackTrace();
    return R.error().message("执行了特定异常");
}

5.2 自定义异常处理

handler包下创建自定义异常类GuliException

/**
 * 自定义异常
 * 需要继承RuntimeException
 * @author xppll
 * @date 2021/11/29 20:09
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GuliException extends RuntimeException {
    //状态码
    private Integer code;
    //异常信息
    private String msg;
}

处理自定义异常:

//添加自定义异常
//需要自己手动抛出
@ExceptionHandler(GuliException.class)
@ResponseBody
public R error(GuliException e){
    log.error(e.getMessage());
    e.printStackTrace();
    //传入自己定义的参数
    return R.error().code(e.getCode()).message(e.getMsg());
}

栗子:自己手动抛出

@GetMapping("findAll")
public R list(){
    try {
        int a = 10/0;
    }catch(Exception e) {
        throw new GuliException(20003,"出现自定义异常");
    }
    List<EduTeacher> list = teacherService.list(null);
    return R.ok().data("items",list);
}

6.统一日志处理

6.1 配置日志级别

日志记录器(Logger)的行为是分等级的。如下表所示: 分为:OFFFATALERRORWARNINFODEBUGALL 默认情况下,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"?>
<configuration  scan="true" scanPeriod="10 seconds">
    <!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
    <!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
    <!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
    <!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->

    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <property name="log.path" value="D:/guli_1010/edu" />

    <!-- 彩色日志 -->
    <!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
    <!-- magenta:洋红 -->
    <!-- boldMagenta:粗红-->
    <!-- cyan:青色 -->
    <!-- white:白色 -->
    <!-- magenta:洋红 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>


    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>


    <!--输出到文件-->

    <!-- 时间滚动输出 level为 INFO 日志 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_info.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 WARN 日志 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_warn.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>


    <!-- 时间滚动输出 level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_error.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!--
        <logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
        <logger>仅有一个name属性,
        一个可选的level和一个可选的addtivity属性。
        name:用来指定受此logger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
              如果未设置此属性,那么当前logger将会继承上级的级别。
    -->
    <!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
     -->
    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <!--可以输出项目中的debug日志,包括mybatis的sql日志-->
        <logger name="com.guli" level="INFO" />

        <!--
            root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
            level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
            可以包含零个或多个appender元素。
        -->
        <root level="INFO">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
        </root>
    </springProfile>


    <!--生产环境:输出到文件-->
    <springProfile name="pro">

        <root level="INFO">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="WARN_FILE" />
        </root>
    </springProfile>

</configuration>

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
 */
public class JwtUtils {

    //token过期时间
    public static final long EXPIRE = 1000 * 60 * 60 * 24;
    //秘钥
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

    /**
     * 获得token
     *
     * @param id       用户id
     * @param nickname 用户昵称
     * @return
     */
    public static String getJwtToken(String id, String nickname) {

        String JwtToken = Jwts.builder()
            //设置jwt头信息
            .setHeaderParam("typ", "JWT")
            .setHeaderParam("alg", "HS256")
            //设置分类
            .setSubject("guli-user")
            //设置签发时间
            .setIssuedAt(new Date())
            //设置过期时间=当前时间+过多久过期的时间
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
            //设置token主体部分,存储用户信息
            .claim("id", id)
            .claim("nickname", nickname)
            //设置签发算法+秘钥
            .signWith(SignatureAlgorithm.HS256, APP_SECRET)
            .compact();
        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     *
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) return false;
        try {
            //验证token是否是有效的token
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     *
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");
            if (StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token获取用户id
     *
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(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 封装前端接受和传来的信息

登录信息:

@Data
public class UcentMemberVo {
    @ApiModelProperty(value = "手机号")
    private String mobile;

    @ApiModelProperty(value = "密码")
    private String password;
}

注册信息:

@Data
public class RegisterVo {
    private String nickname;
    private String mobile;
    private String password;
    private String code;
}

10.5 controller层

主要有三个接口:

  1. 登录
  2. 注册
  3. 登录成功后,根据token获取用户信息,用于前端显示
/**
 * 会员表 前端控制器
 *
 * @author xppll
 * @since 2021-12-08
 */
@CrossOrigin
@RestController
@RequestMapping("/educenter/member")
public class UcenterMemberController {

    @Autowired
    private UcenterMemberService memberService;

    /**
     * 登录
     *
     * @param member 接收前端登录传来的数据
     * @return 返回R
     */
    @PostMapping("login")
    public R loginUser(@RequestBody UcentMemberVo member) {
        //返回token,使用jwt生成
        String token = memberService.login(member);
        return R.ok().data("token", token);
    }


    /**
     * 注册
     *
     * @param registerVo 接收前端注册传来的数据
     * @return 返回R
     */
    @PostMapping("register")
    public R registerUser(@RequestBody RegisterVo registerVo) {
        memberService.register(registerVo);
        return R.ok();
    }

    /**
     * 根据token获取用户信息,用于前端显示
     *
     * @param request
     * @return
     */
    @GetMapping("getMemberInfo")
    public R getMemberInfo(HttpServletRequest request) {
        //调用jwt工具类,根据request对象获取头信息,返回用户id
        String memberId = JwtUtils.getMemberIdByJwtToken(request);
        UcentMemberVo member = memberService.getLoginInfo(memberId);
        return R.ok().data("userInfo", member);
    }
}

10.6 service层

/**
 * 会员表 服务实现类
 *
 * @author xppll
 * @since 2021-12-08
 */
@Service
public class UcenterMemberServiceImpl extends ServiceImpl<UcenterMemberMapper, UcenterMember> implements UcenterMemberService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 登录
     *
     * @param member 前端传的参数
     * @return 返回token
     */
    @Override
    public String login(UcentMemberVo member) {
        //获取登录手机号和密码
        String mobile = member.getMobile();
        String password = member.getPassword();
        //1.两个有一个为空,登录失败!
        if (StringUtils.isBlank(mobile) || StringUtils.isBlank(password)) {
            throw new GuliException(20001, "手机号和密码不能为空,登录失败!");
        }
        //2.判断手机号是否存在
        LambdaQueryWrapper<UcenterMember> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UcenterMember::getMobile, mobile);
        UcenterMember mobileMember = baseMapper.selectOne(queryWrapper);
        if (mobileMember == null) {
            throw new GuliException(20001, "手机号不存在,登录失败!");
        }
        //3.判断密码是否正确
        //数据库的密码加了密
        //需要把输入密码加密在比较
        if (!MD5.encrypt(password).equals(mobileMember.getPassword())) {
            throw new GuliException(20001, "密码错误,登录失败!");
        }

        //4.判断用户是否被禁(封号)
        if (mobileMember.getIsDisabled()) {
            throw new GuliException(20001, "用户已被禁止登录,登录失败!");
        }
        //调用JWT工具类生成token
        //传入id,nickname
        return JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());
    }

    //注册
    @Override
    public void register(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)) {
            throw new GuliException(20001, "传入参数不能为空!,注册失败");
        }
        //从redis取出验证码
        String redisCode = redisTemplate.opsForValue().get(mobile);
		//判断验证码是否失效
        if (StringUtils.isBlank(redisCode)) {
            throw new GuliException(20001, "验证码失效!,注册失败");
        }
        //判断验证码是否正确
        if (!code.equals(redisCode)) {
            throw new GuliException(20001, "验证码错误!,注册失败");
        }
        //判断手机号是否已经注册过
        LambdaQueryWrapper<UcenterMember> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(UcenterMember::getMobile, mobile);
        Integer count = baseMapper.selectCount(queryWrapper);
        if (count > 0) {
            throw new GuliException(20001, "该手机号已经被注册!注册失败");
        }
        //添加到数据库
        UcenterMember member = new UcenterMember();
        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获取信息传给前端
    @Override
    public UcentMemberVo getLoginInfo(String memberId) {
        UcenterMember member = baseMapper.selectById(memberId);
        UcentMemberVo ucentMemberVo = new UcentMemberVo();
        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")
public class MsmController {
    @Autowired
    private MsmService msmService;

    @Autowired
    private RedisTemplate<String, String> reditemplate;

    //通过手机号发送短信的方法
    @GetMapping("send/{phone}")
    public R sendMsm(@PathVariable("phone") String phone) {
        //1.从redis获取验证码,如果获取到直接返回
        String code = reditemplate.opsForValue().get(phone);
        if (!StringUtils.isEmpty(code)) {
            return R.ok();
        }

        //2.调用工具类生成四位随机数,传递给阿里云进行发送
        code = RandomUtil.getFourBitRandom();
        Map<String, Object> param = new HashMap<>();
        param.put("code", code);
        //3.调用service里的方法实现短信发送
        boolean isSend = msmService.send(param, phone);
        if (isSend) {
            //4.发送成功,把发送成功的验证码放到redis中去并设置有效时间
            reditemplate.opsForValue().set(phone, code, 5, TimeUnit.MINUTES);
            return R.ok();
        } else {
            //5.发送失败,返回失败信息
            return R.error().message("短信发送失败!");
        }
    }
}

service层:

/**
 * @author xppll
 * @date 2021/12/8 19:53
 */
@Service
public class MsmServiceImpl implements MsmService {
    /**
     * 发送短信
     * @param param 需要阿里云发送的验证码
     * @param phone 手机号
     * @return
     */
    @Override
    public boolean send(Map<String, Object> param, String phone) {

        //手机号为空,返回false
        if (StringUtils.isEmpty(phone)) return false;
        //地域节点,id,密钥
        DefaultProfile profile =
            DefaultProfile.getProfile("default", "xxx", "xxx");
        IAcsClient client = new DefaultAcsClient(profile);

        //设置相关参数
        CommonRequest request = new CommonRequest();
        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();
        }
        return false;
    }
}

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="网站统计日数据")
public class StatisticsDaily implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "主键")
    @TableId(value = "id", type = IdType.ID_WORKER_STR)
    private String id;

    @ApiModelProperty(value = "统计日期")
    private String dateCalculated;

    @ApiModelProperty(value = "注册人数")
    private Integer registerNum;

    @ApiModelProperty(value = "登录人数")
    private Integer loginNum;

    @ApiModelProperty(value = "每日播放视频数")
    private Integer videoViewNum;

    @ApiModelProperty(value = "每日新增课程数")
    private Integer courseNum;

    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreate;

    @ApiModelProperty(value = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;

}

13.2 实现接口

service_ucenter模块创建接口,统计某一天的注册人数:

controller层:

//查询某一天的注册人数
@GetMapping("countRegister/{day}")
public R countRegister(@PathVariable("day") String day){
    Integer count=memberService.countRegisterDay(day);
    return R.ok().data("countRegister",count);
}

service层:

@Override
public Integer countRegisterDay(String day) {
    return baseMapper.countRegisterDay(day);
}

mapper层:

<!--查询某一天的注册人数-->
<select id="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")
public interface UcenterClient {
    //查询某一天的注册人数
    @GetMapping("/educenter/member/countRegister/{day}")
    public R countRegister(@PathVariable("day") String day);
}

controller层:

//统计某一天的注册人数,生成统计数据
@PostMapping("registerCount/{day}")
public R registerCount(@PathVariable("day") String day){
    staService.registerCount(day);
    return R.ok();
}

service层:

@Service
public class StatisticsDailyServiceImpl extends ServiceImpl<StatisticsDailyMapper, StatisticsDaily> implements StatisticsDailyService {
    @Autowired
    private UcenterClient ucenterClient;

    @Override
    public void registerCount(String day) {
        //先删除数据库该天的记录,然后重写添加,防止添加多个
        LambdaQueryWrapper<StatisticsDaily> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(StatisticsDaily::getDateCalculated, day);
        baseMapper.delete(queryWrapper);
        //远程调用得到某一天的注册人数
        R register = ucenterClient.countRegister(day);
        Integer count = (Integer) register.getData().get("countRegister");
        //把获取的数据添加到数据库,统计分析表里
        StatisticsDaily sta = new StatisticsDaily();
        //注册人数
        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
 */
@Component
public class ScheduledTask {

    @Autowired
    private StatisticsDailyService staService;

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

这里的日期转换工具类DateUtil

/**
 * 日期操作工具类
 *
 * @author qy
 * @since 1.0
 */
public class DateUtil {

    private static final String dateFormat = "yyyy-MM-dd";

    /**
     * 格式化日期
     *
     * @param date
     * @return
     */
    public static String formatDate(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
        return sdf.format(date);

    }

    /**
     * 在日期date上增加amount天 。
     *
     * @param date   处理的日期,非null
     * @param amount 要加的天数,可能为负数
     */
    public static Date addDays(Date date, int amount) {
        Calendar now = Calendar.getInstance();
        now.setTime(date);
        now.set(Calendar.DATE, now.get(Calendar.DATE) + amount);
        return now.getTime();
    }

    public static void main(String[] args) {
        System.out.println(DateUtil.formatDate(new Date()));
        System.out.println(DateUtil.formatDate(DateUtil.addDays(new Date(), -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解决跨域了:

@Configuration
public class CorsConfig {
    @Bean
    public CorsWebFilter corsFilter() {
        CorsConfiguration config = new CorsConfiguration();
        //允许的跨域ajax的请求方式
        config.addAllowedMethod("*");
        //允许哪些网站的跨域请求 ,这里*就是全部
        config.addAllowedOrigin("*");
        //允许在请求中携带的头信
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }
}

16.4 Filter使用

创建全局Filter类,需要实现GlobalFilter接口,统一处理会员登录与外部不允许访问的服务:

/**
 * 全局Filter,统一处理会员登录与外部不允许访问的服务
 *
 * @author qy
 * @since 2019-11-21
 */
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<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();
                return out(response);
            } else {
                //拦截请求
                ServerHttpResponse response = exchange.getResponse();
                return out(response);
            }
        }
        //内部服务接口,不允许外部访问
        if (antPathMatcher.match("/**/inner/**", path)) {
            //拦截请求
            ServerHttpResponse response = exchange.getResponse();
            return out(response);
        }
        //放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }

    private Mono<Void> out(ServerHttpResponse response) {
        JsonObject message = new JsonObject();
        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})
public class ErrorHandlerConfig {

    private final ServerProperties serverProperties;

    private final ApplicationContext applicationContext;

    private final ResourceProperties resourceProperties;

    private final List<ViewResolver> viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public ErrorHandlerConfig(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)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
        JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(
            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);
    }

    /**
     * 获取异常属性
     */
    @Override
    protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Map<String, Object> map = new HashMap<>();
        map.put("success", false);
        map.put("code", 20005);
        map.put("message", "网关失败");
        map.put("data", null);
        return map;
    }

    /**
     * 指定响应处理方法为JSON处理的方法
     *
     * @param errorAttributes
     */
    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    /**
     * 根据code获取对应的HttpStatus
     *
     * @param errorAttributes
     */
    @Override
    protected int getHttpStatus(Map<String, Object> errorAttributes) {
        return 200;
    }
}

17.Redis进行缓存

首页数据通过Redis进行缓存,Redis缓存配置类RedisConfig

//开启缓存
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory
                                                               factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
                Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        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;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
                Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        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管理、订单管理、权限管理等功能

在这里插入图片描述

Logo

获得宣传、场地、资金和创业导师资源

更多推荐