在线刷题考试网站
项目初始化
项目搭建
maven创建 pom.xml
<!-- 引入 Spring Boot 统一版本父项目管理依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
数据库表
- 用户表(id、姓名、密码、专业、班级、头像、邮箱、角色、状态、登录ip、注册ip、创建时间、更新时间)
create table tb_user(
id varchar(20) not null,
user_name varchar(12) not null COMMENT '姓名',
password varchar(64) not null COMMENT '密码',
subject_id varchar(20) COMMENT '专业id',
class_id varchar(20) COMMENT '班级id',
avatar varchar(32) not null COMMENT '头像',
email varchar(24) not null COMMENT '邮箱',
sign text COMMENT '签名',
role varchar(12) not null COMMENT '角色',
state varchar(1) not null COMMENT '状态',
login_ip varchar(32) not null COMMENT '登录ip',
reg_ip varchar(32) not null COMMENT '注册ip',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- 题目表(id、题目标题、题目信息、题目答案、题目分类、题目类型(选择题、简答题、填空题)、状态、创建时间、更新时间)
create table tb_topic(
id varchar(20) not null,
title varchar(24) not null COMMENT '题目标题',
content text not null COMMENT '题目内容',
solution text COMMENT '题目答案',
value int COMMENT '分数',
category_id varchar(20) not null COMMENT '题目分类',
type varchar(1) not null COMMENT '题目类型(0为单选,1为多选,2为判断,3为填空,4为问答)',
state varchar(1) not null COMMENT '状态',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- 专业表(id、专业名称、专业描述、状态、创建时间、更新时间)
create table tb_subject(
id varchar(20) not null,
name varchar(12) not null COMMENT '专业名称',
description varchar(1024) not null COMMENT '专业描述',
state varchar(1) not null COMMENT '状态',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- 评论表(id、parent_id、题目id、用户id、评论内容、创建时间、更新时间)
create table tb_comment(
id varchar(20) not null,
parent_id varchar(20) COMMENT '父评论id',
topic_id varchar(20) not null COMMENT '题目id',
user_id varchar(20) not null COMMENT '用户id',
content text not null COMMENT '内容',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- 班级表(id、班级名称、状态、创建时间、更新时间)
create table tb_class(
id varchar(20) not null,
name varchar(24) not null COMMENT '班级名称',
state varchar(1) not null COMMENT '状态',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- 分类表(id、分类名称、分类描述、状态、创建时间、更新时间)
create table tb_category(
id varchar(20) not null,
name varchar(24) not null COMMENT '分类名称',
description varchar(1024) not null COMMENT '分类描述',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- 设置表(id、key、value、创建时间、更新时间)
create table tb_setting(
id varchar(20) not null,
`key` varchar(512) not null COMMENT '键',
`value` varchar(1024) not null COMMENT '值',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- refreshToken表(id、refreshToken、用户id,token_key、mobile_token_key、创建时间、更新时间)
create table tb_refresh_token(
id varchar(20) not null,
refresh_token text not null COMMENT 'refreshToken',
user_id varchar(20) not null COMMENT '用户id',
pc_token_key varchar(34) COMMENT 'pc端tokenKey',
mobile_token_key varchar(34) COMMENT '手机端tokenKey',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- 图片表(id、url、存放位置、名称、类型、状态、创建时间、更新时间)
create table tb_image(
id varchar(20) not null,
user_id varchar(20) not null COMMENT '用户id',
url varchar(255) not null COMMENT '图片地址',
name varchar(255) not null COMMENT '图片名称',
path varchar(255) not null COMMENT '图片路径',
type varchar(24) not null COMMENT '图片类型',
origin varchar(12) not null COMMENT '图片来源',
state varchar(1) not null COMMENT '状态',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
);
- 用户表与专业表、班级表相关联
alter table tb_user add foreign key fk_user_subject (subject_id) references tb_subject (id);
alter table tb_user add foreign key fk_user_class (class_id) references tb_class (id);
- 题目表与分类表相关联
alter table tb_topic add foreign key fk_topic_category (category_id) references tb_category (id);
- 评论表与题目表相关联
alter table tb_comment add foreign key fk_comment_topic (topic_id) references tb_topic (id);
- 图片表与用户表相关联
alter table tb_image add foreign key fk_image_user (user_id) references tb_user (id);
创建目录结构和bean类 and mapper
sql依赖
pom.xml
<!--数据库依赖-->
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
lombok依赖
pom.xml
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
pojo脚本
- 充分解决写bean类之烦恼
- 命名规范
- 表命名 tb_xxx
- 字段命名 user_name login_ip category_id
import com.intellij.database.model.DasTable
import com.intellij.database.util.Case
import com.intellij.database.util.DasUtil
/*
* Available context bindings:
* SELECTION Iterable<DasObject>
* PROJECT project
* FILES files helper
*/
packageName = "com.nature.exercise.pojo;"
typeMapping = [
(~/(?i)int/) : "long",
(~/(?i)float|double|decimal|real/): "double",
(~/(?i)timestamp/) : "java.sql.Timestamp",
(~/(?i)date|datetime/) : "Date",
(~/(?i)time/) : "java.sql.Time",
(~/(?i)/) : "String"
]
FILES.chooseDirectoryAndSave("Choose directory", "Choose where to store generated files") { dir ->
SELECTION.filter { it instanceof DasTable }.each { generate(it, dir) }
}
def generate(table, dir) {
def className = javaName(table.getName(), true)
def targetClassName = className.substring(2, className.size())
def fields = calcFields(table)
new File(dir, targetClassName + ".java").withPrintWriter { out -> generate(out, targetClassName, fields, table) }
}
def generate(out, className, fields, table) {
out.println "package $packageName"
out.println ""
out.println "import lombok.Data;"
out.println "import lombok.AllArgsConstructor;"
out.println "import lombok.NoArgsConstructor;"
out.println "import java.util.Date;"
out.println ""
out.println "@Data"
out.println "@AllArgsConstructor"
out.println "@NoArgsConstructor"
out.println "public class $className {"
out.println ""
fields.each() {
String[] nameSplits = it.name.split("_")
String newName = "";
if (nameSplits.length >= 2){
boolean sign = false;
for (String tempStr in nameSplits){
String tempStart = tempStr[0];
String tempSubstr = tempStr.substring(1, tempStr.length());
if (sign){
tempStart = tempStart.toUpperCase();
}
newName += (tempStart + tempSubstr);
sign = true;
}
}else {
newName = it.name
}
out.println " \tprivate ${it.type} ${newName};"
out.println ""
}
out.println "}"
}
def calcFields(table) {
DasUtil.getColumns(table).reduce([]) { fields, col ->
def spec = Case.LOWER.apply(col.getDasType().getSpecification())
def typeStr = typeMapping.find { p, t -> p.matcher(spec).find() }.value
fields += [[
//name : javaName(col.getName(), false),
name : col.getName(),
type : typeStr,
annos: ""]]
}
}
def javaName(str, capitalize) {
def s = com.intellij.psi.codeStyle.NameUtil.splitNameIntoWords(str)
.collect { Case.LOWER.apply(it).capitalize() }
.join("")
.replaceAll(/[^\p{javaJavaIdentifierPart}[_]]/, "_")
capitalize || s.length() == 1 ? s : Case.LOWER.apply(s[0]) + s[1..-1]
}
application.yml文件配置
server:
port: 2024
spring:
application:
name: nature-exercise-system
datasource:
druid:
url: jdbc:mysql://192.168.226.128:3306/nature_exercise_system?characterEncoding=utf-8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
# mybatis
mybatis:
# mapper文件的存放位置 classpath指向的是resource文件夹
mapper-locations: classpath:/mapper/*.xml
# 是否开启驼峰命名
# 包别名
type-aliases-package: com.nature.exercise.pojo
configuration:
map-underscore-to-camel-case: true
配置SpringSecurity
package com.nature.exercise.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//所有都放行
http.authorizeRequests()
.antMatchers("/**").permitAll()
.and().csrf().disable();
}
}
配置Swagger
package com.nature.exercise.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
@Configuration
public class Swagger2Configuration {
//版本
public static final String VERSION = "1.0.0";
/**
* 门户api,接口前缀:portal
*
* @return
*/
@Bean
public Docket portalApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(portalApiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.nature.exercise.controller.portal"))
.paths(PathSelectors.any()) // 可以根据url路径设置哪些请求加入文档,忽略哪些请求
.build()
.groupName("前端门户");
}
private ApiInfo portalApiInfo() {
return new ApiInfoBuilder()
.title("在线练习考试系统门户接口文档") //设置文档的标题
.description("门户接口文档") // 设置文档的描述
.version(VERSION) // 设置文档的版本信息-> 1.0.0 Version information
.build();
}
/**
* 管理中心api,接口前缀:admin
*
* @return
*/
@Bean
public Docket adminApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(adminApiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.nature.exercise.controller.admin"))
.paths(PathSelectors.any()) // 可以根据url路径设置哪些请求加入文档,忽略哪些请求
.build()
.groupName("管理中心");
}
private ApiInfo adminApiInfo() {
return new ApiInfoBuilder()
.title("在线练习考试系统管理中心接口文档") //设置文档的标题
.description("管理中心接口") // 设置文档的描述
.version(VERSION) // 设置文档的版本信息-> 1.0.0 Version information
.build();
}
@Bean
public Docket UserApi() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(userApiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.nature.exercise.controller.user"))
.paths(PathSelectors.any()) // 可以根据url路径设置哪些请求加入文档,忽略哪些请求
.build()
.groupName("用户中心");
}
private ApiInfo userApiInfo() {
return new ApiInfoBuilder()
.title("在线练习考试系统用户接口") //设置文档的标题
.description("用户接口文档") // 设置文档的描述
.version(VERSION) // 设置文档的版本信息-> 1.0.0 Version information
.build();
}
}
idWoker id生成
package com.nature.exercise.utils;
public class IdWorker {
// ==============================Fields===========================================
/** 开始时间截 (2015-01-01) */
private final long twepoch = 1420041600000L;
/** 机器id所占的位数 */
private final long workerIdBits = 5L;
/** 数据标识id所占的位数 */
private final long datacenterIdBits = 5L;
/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大数据标识id,结果是31 */
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位数 */
private final long sequenceBits = 12L;
/** 机器ID向左移12位 */
private final long workerIdShift = sequenceBits;
/** 数据标识id向左移17位(12+5) */
private final long datacenterIdShift = sequenceBits + workerIdBits;
/** 时间截向左移22位(5+5+12) */
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 工作机器ID(0~31) */
private long workerId;
/** 数据中心ID(0~31) */
private long datacenterId;
/** 毫秒内序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的时间截 */
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public IdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
}
密码加密
@Bean
public BCryptPasswordEncoder createBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
natureUser.setPassword(bCryptPasswordEncoder.encode(natureUser.getPassword()));
初始化管理员账户
/**
* 初始化管理员用户
*
* @param natureUser 管理员信息
* @return result
*/
@Override
public ResponseResult initAdminAccount(NatureUser natureUser) {
// 查看管理员账号是否初始化
if (settingMapper.hasOneByKey(Constants.SETTING.ADMIN_ACCOUNT_STATE) != null){
return ResponseResult.FAILURE("管理员账号已经初始化");
}
// 检查数据
// 必填数据userName password email
if (TextUtils.isEmpty(natureUser.getUserName())) {
return ResponseResult.FAILURE("用户名不能为空");
}
if (TextUtils.isEmpty(natureUser.getPassword())){
return ResponseResult.FAILURE("密码不能为空");
}
if (TextUtils.isEmpty(natureUser.getEmail())){
return ResponseResult.FAILURE("邮箱不能为空");
}
// 填充数据
// 密码加密
natureUser.setPassword(bCryptPasswordEncoder.encode(natureUser.getPassword()));
natureUser.setId(String.valueOf(idWorker.nextId()));
natureUser.setAvatar(Constants.USER.DEFAULT_AVATAR);
natureUser.setRole(Constants.USER.ROLE_ADMIN);
natureUser.setLoginIp(getRequest().getRemoteAddr());
natureUser.setRegIp(getRequest().getRemoteAddr());
natureUser.setCreateTime(new Date());
natureUser.setUpdateTime(new Date());
// 保存到数据库
boolean userResult = natureUserMapper.saveOne(natureUser);
// 更新setting表
Setting setting = new Setting();
setting.setId(String.valueOf(idWorker.nextId()));
setting.setKey(Constants.SETTING.ADMIN_ACCOUNT_STATE);
setting.setValue(Constants.DEFAULT_STATE);
setting.setCreateTime(new Date());
setting.setUpdateTime(new Date());
// 保存到数据库
boolean settingResult = settingMapper.saveOne(setting);
return userResult && settingResult ? ResponseResult.SUCCESS("初始化管理员信息成功") : ResponseResult.FAILURE("初始化管理员信息失败");
}
redis
- pom.xml
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- redis配置类
@Configuration
public class RedisConfiguration {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<String, Object> redisTemplate(){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
- redis工具类
package com.nature.exercise.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Resource
private RedisTemplate redisTemplate;
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
private boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
private boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
public void expire(int i, int i1, int i2) {
}
}
线程池
- 配置类
@Configuration
@EnableAsync
public class AsyncConfiguration {
@Bean
public Executor asyncExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setThreadNamePrefix("nature-exercise-task-worker-");
executor.setQueueCapacity(30);
executor.initialize();
return executor;
}
}
注册实现
发送验证码实现
/**
* 发送邮箱验证码
*
* @param emailAddress 邮箱地址
* @param type 什么功能发送验证码
* "register" and "update" 注册用户与更新邮箱验证码
* "forget" 找回密码邮箱验证码
* @param request 获取ip地址
* @return result
*/
@Override
public ResponseResult sendVerifyCode(String emailAddress, String type, HttpServletRequest request) {
// 检查数据
if (TextUtils.isEmpty(emailAddress)){
return ResponseResult.FAILURE("邮箱地址不能为空");
}
if (!TextUtils.isEmailAddress(emailAddress)){
return ResponseResult.FAILURE("邮箱地址格式不正确");
}
// 查看类型
if (Constants.USER.REGISTER_EMAIL.equals(type) || Constants.USER.UPDATE_EMAIL.equals(type)){
// 查找数据库,邮箱地址是否存在
Integer count = natureUserMapper.hasOneByEmail(emailAddress);
if (count != null){
return ResponseResult.FAILURE("邮箱已经注册");
}
}else if (Constants.USER.FORGET_EMAIL.equals(type)){
Integer count = natureUserMapper.hasOneByEmail(emailAddress);
if (count == null){
return ResponseResult.FAILURE("邮箱未注册");
}
}else {
return ResponseResult.GET(ResponseState.NO_FUNCTION);
}
// 防止频繁发送邮箱验证码
// 每一个邮箱每隔30s,可以发送一次验证码. 每一个ip一小时内可以发送10次验证码
// ip格式规范(将:改为- 解决redis bug) 本地需要解决,线上不需要解决
String ipAddress = request.getRemoteAddr();
String ipAddressToCache = ipAddress.replaceAll(":", "_");
// 邮箱地址限制
Object emailCache = redisUtil.get(Constants.USER.KEY_EMAIL_ADDRESS_CACHE + emailAddress);
if (emailCache != null){
return ResponseResult.FAILURE("您验证码发送的也太频繁了,请30秒后再发送.");
}
// ip地址限制
String ipSendTimeStr = (String) redisUtil.get(Constants.USER.KEY_IP_ADDRESS_CACHE + ipAddressToCache);
Integer ipSendTime = null;
if (ipSendTimeStr != null) ipSendTime = Integer.valueOf(ipSendTimeStr);
if (ipSendTimeStr != null && ipSendTime > 10){
return ResponseResult.FAILURE("您电脑IP发送的验证码也太频繁了,请1小时后再发送.");
}
// 发送邮箱验证码
// 生成邮箱验证码
int verifyCode = random.nextInt(999999);
if (verifyCode < 100000){
verifyCode += 1000000;
}
log.info("verifyCode ==> " + verifyCode);
try {
taskService.sendVerifyCode(String.valueOf(verifyCode), emailAddress);
} catch (Exception e) {
return ResponseResult.FAILURE("验证码发送失败,请稍后重试.");
}
// 发送成功,频繁发送限制
// 邮箱地址
redisUtil.set(Constants.USER.KEY_EMAIL_ADDRESS_CACHE + emailAddress, "1", Constants.TIME.MINUTE_HALF);
// ip地址
if (ipSendTime == null){
ipSendTime = 0;
}
ipSendTime++;
redisUtil.set(Constants.USER.KEY_IP_ADDRESS_CACHE + ipAddressToCache, String.valueOf(ipSendTime), Constants.TIME.HOUR);
// 验证码放入redis中,注册时对比 十分钟有效
redisUtil.set(Constants.USER.KEY_VERIFY_CODE_CACHE + emailAddress, String.valueOf(verifyCode), Constants.TIME.MINUTE_10);
return ResponseResult.SUCCESS("验证码发送成功");
}
异步发送邮件
@Service
public class TaskService {
@Async
public void sendVerifyCode(String verifyCode, String emailAddress) throws Exception {
EmailSender.sendVerifyCode(verifyCode, emailAddress);
}
}
发送邮件工具类
package com.nature.exercise.utils;
import lombok.extern.slf4j.Slf4j;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.mail.*;
import javax.mail.internet.*;
import java.io.File;
import java.net.URL;
import java.util.*;
@Slf4j
public class EmailSender {
private static final String TAG = "EmailSender";
private static Session session;
private static String user;
private MimeMessage msg;
private String text;
private String html;
private List<MimeBodyPart> attachments = new ArrayList<MimeBodyPart>();
private EmailSender() {
EmailSender.config(EmailSender.SMTP_163(false), "19853809805@163.com", "TDJFSHSFINSNIHUK");
}
public static Properties defaultConfig(Boolean debug) {
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.transport.protocol", "smtp");
props.put("mail.debug", null != debug ? debug.toString() : "false");
props.put("mail.smtp.timeout", "10000");
props.put("mail.smtp.port", "465");
return props;
}
/**
* smtp entnterprise qq
*
* @param debug
* @return
*/
public static Properties SMTP_ENT_QQ(boolean debug) {
Properties props = defaultConfig(debug);
props.put("mail.smtp.host", "smtp.exmail.qq.com");
return props;
}
/**
* smtp qq
*
* @param debug enable debug
* @return
*/
public static Properties SMTP_QQ(boolean debug) {
Properties props = defaultConfig(debug);
props.put("mail.smtp.host", "smtp.qq.com");
return props;
}
/**
* smtp 163
*
* @param debug enable debug
* @return
*/
public static Properties SMTP_163(Boolean debug) {
Properties props = defaultConfig(debug);
props.put("mail.smtp.host", "smtp.163.com");
return props;
}
/**
* config username and password
*
* @param props email property config
* @param username email auth username
* @param password email auth password
*/
public static void config(Properties props, final String username, final String password) {
props.setProperty("username", username);
props.setProperty("password", password);
config(props);
}
public static void config(Properties props) {
final String username = props.getProperty("username");
final String password = props.getProperty("password");
user = username;
session = Session.getInstance(props, new Authenticator() {
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(username, password);
}
});
}
/**
* set email subject
*
* @param subject subject title
*/
public static EmailSender subject(String subject) {
EmailSender EmailSender = new EmailSender();
EmailSender.msg = new MimeMessage(session);
try {
EmailSender.msg.setSubject(subject, "UTF-8");
} catch (Exception e) {
log.info(TAG, e);
}
return EmailSender;
}
/**
* set email from
*
* @param nickName from nickname
*/
public EmailSender from(String nickName) {
return from(nickName, user);
}
/**
* set email nickname and from user
*
* @param nickName from nickname
* @param from from email
*/
public EmailSender from(String nickName, String from) {
try {
String encodeNickName = MimeUtility.encodeText(nickName);
msg.setFrom(new InternetAddress(encodeNickName + " <" + from + ">"));
} catch (Exception e) {
e.printStackTrace();
}
return this;
}
public EmailSender replyTo(String... replyTo) {
String result = Arrays.asList(replyTo).toString().replaceAll("(^\\[|\\]$)", "").replace(", ", ",");
try {
msg.setReplyTo(InternetAddress.parse(result));
} catch (Exception e) {
e.printStackTrace();
}
return this;
}
public EmailSender replyTo(String replyTo) {
try {
msg.setReplyTo(InternetAddress.parse(replyTo.replace(";", ",")));
} catch (Exception e) {
e.printStackTrace();
}
return this;
}
public EmailSender to(String... to) throws MessagingException {
return addRecipients(to, Message.RecipientType.TO);
}
public EmailSender to(String to) throws MessagingException {
return addRecipient(to, Message.RecipientType.TO);
}
public EmailSender cc(String... cc) throws MessagingException {
return addRecipients(cc, Message.RecipientType.CC);
}
public EmailSender cc(String cc) throws MessagingException {
return addRecipient(cc, Message.RecipientType.CC);
}
public EmailSender bcc(String... bcc) throws MessagingException {
return addRecipients(bcc, Message.RecipientType.BCC);
}
public EmailSender bcc(String bcc) throws MessagingException {
return addRecipient(bcc, Message.RecipientType.BCC);
}
private EmailSender addRecipients(String[] recipients, Message.RecipientType type) throws MessagingException {
String result = Arrays.asList(recipients).toString().replace("(^\\[|\\]$)", "").replace(", ", ",");
msg.setRecipients(type, InternetAddress.parse(result));
return this;
}
private EmailSender addRecipient(String recipient, Message.RecipientType type) throws MessagingException {
msg.setRecipients(type, InternetAddress.parse(recipient.replace(";", ",")));
return this;
}
public EmailSender text(String text) {
this.text = text;
return this;
}
public EmailSender html(String html) {
this.html = html;
return this;
}
public EmailSender attach(File file) {
attachments.add(createAttachment(file, null));
return this;
}
public EmailSender attach(File file, String fileName) {
attachments.add(createAttachment(file, fileName));
return this;
}
public EmailSender attachURL(URL url, String fileName) {
attachments.add(createURLAttachment(url, fileName));
return this;
}
private MimeBodyPart createAttachment(File file, String fileName) {
MimeBodyPart attachmentPart = new MimeBodyPart();
FileDataSource fds = new FileDataSource(file);
try {
attachmentPart.setDataHandler(new DataHandler(fds));
attachmentPart.setFileName(null == fileName ? MimeUtility.encodeText(fds.getName()) : MimeUtility.encodeText(fileName));
} catch (Exception e) {
e.printStackTrace();
}
return attachmentPart;
}
private MimeBodyPart createURLAttachment(URL url, String fileName) {
MimeBodyPart attachmentPart = new MimeBodyPart();
DataHandler dataHandler = new DataHandler(url);
try {
attachmentPart.setDataHandler(dataHandler);
attachmentPart.setFileName(null == fileName ? MimeUtility.encodeText(fileName) : MimeUtility.encodeText(fileName));
} catch (Exception e) {
e.printStackTrace();
}
return attachmentPart;
}
public void send() {
if (text == null && html == null) {
throw new IllegalArgumentException("At least one context has to be provided: Text or Html");
}
MimeMultipart cover;
boolean usingAlternative = false;
boolean hasAttachments = attachments.size() > 0;
try {
if (text != null && html == null) {
// TEXT ONLY
cover = new MimeMultipart("mixed");
cover.addBodyPart(textPart());
} else if (text == null && html != null) {
// HTML ONLY
cover = new MimeMultipart("mixed");
cover.addBodyPart(htmlPart());
} else {
// HTML + TEXT
cover = new MimeMultipart("alternative");
cover.addBodyPart(textPart());
cover.addBodyPart(htmlPart());
usingAlternative = true;
}
MimeMultipart content = cover;
if (usingAlternative && hasAttachments) {
content = new MimeMultipart("mixed");
content.addBodyPart(toBodyPart(cover));
}
for (MimeBodyPart attachment : attachments) {
content.addBodyPart(attachment);
}
msg.setContent(content);
msg.setSentDate(new Date());
Transport.send(msg);
log.info("email send success...");
} catch (Exception e) {
e.printStackTrace();
}
}
private MimeBodyPart toBodyPart(MimeMultipart cover) throws MessagingException {
MimeBodyPart wrap = new MimeBodyPart();
wrap.setContent(cover);
return wrap;
}
private MimeBodyPart textPart() throws MessagingException {
MimeBodyPart bodyPart = new MimeBodyPart();
bodyPart.setText(text);
return bodyPart;
}
private MimeBodyPart htmlPart() throws MessagingException {
MimeBodyPart bodyPart = new MimeBodyPart();
bodyPart.setContent(html, "text/html; charset=utf-8");
return bodyPart;
}
public static void sendVerifyCode(String code, String emailAddress) throws Exception {
EmailSender.subject("在线练习考试系统验证码")
.from("在线练习考试系统")
.text("【在线练习考试】 您的邮箱验证码是:" + code + ",验证码10分种内有效。")
.to(emailAddress)
.send();
}
}
邮箱格式判断
public static boolean isEmailAddress(String emailAddress){
String regEx = "^([a-z0-9A-Z]+[-|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
Pattern pattern = Pattern.compile(regEx);
Matcher matcher = pattern.matcher(emailAddress);
return matcher.matches();
}
验证码实现
- pom.xml
<!--captcha-->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
- java代码
private final int[] fontTypes = {
Captcha.FONT_1,
Captcha.FONT_2,
Captcha.FONT_3,
Captcha.FONT_4,
Captcha.FONT_5,
Captcha.FONT_6,
Captcha.FONT_7,
Captcha.FONT_8,
Captcha.FONT_9,
Captcha.FONT_10,
};
private final int[] charTypes = {
TYPE_DEFAULT,
TYPE_ONLY_NUMBER,
TYPE_ONLY_CHAR,
TYPE_ONLY_UPPER,
TYPE_ONLY_LOWER,
TYPE_NUM_AND_UPPER
};
/**
* 获取人类验证码
* @param captchaKey 时间戳
* @param response 设置数据
* @throws IOException io流异常
*/
@Override
public void getCaptcha(String captchaKey, HttpServletResponse response) throws Exception {
// 检查数据 时间戳长度不能小于13位
if (captchaKey.length() < 13){
return;
}
// 设置请求头为输出图片类型
response.setContentType("image/gif");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
// 验证码样式设置
// 随机设置验证码类型
Captcha captcha = null;
int randomNumber = random.nextInt(5);
if (randomNumber == Constants.USER.CAPTCHA_ARITHMETIC_TYPE){
captcha = new ArithmeticCaptcha(Constants.USER.CAPTCHA_WIDTH, Constants.USER.CAPTCHA_HEIGHT);
} else if (randomNumber == Constants.USER.CAPTCHA_GIF_TYPE) {
captcha = new GifCaptcha(Constants.USER.CAPTCHA_WIDTH, Constants.USER.CAPTCHA_HEIGHT);
}else if (randomNumber == Constants.USER.CAPTCHA_CHINESE_TYPE){
captcha = new ChineseCaptcha(Constants.USER.CAPTCHA_WIDTH, Constants.USER.CAPTCHA_HEIGHT);
}else if (randomNumber == Constants.USER.CAPTCHA_CHINESE_GIF_TYPE){
captcha = new ChineseGifCaptcha(Constants.USER.CAPTCHA_WIDTH, Constants.USER.CAPTCHA_HEIGHT);
}else {
captcha = new SpecCaptcha(Constants.USER.CAPTCHA_WIDTH, Constants.USER.CAPTCHA_HEIGHT);
}
// 随机设置字体
if (randomNumber == Constants.USER.CAPTCHA_CHINESE_TYPE
|| randomNumber == Constants.USER.CAPTCHA_CHINESE_GIF_TYPE
|| randomNumber == Constants.USER.CAPTCHA_ARITHMETIC_TYPE){
captcha.setFont(new Font("楷体", Font.PLAIN, 28));
}else {
captcha.setFont(fontTypes[random.nextInt(fontTypes.length)]);
}
// 随机设置类型
captcha.setCharType(charTypes[random.nextInt(charTypes.length)]);
// 将人类验证码value放入redis
redisUtil.set(Constants.USER.KEY_CAPTCHA_VALUE_CACHE + captchaKey, captcha.text());
captcha.out(response.getOutputStream());
}
注册最终代码
/**
* 注册用户
*
* @param natureUser 用户信息
* @param verifyCode 邮箱验证码
* @param captcha 人类验证码
* @param captchaKey 人类验证码时间戳
* @return result
*/
@Override
public ResponseResult register(NatureUser natureUser, String verifyCode, String captcha, String captchaKey) {
// 检查数据 人类验证码时间戳 人类验证码 邮箱验证码
if (TextUtils.isEmpty(captchaKey)) {
return ResponseResult.GET(ResponseState.MESSAGE_FAILURE);
}
if (TextUtils.isEmpty(captcha)){
return ResponseResult.FAILURE("人类验证码不能为空");
}
if (TextUtils.isEmpty(verifyCode)){
return ResponseResult.FAILURE("邮箱验证码不能为空");
}
// 检查数据 用户名 密码 邮箱 角色
if (TextUtils.isEmpty(natureUser.getUserName())) {
return ResponseResult.FAILURE("用户名不为空");
}
if (TextUtils.isEmpty(natureUser.getPassword())){
return ResponseResult.FAILURE("密码不能为空");
}
if (TextUtils.isEmpty(natureUser.getEmail())){
return ResponseResult.FAILURE("邮箱不能为空");
}
if (TextUtils.isEmpty(natureUser.getRole())){
return ResponseResult.FAILURE("角色不能为空");
}
// 如果角色为普通用户,也就是学生的话,需要判断专业和班级是否为空
if (Constants.USER.ROLE_NORMAL.equals(natureUser.getRole())){
if (TextUtils.isEmpty(natureUser.getSubjectId())){
return ResponseResult.FAILURE("专业不能为空");
}
if (TextUtils.isEmpty(natureUser.getClassId())){
return ResponseResult.FAILURE("班级不能为空");
}
}
// 检查人类验证码 邮箱验证码 是否正确
String captchaCache = (String)redisUtil.get(Constants.USER.KEY_CAPTCHA_VALUE_CACHE + captchaKey);
if (!captcha.equalsIgnoreCase(captchaCache)){
return ResponseResult.FAILURE("人类验证码不正确");
}
String verifyCodeCache = (String)redisUtil.get(Constants.USER.KEY_VERIFY_CODE_CACHE + natureUser.getEmail());
if (!verifyCode.equals(verifyCodeCache)){
return ResponseResult.FAILURE("邮箱验证码不正确");
}
// 删除redis中的 人类验证码和邮箱验证码
redisUtil.del(Constants.USER.KEY_CAPTCHA_VALUE_CACHE + captchaKey);
redisUtil.del(Constants.USER.KEY_VERIFY_CODE_CACHE + natureUser.getEmail());
// 补全数据
// 密码加密
natureUser.setPassword(bCryptPasswordEncoder.encode(natureUser.getPassword()));
natureUser.setId(String.valueOf(idWorker.nextId()));
natureUser.setAvatar(Constants.USER.DEFAULT_AVATAR);
natureUser.setLoginIp(getRequest().getRemoteAddr());
natureUser.setRegIp(getRequest().getRemoteAddr());
natureUser.setCreateTime(new Date());
natureUser.setUpdateTime(new Date());
// 保存用户信息到数据库
boolean result = natureUserMapper.saveOne(natureUser);
return result ? ResponseResult.SUCCESS("用户注册成功") : ResponseResult.FAILURE("用户注册失败");
}
登录实现
- jwt pom.xml
<!--jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
- jwt 工具类
package com.nature.exercise.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
//盐值
private static String key = "ed755f7c166df7f72bb004f6e531aedb";
private static long ttl = Constants.TIME_MILLISECOND.HOUR_2; //毫秒为单位 2小时
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTtl() {
return ttl;
}
public void setTtl(long ttl) {
this.ttl = ttl;
}
/**
* @param claims 载荷内容
* @param ttl 有效时长
* @return
*/
public static String createToken(Map<String, Object> claims, long ttl) {
JwtUtil.ttl = ttl;
return createToken(claims);
}
public static String createRefreshToken(String userId, long ttl) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder().setId(userId)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key);
if (ttl > 0) {
builder.setExpiration(new Date(nowMillis + ttl));
}
return builder.compact();
}
/**
* @param claims 载荷
* @return token
*/
public static String createToken(Map<String, Object> claims) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, key);
if (claims != null) {
builder.setClaims(claims);
}
if (ttl > 0) {
builder.setExpiration(new Date(nowMillis + ttl));
}
return builder.compact();
}
public static Claims parseJWT(String jwtStr) {
return Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(jwtStr)
.getBody();
}
}
- claims工具类
package com.nature.exercise.utils;
import com.nature.exercise.pojo.NatureUser;
import io.jsonwebtoken.Claims;
import java.util.HashMap;
import java.util.Map;
public class ClaimsUtils {
private static final String ID = "id";
private static final String USER_NAME = "user_name";
private static final String SUBJECT_ID = "subject_id";
private static final String CLASS_ID = "class_id";
private static final String AVATAR = "avatar";
private static final String EMAIL = "email";
private static final String ROLE = "role";
private static final String FROM = "from";
public static Map<String, Object> userAndFromToClaims(NatureUser natureUser, String from){
Map<String, Object> claims = new HashMap<>();
claims.put(ID, natureUser.getId());
claims.put(USER_NAME, natureUser.getUserName());
claims.put(SUBJECT_ID, natureUser.getSubjectId());
claims.put(CLASS_ID, natureUser.getClassId());
claims.put(AVATAR, natureUser.getAvatar());
claims.put(EMAIL, natureUser.getEmail());
claims.put(ROLE, natureUser.getRole());
claims.put(FROM, from);
return claims;
}
public static NatureUser claimsToUser(Claims claims){
NatureUser natureUser = new NatureUser();
natureUser.setId((String) claims.get(ID));
natureUser.setUserName((String) claims.get(USER_NAME));
natureUser.setSubjectId((String) claims.get(SUBJECT_ID));
natureUser.setClassId((String) claims.get(CLASS_ID));
natureUser.setAvatar((String) claims.get(AVATAR));
natureUser.setEmail((String) claims.get(EMAIL));
natureUser.setRole((String) claims.get(ROLE));
return natureUser;
}
}
- cookie工具类
package com.nature.exercise.utils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class CookieUtils {
private static final String DOMAIN = "localhost";
private static final int DEFAULT_AGE = Constants.TIME.YEAR; // 1年有效期
public static void setCookie(HttpServletResponse response, String key, String value){
setCookie(response, key, value, DEFAULT_AGE);
}
/**
* 设置cookie
* @param key cookie key
* @param value cookie value
* @param age 有效期
*/
public static void setCookie(HttpServletResponse response, String key, String value, int age){
Cookie cookie = new Cookie(key, value);
cookie.setPath("/");
cookie.setDomain(DOMAIN);
cookie.setMaxAge(age);
response.addCookie(cookie);
}
/**
* 获取cookie
* @param key cookie key
* @return cookie value
*/
public static String getCookie(HttpServletRequest request, String key){
Cookie[] cookies = request.getCookies();
if (cookies == null){
return null;
}
for (Cookie cookie : cookies){
if (key.equals(cookie.getName())){
return cookie.getValue();
}
}
return null;
}
/**
* 删除cookie
*/
public static void deleteCookie(HttpServletResponse response, String key){
setCookie(response, key, null, 0);
}
}
登录代码实现
/**
* 用户登录
* @param natureUser 用户信息
* @param captcha 人类验证码
* @param captchaKey 人类验证码key
* @return result
*/
@Override
public ResponseResult doLogin(NatureUser natureUser, String captcha, String captchaKey, String from) {
// 判断客户端
if (!Constants.USER.PC_FROM_SIGN.equals(from) && !Constants.USER.MOBILE_FROM_SIGN.equals(from)){
return ResponseResult.GET(ResponseState.NO_CLIENT);
}
// 人类验证码和key,在路径上,不用进行判空操作
// 判断人类验证码是否正确
String captchaCache = (String) redisUtil.get(Constants.USER.KEY_CAPTCHA_VALUE_CACHE + captchaKey);
if (TextUtils.isEmpty(captchaCache)){
return ResponseResult.FAILURE("人类验证码已过期");
}
if (!captcha.equalsIgnoreCase(captchaCache)){
return ResponseResult.FAILURE("人类验证码不正确");
}
// 删除人类验证码
redisUtil.del(Constants.USER.KEY_CAPTCHA_VALUE_CACHE + captchaKey);
if (TextUtils.isEmpty(natureUser.getUserName()) && TextUtils.isEmpty(natureUser.getEmail())){
return ResponseResult.FAILURE("账号不能为空");
}
if (TextUtils.isEmpty(natureUser.getPassword())){
return ResponseResult.FAILURE("密码不能为空");
}
// 判断用户名是否存在(可以通过username和邮箱登录)
NatureUser userFromDb = natureUserMapper.findOneByUserName(natureUser.getUserName());
if (userFromDb == null){
userFromDb = natureUserMapper.findOneByEmail(natureUser.getEmail());
if (userFromDb == null){
return ResponseResult.FAILURE("账号不正确");
}
}
// 判断密码是否正确
boolean matches = bCryptPasswordEncoder.matches(natureUser.getPassword(), userFromDb.getPassword());
if (!matches){
return ResponseResult.FAILURE("账号密码错误");
}
// 判断用户状态
if (!Constants.USER.DEFAULT_STATE.equals(userFromDb.getState())){
return ResponseResult.GET(ResponseState.ACCOUNT_DENIED);
}
// 更新用户登录ip以及更新时间
boolean result = natureUserMapper.updateOneTimeAndIp(userFromDb.getId(), getRequest().getRemoteAddr(), new Date());
if (!result){
return ResponseResult.GET(ResponseState.DATABASE_ERROR);
}
// 密码正确 生成token以及refreshToken
createTokenAndRefreshToken(userFromDb, from);
return ResponseResult.GET(ResponseState.LOGIN_SUCCESS);
}
创建token和refreshToken
/**
* 创建token和refreshToken
* @param userFromDb 用户信息(生成token)
* @param from 客户端来源
*/
private String createTokenAndRefreshToken(NatureUser userFromDb, String from){
RefreshToken refreshTokenFromDb = refreshTokenMapper.findOneByUserId(userFromDb.getId());
// 如果refreshToken不为null,说明用户不是第一次登录,需要删除原来的redis中的token和refreshToken表中的refreshToken以及token_key
// 删除原来的token和refreshToken,重新生成新的
if (refreshTokenFromDb != null){
if (Constants.USER.PC_FROM_SIGN.equals(from)){
// 删除redis中的token(通过原来的tokenKey)
redisUtil.del(Constants.USER.KEY_TOKEN_CACHE + refreshTokenFromDb.getPcTokenKey());
}else if (Constants.USER.MOBILE_FROM_SIGN.equals(from)){
redisUtil.del(Constants.USER.KEY_TOKEN_CACHE + refreshTokenFromDb.getMobileTokenKey());
}
}
// 生成新的token与refreshToken
Map<String, Object> claims = ClaimsUtils.userAndFromToClaims(userFromDb, from);
// 有效期2小时
String token = JwtUtil.createToken(claims);
// md5加密token
String tokenMD5 = DigestUtils.md5DigestAsHex(token.getBytes());
// cookie的value tokenKey
String tokenKey = from + tokenMD5;
// 将token放入redis中 tokenKey作为key 有效期2小时
redisUtil.set(Constants.USER.KEY_TOKEN_CACHE + tokenKey, token, Constants.TIME.HOUR_2);
// 将tokenKey返回给用户
CookieUtils.setCookie(getResponse(), Constants.USER.KEY_COOKIE, tokenKey);
// 创建新的refreshToken
String refreshToken = JwtUtil.createRefreshToken(userFromDb.getId(), Constants.TIME_MILLISECOND.MONTH);
// 创建或者更新refreshToken表
if (refreshTokenFromDb != null){
if (Constants.USER.PC_FROM_SIGN.equals(from)){
// 更新refreshToken表中的refreshToken和tokenKey
refreshTokenMapper.updateOneRefreshTokenAndPcTokenKey(userFromDb.getId(), refreshToken, tokenKey);
}else if (Constants.USER.MOBILE_FROM_SIGN.equals(from)){
redisUtil.del(Constants.USER.KEY_TOKEN_CACHE + refreshTokenFromDb.getMobileTokenKey());
refreshTokenMapper.updateOneRefreshTokenAndMobileTokenKey(userFromDb.getId(), refreshToken, tokenKey);
}
// 更新refreshToken 更新日期
refreshTokenMapper.updateOneTime(userFromDb.getId(), new Date());
}else {
// 创建refresh
RefreshToken refreshTokenBean = new RefreshToken();
refreshTokenBean.setId(String.valueOf(idWorker.nextId()));
refreshTokenBean.setRefreshToken(refreshToken);
refreshTokenBean.setUserId(userFromDb.getId());
refreshTokenBean.setCreateTime(new Date());
refreshTokenBean.setUpdateTime(new Date());
if (Constants.USER.PC_FROM_SIGN.equals(from)){
refreshTokenBean.setPcTokenKey(tokenKey);
}else if (Constants.USER.MOBILE_FROM_SIGN.equals(from)){
refreshTokenBean.setMobileTokenKey(tokenKey);
}
// 保存到数据库
refreshTokenMapper.saveOne(refreshTokenBean);
}
return tokenKey;
}
检查用是否登录
- main
/**
* 检查用户是否登录
* @return 用户信息
*/
private NatureUser checkNatureUser(){
// 取出cookie中的tokenKey
String tokenKey = CookieUtils.getCookie(getRequest(), Constants.USER.KEY_COOKIE);
if (tokenKey == null){
log.info("tokenKey ==> null");
return null;
}
// 解析tokenKey
NatureUser natureUser = parseTokenKey(tokenKey);
// redis中无token或token过期(无token过期情况,token有效期与redis有效期设置为同一时间)
if (natureUser == null){
log.info("to ==> refreshToken");
// 通过tokenKey查询refreshToken
String from = tokenKey.substring(0, 2);
RefreshToken refreshToken = null;
if (Constants.USER.PC_FROM_SIGN.equals(from)){
refreshToken = refreshTokenMapper.findOneByPcTokenKey(tokenKey);
}else if (Constants.USER.MOBILE_FROM_SIGN.equals(from)){
refreshToken = refreshTokenMapper.findOneByMobileTokenKey(tokenKey);
}
if (refreshToken != null){
// 解析refreshToken
try {
JwtUtil.parseJWT(refreshToken.getRefreshToken());
// 通过refreshToken中的userId查询用户信息
NatureUser userFromDb = natureUserMapper.findOneById(refreshToken.getUserId());
// 创建新的token和refreshToken
String newTokenKey = createTokenAndRefreshToken(userFromDb, from);
return parseTokenKey(newTokenKey);
}catch (Exception e){
// refreshToken过期
return null;
}
}
return null;
}
return natureUser;
}
- 解析tokenKey
/**
* 解析tokenKey
* @param tokenKey tokenKey
* @return 用户信息
*/
private NatureUser parseTokenKey(String tokenKey){
// 在redis中查询token
String token = (String)redisUtil.get(Constants.USER.KEY_TOKEN_CACHE + tokenKey);
if (token != null){
try {
// 解析token 返回claims
Claims claims = JwtUtil.parseJWT(token);
// 解析claims 返回用户信息
return ClaimsUtils.claimsToUser(claims);
}catch (Exception e){
return null;
}
}
return null;
}
获取用户个信息
/**
* 获取用户信息
* @return result
*/
@Override
public ResponseResult getUserInfo(String userId) {
// 检查用户是否登录
NatureUser natureUser = natureUserMapper.findOneById(userId);
if (natureUser == null){
return ResponseResult.FAILURE("获取用户信息失败");
}
return ResponseResult.SUCCESS("获取用户信息成功").setData(natureUser);
}
修改用户信息
/**
* 修改用户信息
* @param userId 用户id
* @param natureUser 新的用户信息
* @return result
*/
@Override
public ResponseResult updateUserInfo(String userId, NatureUser natureUser) {
// 检查是否登录
NatureUser parseUser = checkNatureUser();
if (parseUser == null){
return ResponseResult.GET(ResponseState.NO_LOGIN);
}
// 对比登录用户id是否与修改的用户信息id一致
if (!parseUser.getId().equals(userId)){
return ResponseResult.GET(ResponseState.PERMISSION_DENIED);
}
// 可以修改的内容(userName、专业、班级、avatar、sign)
NatureUser userFromDb = natureUserMapper.findOneById(userId);
// 检查数据
String userName = natureUser.getUserName();
// 用户名
// 检查用户名是否被注册
if (TextUtils.isEmpty(userName)){
return ResponseResult.SUCCESS("用户名不能为空");
}
if (natureUserMapper.hasOneByUserName(userName) != null && !userName.equals(userFromDb.getUserName())){
return ResponseResult.FAILURE("用户名已经注册");
}
userFromDb.setUserName(userName);
// 专业
String subjectId = natureUser.getSubjectId();
if (!TextUtils.isEmpty(subjectId)){
// 判断专业是否存在
if (subjectMapper.hasOneById(subjectId) == null){
return ResponseResult.FAILURE("不存在该专业");
}
if (!subjectId.equals(userFromDb.getSubjectId())){
userFromDb.setSubjectId(subjectId);
}
}
// 班级
String classId = natureUser.getClassId();
if (!TextUtils.isEmpty(classId)){
// 判断班级是否存在
if (classMapper.hasOneById(classId) == null){
return ResponseResult.FAILURE("不存在该班级");
}
if (!classId.equals(userFromDb.getClassId())){
userFromDb.setClassId(classId);
}
}
// 头像
String avatar = natureUser.getAvatar();
if (!TextUtils.isEmpty(avatar) && !avatar.equals(userFromDb.getAvatar())){
userFromDb.setAvatar(avatar);
}
// 签名
userFromDb.setSign(natureUser.getSign());
// 补充数据
userFromDb.setUpdateTime(new Date());
// 更新到数据库
boolean result = natureUserMapper.updatePartOne(userFromDb);
// 删除redis中的token, 下一次通过refreshToken生成新的token
String tokenKey = CookieUtils.getCookie(getRequest(), Constants.USER.KEY_COOKIE);
redisUtil.del(Constants.USER.KEY_TOKEN_CACHE + tokenKey);
return result ? ResponseResult.SUCCESS("修改用户信息成功") : ResponseResult.FAILURE("修改用户信息失败");
}
获取当前登录用户
/**
* 获取当前登录用户信息
* @return result
*/
@Override
public ResponseResult getOnlineUser() {
NatureUser natureUser = checkNatureUser();
if (natureUser == null){
return ResponseResult.FAILURE("当前用户未登录");
}
return ResponseResult.SUCCESS("获取当前登录用户成功").setData(natureUser);
}
检查用户权限
package com.nature.exercise.service.impl;
import com.nature.exercise.pojo.NatureUser;
import com.nature.exercise.service.INatureUserService;
import com.nature.exercise.utils.Constants;
import com.nature.exercise.utils.CookieUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service("permission")
public class PermissionService extends BaseService {
@Autowired
INatureUserService natureUserService;
public boolean admin(){
// 先判断cookie
String tokenKey = CookieUtils.getCookie(getRequest(), Constants.USER.KEY_COOKIE);
if (tokenKey == null){
return false;
}
// 判断是否登录
NatureUser natureUser = natureUserService.checkNatureUser();
if (natureUser == null){
return false;
}
// 判断是否是管理员
if (Constants.USER.ROLE_ADMIN.equals(natureUser.getRole())){
return true;
}
return false;
}
}
禁用用户
@PreAuthorize("@permission.admin()")
@DeleteMapping("/user_info/{user_id}")
public ResponseResult disableUser(@PathVariable("user_id")String userId){
return natureUserService.disableUser(userId);
}
/**
* 禁用用户
* @param userId 用户id
* @return result
*/
@Override
public ResponseResult disableUser(String userId) {
if (natureUserMapper.hasOneById(userId) == null) {
return ResponseResult.FAILURE("用户不存在");
}
// 修改用户状态为 0
boolean userResult = natureUserMapper.updateOneState(userId);
// 强制下线用户
// 通过refreshToken表拿到用户的tokenKey
RefreshToken refreshTokenFrom = refreshTokenMapper.findOneByUserId(userId);
// 删除PC端redis中token
String pcTokenKey = refreshTokenFrom.getPcTokenKey();
if (!TextUtils.isEmpty(pcTokenKey)){
// 删除redis中的tokenKey,可能有,也可能没有
redisUtil.del(Constants.USER.KEY_TOKEN_CACHE + pcTokenKey);
}
// 删除手机端redis中token
String mobileTokenKey = refreshTokenFrom.getMobileTokenKey();
if (!TextUtils.isEmpty(mobileTokenKey)){
redisUtil.del(Constants.USER.KEY_TOKEN_CACHE + mobileTokenKey);
}
// 删除refreshToken中的该记录
boolean tokenResult = refreshTokenMapper.deleteOneById(refreshTokenFrom.getId());
return userResult && tokenResult ? ResponseResult.SUCCESS("禁用用户成功") : ResponseResult.FAILURE("禁用用户失败");
}
设置状态码统一处理
- 配置类
package com.nature.exercise.config;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
@Configuration
public class ErrorCodeConfig implements ErrorPageRegistrar {
@Override
public void registerErrorPages(ErrorPageRegistry registry) {
registry.addErrorPages(new ErrorPage(HttpStatus.FORBIDDEN, "/403"));
}
}
- controller
package com.nature.exercise.controller;
import com.nature.exercise.response.ResponseResult;
import com.nature.exercise.response.ResponseState;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ErrorPageController {
@RequestMapping("/403")
public ResponseResult page403(){
return ResponseResult.GET(ResponseState.PERMISSION_DENIED);
}
}
恢复用户
/**
* 恢复用户
* @param userId 用户id
* @return result
*/
@Override
public ResponseResult refreshUser(String userId) {
if (natureUserMapper.hasOneById(userId) == null) {
return ResponseResult.FAILURE("用户不存在");
}
boolean result = natureUserMapper.updateOneState(userId, Constants.DISABLE_STATE);
return result ? ResponseResult.SUCCESS("恢复用户成功") : ResponseResult.FAILURE("恢复用户失败");
}
获取用户列表
pagehelper插件
- pom.xml
<!--pageHelper-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
/**
* @param page 页数
* @param size 数量
* @return result
*/
@Override
public ResponseResult listUsers(int page, int size) {
// 检查页数
page = checkPage(page);
size = checkSize(size);
// 分页
PageHelper.startPage(page, size);
// 查询全部用户数据
List<NatureUser> users = natureUserMapper.findAllNoPwd();
PageInfo<NatureUser> userPageInfo = new PageInfo<>(users);
return ResponseResult.SUCCESS("获取用户列表成功").setData(userPageInfo);
}
题目表与分类表增删改查(略)
判断题目是否正确
/**
* 检查题目
* @param topicId 题目id
* @param solution 用户答案
* @return result
*/
@Override
public ResponseResult correctTopic(String topicId, String solution) {
// 检查数据
if (TextUtils.isEmpty(solution)){
return ResponseResult.FAILURE("请提交答案");
}
// 查看题目
Topic topicFromDb = topicMapper.findOneById(topicId);
// 查看类型,根据类型判断答案是否正确
String type = topicFromDb.getType();
String solutionFromDb = topicFromDb.getSolution();
// 答案正确或错误标志
boolean sign = false;
// 答案错误返回正确答案
if (Constants.TOPIC.TYPE_SINGLE_CHOICE.equals(type)){
// 单选题
sign = solutionFromDb.equalsIgnoreCase(solution);
}else if (Constants.TOPIC.TYPE_MULTIPLE_CHOICE.equals(type)){
// 多选题
// 多选题,答案通过英文符号 , 分隔
sign = checkMultipleTopic(solutionFromDb, solution, Constants.TOPIC.TYPE_MULTIPLE_CHOICE);
}else if (Constants.TOPIC.TYPE_TRUE_OR_FALSE_QUESTION.equals(type)){
// 判断题 T F
sign = solutionFromDb.equalsIgnoreCase(solution);
}else if (Constants.TOPIC.TYPE_GAP_FILLING.equals(type)){
// 填空题 多个答案用,答案通过英文符号 , 分隔
sign = !solution.contains(",") ? solutionFromDb.equals(solution) : checkMultipleTopic(solutionFromDb, solution, Constants.TOPIC.TYPE_GAP_FILLING);
}
return sign ? ResponseResult.SUCCESS("答案正确") : ResponseResult.FAILURE("答案错误").setData(solutionFromDb);
}
/**
* 判断多选题和多个填空题答案是否正确
* @param solutionFromDb 正确答案
* @param solution 用户答案
* @return 是否正确
*/
private boolean checkMultipleTopic(String solutionFromDb, String solution, String type){
String[] multipleSolution = solution.split(",");
String[] multipleSolutionFromDb = solutionFromDb.split(",");
if (multipleSolutionFromDb.length == multipleSolution.length){
int count = 0;
// 将每一个item提出来与正确答案做对比
for(String item : multipleSolution){
if (Constants.TOPIC.TYPE_MULTIPLE_CHOICE.equals(type)){
if (solutionFromDb.contains(item)) count++;
}else if (Constants.TOPIC.TYPE_GAP_FILLING.equals(type)){
while (count < multipleSolutionFromDb.length){
// 每一个填空与每一个正确答案做对比
if (matchItem(multipleSolutionFromDb, multipleSolution[count])) count++; else break;
}
}
}
return count == multipleSolutionFromDb.length;
}
return false;
}
/**
* 对比每一项
* @param multipleSolutionFromDb 正确答案数组
* @param multipleSolution 用户答案的每一项
* @return result
*/
private boolean matchItem(String[] multipleSolutionFromDb, String multipleSolution){
int count = 0;
for (String item : multipleSolutionFromDb) {
if (!item.equals(multipleSolution)) {
count++;
}
if (count == multipleSolutionFromDb.length) {
return false;
}
}
return true;
}
新增两个表(试卷表与成绩表)
- 试卷表
create table tb_paper(
id varchar(20) not null,
title varchar(24) not null COMMENT '标题',
topic_id text not null COMMENT '题目ID集合',
user_id varchar(20) not null COMMENT '用户ID',
score int not null COMMENT '成绩',
start_time datetime not null COMMENT '开始时间',
end_time datetime not null COMMENT '结束时间',
total_time datatime not null COMMENT '做题时间',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
)
create table tb_record(
id varchar(20) not null,
paper_id varchar(20) not null COMMENT '试卷ID',
user_id varchar(20) not null COMMENT '用户ID',
score int not null COMMENT '成绩',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
)
- 成绩表与试卷表和用户表相关联
alter table tb_record add foreign key fk_record_paper (paper_id) references tb_paper (id);
alter table tb_record add foreign key fk_record_user (user_id) references tb_user (id);
- 修改sql表
create table tb_paper(
id varchar(20) not null,
title varchar(24) not null COMMENT '标题',
topic_id text not null COMMENT '题目ID集合',
user_id varchar(20) not null COMMENT '用户ID',
score int not null COMMENT '成绩',
start_time datetime not null COMMENT '开始时间',
end_time datetime not null COMMENT '结束时间',
total_time datatime not null COMMENT '做题时间',
create_time datetime not null COMMENT '创建时间',
update_time datetime not null COMMENT '更新时间',
PRIMARY KEY (id)
)
alter table tb_paper add foreign key fk_paper_user (user_id) references tb_user(id);
想一想逻辑
- 管理员将试卷标题和题目id集合、开始时间、关闭时间与做题时间传入系统
- 开启一个定时器,时间为试卷的开始时间,定时器执行,任务为将数据放入redis中,时间为开始与关闭的间隔时间
定时任务
@Override
public ResponseResult addPaper(Paper paper) {
// 检查数据
if (TextUtils.isEmpty(paper.getTitle())){
return ResponseResult.FAILURE("试卷标题不能为空");
}
if (TextUtils.isEmpty(paper.getTopicId())){
return ResponseResult.FAILURE("试卷题目不能为空");
}
if (paper.getTotalTime() == 0){
return ResponseResult.FAILURE("做题时间不能为空");
}
if (paper.getStartTime() == null){
return ResponseResult.FAILURE("试卷开始时间不能为空");
}
if (paper.getEndTime() == null){
return ResponseResult.FAILURE("试卷结束时间不能为空");
}
// 填充数据
String paperId = String.valueOf(idWorker.nextId());
paper.setId(paperId);
paper.setCreateTime(new Date());
paper.setUpdateTime(new Date());
// 设置定时器 传入开始时间和id值
// 发布试题
// 计算时间戳,时间戳转化为秒
long spanTime = paper.getEndTime().getTime() - paper.getStartTime().getTime();
spanTime = TimeUnit.MILLISECONDS.toSeconds(spanTime);
taskService.executeTask(paper.getStartTime().getTime(), spanTime, paperId);
// 放入数据库
boolean result = paperMapper.saveOne(paper);
return result ? ResponseResult.SUCCESS("设置试卷成功") : ResponseResult.FAILURE("设置试卷失败");
}
@Async
public void executeTask(long date, long spanTime, String paperId){
ScheduledExecutorService localExecutor = Executors.newSingleThreadScheduledExecutor();
scheduler = new ConcurrentTaskScheduler(localExecutor);
scheduledFuture = scheduler.schedule(
() -> {
// 从表中查出试卷数据,放入redis中
Paper paper = paperMapper.findOneById(paperId);
// 通过topic_id 查出试题
String[] topicIds = paper.getTopicId().split(",");
paper.setTopics(topicMapper.findPartItem(topicIds));
String jsonPaper = gson.toJson(paper);
// id值为key
redisUtil.set(Constants.PAPER.KEY_PAPER_CACHE + paperId, jsonPaper, spanTime);
// 关闭定时器
scheduledFuture.cancel(true);
},
new Date(date));
}
提交试卷
/**
* 提交试卷
* @param paper 试卷内容
* @param paperId 试卷id
* @return result
*/
@Override
public ResponseResult postPaper(String paperId, Paper paper) {
// 检查用户是否登录
NatureUser natureUser = natureUserService.checkNatureUser();
if (natureUser == null){
return ResponseResult.GET(ResponseState.NO_LOGIN);
}
// 检查数据
int sum = 0;
// 计算总分
List<Topic> topics = paper.getTopics();
Map<String, Object> result = null;
for (Topic topic : topics){
result = topicService.mainCorrect(topic.getId(), topic.getSolution());
if ((boolean)result.get("sign")){
sum += topic.getValue();
}
}
Record record = new Record();
record.setPaperId(paperId);
record.setUserId(natureUser.getId());
record.setScore(sum);
record.setCreateTime(new Date());
record.setUpdateTime(new Date());
record.setId(String.valueOf(idWorker.nextId()));
// 将成绩保存到record表
boolean saveResult = recordMapper.saveOne(record);
return saveResult ? ResponseResult.SUCCESS("批改试卷成功").setData(record.getId()) : ResponseResult.FAILURE("批改试卷失败");
}
专业表与班级表增删改查 略过
设置过滤器,防止重复提交
- interceptor
@Component
public class ApiInterceptor implements HandlerInterceptor {
@Autowired
private RedisUtil redisUtil;
@Autowired
private Gson gson;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取方法上的注释
CheckTooFrequentCommit methodAnnotation = handlerMethod.getMethodAnnotation(CheckTooFrequentCommit.class);
if (methodAnnotation != null){
// 获取方法名
String methodName = handlerMethod.getMethod().getName();
// 获取用户tokenKey
String tokenKey = CookieUtils.getCookie(request, Constants.USER.KEY_COOKIE);
// 查看redis中是否有标记
Object commitSign = redisUtil.get(Constants.USER.KEY_COMMIT_SIGN + tokenKey + methodName);
if (commitSign != null){
// 重复提交
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
ResponseResult result = ResponseResult.FAILURE("提交频繁,稍后重试");
PrintWriter writer = response.getWriter();
writer.write(gson.toJson(result));
writer.flush();
return false;
}else {
// 设置标记,有效期5秒
redisUtil.set(Constants.USER.KEY_COMMIT_SIGN + tokenKey + methodName, "1", Constants.TIME.SECOND * 5);
}
}
}
return true;
}
}
- 注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckTooFrequentCommit {
}
- 注册
@Configuration
public class SpringMvcConfiguration implements WebMvcConfigurer {
@Autowired
private ApiInterceptor apiInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiInterceptor);
}
}