谷粒学院项目总结
文章目录谷粒学院项目总结1.项目介绍1.1 采用的商业模式1.2 功能模块1.3 采用技术2.Mybatis-Plus相关配置2.1 配置分页插件2.2 自动填充2.3 代码生成器3.Swagger配置4.统一返回数据格式4.1 统一结果返回类4.2 统一定义返回码5.统一异常处理5.1 创建统一异常处理器5.2 自定义异常处理6.统一日志处理6.1 配置日志级别6.2 Logback日志6.3
文章目录
谷粒学院项目总结
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")
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);
}
}
在需要自动填充的字段加上注解:
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模块,可以看到:
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
)的行为是分等级的。如下表所示: 分为: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"?>
<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 将错误日志输出到文件
举个例子:
GlobalExceptionHandler
中类上添加注解@Slf4j
- 异常输出语句:
log.error(e.getMessage());
7.整合阿里云OSS
8.整合EasyExcel
9.整合阿里云视频点播
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
*/
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层
主要有三个接口:
- 登录
- 注册
- 登录成功后,根据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.整合微信扫描登录
13.定时统计每天的注册人数
13.1 数据库表和实体类
数据库表statistics_daily
:
对应的实体类:
/**
* 网站统计日数据
*
* @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.整合微信支付
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
解决跨域了:
@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;
}
}
用在查询教师的方法上:
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管理、订单管理、权限管理等功能
更多推荐
所有评论(0)