抽奖系统-基本-注册
前言
数据库建立
分别是用户表,活动表,奖品表,活动人员关联表,中奖记录表,活动奖品关联表
⼈员业务模块:管理员注册、登录,及普通⽤⼾的创建。
• 活动业务模块:活动管理及活动状态管理。
• 奖品业务模块:奖品管理与奖品的分配。还包括奖品图的上传。
• 通知业务模块:发送短信、邮件等业务,例如验证码发送,中奖通知。
• 抽奖业务模块:完成抽奖动作,以及抽奖后的结果展⽰
唯一索引用uk开头,主键索引用pk开头,普通索引就用idx开头
我们直接用mysql的source命令,执行sql脚本,然后创造数据库
数据库source命令也是.命令
sql脚本就是写它的路径,路径不要有中文
创建工程及分层
错误码定义
我们不对dao层进行错误码定义,dao的错误码被service包含就可以了
所以我们只定义controller和service层的错误码,还有一个全局的错误码
我们的错误码就是一个类,错误码类
定义异常类
定义同一结果返回类
Jackson序列化工具
序列化和反序列化就是利用objectMapper的readValue和writevalueasString方法来的
然后就是list的反序列化方法有点不一样
但是这个方法要抛异常,很麻烦,我们可以写在工具包里面
然后是测试
日志配置
第一个是指定日志的配置文件在哪里
第二个是指定当前环境
这个就是配置文件了
这种是日志的输出格式
这个就是info的过滤器,我们要自定义过滤器,不然所有的info日志都打印进文件了
我们自定义过滤器的话,那么打印进文件的日志就是我们需要的日志了
这段代码定义了一个名为 InfoLevelFilter 的 Logback 过滤器,其作用是只允许 INFO 级别的日志事件通过,其他级别的日志(如 DEBUG、WARN、ERROR 等)都会被过滤掉
我们需要的就是info日志
这个过滤器得到作用就是只要Logback 的info日志
现在我们来测试一下控制台输出的日志格式变没有
我们本地是用控制台输出日志,服务器是把日志输出到文件中
我们用的打印日志的工具是SLF4J,当然也可以用注解SLF4J,我们配置了输出格式的话,SLF4J就不会用它的格式了
加密介绍
我们要对手机号和密码加密
密码用hash加密或者加盐加密
手机号就用对称加密
我们这里用hash加密密码
对称加密手机号
hash加密是不可逆的,只能加密,不能解密,只能根据加密结果来判断对不对
对称加密,只要知道了秘钥就是可逆的,知道了秘钥就可以加密解密了
加盐加密,就是先生成一个随机盐,然后随机盐和密码一起hash
实现加密工具
maven仓库
hutool工具
这里我们使用hutool工具来加密解密
使用哪个工具就引入哪个包
这里我们引入all,因为还要使用其他的
然后开始测试
String s = DigestUtil.sha256Hex("123456789");
直接调用这个就可以hash加密了
//手机号对称加密 aes@Testvoid aesTest(){//先搞个秘钥byte[] KEY = "123456789".getBytes(StandardCharsets.UTF_8);AES aes = SecureUtil.aes(KEY);//根据秘钥生成AES对象,这个对象就可以进行加密解密操作String s = aes.encryptHex("123456789");System.out.println("aes加密:"+s);String s1 = aes.decryptStr(s);System.out.println("aes解密:"+s1);}
但是直接报错了,原因就是这个秘钥的长度必须为16个字节
这样就可以了
秘钥必须为16个字节,或者24,或者32
注册功能分析
请求
{"name":"张三","mail":"451@qq.com","phoneNumber":"13188888888","password":"123456789","identity":"ADMIN"
}[响应]
{"code": 200,"data": {"userId": 22},"msg": ""
}
注册实现1-控制层
接口Serializable是为了序列化
注解RequestBody是为了接收请求参数为json的
<!-- spring-boot 2.3及以上的版本只需要引⼊下⾯的依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
依赖可以对前端传入参数进行校验
如果是对json进行校验,要在json对象的前面先加上注解Validated
然后对参数加上对应的校验注解
注解NotBlank是对string类型进行的校验,校验为空,就会抛异常
还有就是service返回的对象也要单独定义,后缀为DTO
校验基本参数
校验身份信息要创建一个枚举类,然后看这个信息在枚举类里面存不存在
这个就是枚举类
ADMIN就是name,管理员就是message,然后枚举类需要构造函数和get方法
forname就是根据name获得枚举
邮箱是否存在
先引入依赖
校验手机号–typeHandler介绍
校验手机号
typeHandler就是这样得到,我们将String类型转化为Encrypt类型,这个类型具有这种效果
将Encrypt类型插入数据库时,会自动把String给加密,取出这个Encrypt数据时,会自动解密
当然加密和解密的操作都是我们自己要设置的
我们先创建一个对象封装String
因为typeHandler只能处理对象,所以要封装
这个就是typeHandler方法了,针对的是Encrypt类
然后还要添加两个注解
@MappedTypes(Encrypt.class)//被处理的类型
@MappedJdbcTypes(JdbcType.VARCHAR)//处理到数据库的类型,就是转化后的JDBC类型
public class EncryptTypeHandler extends BaseTypeHandler<Encrypt> {private static final byte[] KEY = "123456789abcdefg".getBytes(StandardCharsets.UTF_8);/*** 设置参数* @param ps SQL预编译对象* @param i 需要赋值的索引位置* @param parameter 原本位置i需要的值,就是我们要插入的值Encrypt* @param jdbcType 数据库类型* @throws SQLException */@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, Encrypt parameter, JdbcType jdbcType) throws SQLException {if(parameter == null || parameter.getValue() == null){ps.setString(i, null);//因为jdbcType类型为varchar类型,所以是setStringreturn;}//加密AES aes = SecureUtil.aes(KEY);//创建AES对象String str = aes.encryptHex(parameter.getValue());//加密后的数据ps.setString(i, str);//存入到数据库中System.out.println("加密后的数据:" + str);}/*** 获取值* @param rs 数据库结果集* @param columnName 索引* @return* @throws SQLException*/@Overridepublic Encrypt getNullableResult(ResultSet rs, String columnName) throws SQLException {return decrypt(rs.getString(columnName));}/**** @param rs* @param columnIndex* @return* @throws SQLException*/@Overridepublic Encrypt getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return decrypt(rs.getString(columnIndex));}/**** @param cs* @param columnIndex* @return* @throws SQLException*/@Overridepublic Encrypt getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return decrypt(cs.getString(columnIndex));}private Encrypt decrypt(String value){if(!StringUtils.hasText(value)){return null;}System.out.println("解密前的数据:" + value);return new Encrypt(SecureUtil.aes(KEY).decryptStr(value));}
}
这样我们就写好了
mybatis.type-handlers-package=com.ck.lotterysystem2.dao.handler
注意还要配置typeHandler路径才可以使用
这里的@Param(“phoneNumber”)是和#{phoneNumber}对应的
这个的意思就是把Encrypt phoneNumber转化成varchar类型的phoneNumber了
这个过程是会自动加密的
说明是可以用的
保存信息
先介绍一个插件Database Navigator
有了这个插件,那么我们的就可以在idea上看到数据库了
就是它,然后建立连接
也在这里
这样就有了
每个表都有id,创造时间,修改时间,那么我们就建一个类来存储这三个属性
而且后缀为DO就是专门为mapper准备的,与数据库紧贴的类
useGeneratedKeys = true
这个参数告诉 MyBatis 在执行插入操作后,需要获取数据库自动生成的主键值(如自增 ID)。对于支持自动生成主键的数据库(如 MySQL 的 AUTO_INCREMENT),启用此选项后,MyBatis 会自动将生成的主键值映射到实体类中
keyProperty = “id”
指定实体类中的哪个属性用于存储生成的主键值。在这个例子中,UserDO 类必须有一个名为 id 的属性(及其对应的 getter/setter 方法),生成的主键值会被设置到这个属性中。
keyColumn = “id”
指定数据库表中对应的主键列名。当实体类属性名与数据库表的列名不一致时,需要通过此参数显式映射。在此例中,数据库表的主键列名是 id,与实体类属性名一致。
这里就有数据了,如果没有数据的话,那么就取消链接,然后在连接,然后刷新就可以了
全局异常处理
我们用上注解@RestControllerAdvice+@ExceptionHandler
RestControllerAdvice是接收整个MVC的全部异常
而ExceptionHandler就是处理具体的异常了
我们在controller下建一个handler的包来处理这个事情
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(value = ServiceException.class)public CommonResult<?> handleServiceException(ServiceException e) {log.error("handleServiceException:",e);return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),e.getMessage());}@ExceptionHandler(value = ControllerException.class)public CommonResult<?> handleControllerException(ControllerException e) {log.error("handleControllerException:",e);return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),e.getMessage());}@ExceptionHandler(value = Exception.class)public CommonResult<?> handleException(Exception e) {log.error("handleException:",e);return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode(),e.getMessage());}}
这样就可以了
还有就是错误日志不需要占位符{}
注意这里断言要改一下,不是SUCCESS,或者改为非SUCCESS
这样就OK了
注册前端实现
登录时序图
阿里云短信服务
登录阿里云官网,实名认证
阿里云
点击ACCessKey
创建一个ACCessKey,然后保存
然后就可以开通短信服务了
直接开通短信服务
搜索短信服务
是这个界面
先申请资质
扫码钉钉
新增资质
然后是申请签名
签名就是名称
资质信息就是刚刚申请的资质
签名来源填写:测试或者学习
但是出现了这个问题,阿里云不能让学生使用短信了,所以阿里云不行了
然后是申请模版
结束,用不了了,要企业认证
Redis介绍
就是生成的随机验证码要放在Redis中
然后输入验证吧,就不要发送到手机号了,实现不了了暂时
redis配置使用
## redis spring boot 3.x ##
spring.data.redis.host=localhost
spring.data.redis.port=6379
# 连接空闲超过N(s秒、ms毫秒)后关闭,0为禁⽤,这⾥配置值和tcp-keepalive值⼀致
spring.data.redis.timeout=60s
# 默认使⽤ lettuce 连接池
# 允许最⼤连接数,默认8(负值表⽰没有限制)
spring.data.redis.lettuce.pool.max-active=8
# 最⼤空闲连接数,默认8
spring.data.redis.lettuce.pool.max-idle=8
# 最⼩空闲连接数,默认0
spring.data.redis.lettuce.pool.min-idle=0
# 连接⽤完时,新的请求等待时间(s秒、ms毫秒),超过该时间抛出异常JedisConnectionException,(默认-1,负值表⽰没有限制)
spring.data.redis.lettuce.pool.max-wait=5s
然后就可以链接上了,本地Windows端口号是配置好的
然后记得启动了项目才可以去链接,在idea上链接的时候
RedisUtil
这里我们使用的是StringRedisTemplate,这个是对RedisTemplate的分装,RedisTemplate最终存储在redis中的是字节数组
而StringRedisTemplate是的key和value都是String
而且最终存储在redis中的就是String
StringRedisTemplate这个是自动注入bean的,我们要在RedisUtil中使用StringRedisTemplate,那么
RedisUtil就要注入bean,用注解Configuration
@Configuration
@Slf4j
public class RedisUtil {@Autowiredprivate static StringRedisTemplate stringRedisTemplate;public boolean set(String key, String value) {try{stringRedisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {log.error("redis set error:key:{},value:{},e:",key,value,e);//e不需要占位符throw new RuntimeException("redis set error");}}public static boolean set(String key, String value, Long expireTime) {try{stringRedisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);//到了时间自动删除return true;} catch (Exception e) {log.error("redis set error:key:{},value:{},expireTime,{},e:",key,value,expireTime,e);//e不需要占位符return false;}}public String get(String key) {try{return StringUtils.hasText(key)?stringRedisTemplate.opsForValue().get(key):null;} catch (Exception e) {log.error("redis get error:key:{},e:",key,e);//e不需要占位符return null;}}public boolean del(String... key) {try{if(key==null||key.length<=0)return false;if(key.length==1)stringRedisTemplate.delete(key[0]);stringRedisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));return true;} catch (Exception e) {log.error("redis del error:key:{},e:",key,e);//e不需要占位符return false;}}public boolean hasKey(String key){try{return StringUtils.hasText(key)? stringRedisTemplate.hasKey(key):false;} catch (Exception e) {log.error("redis hasKey error:key:{},e:",key,e);//e不需要占位符return false;}}}
测试一下
短信验证码服务完成
我们总共要实现两个service,一个是发送验证码,一个是获取验证码
验证码我们还是使用hutool的工具
public class CaptchaUtil {public static String getCaptcha(int length) {// 自定义纯数字的验证码(随机4位数字,可重复)RandomGenerator randomGenerator = new RandomGenerator("0123456789", 4);LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);lineCaptcha.setGenerator(randomGenerator);// 重新生成codelineCaptcha.createCode();return lineCaptcha.getCode();}
}
然后就是redis缓存的时候,key要标准化,要有前缀区分,就用手机号,然后是业务区分,就用验证码
@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {private static final String VERIFICATION_CODE_PREFIX = "verificationCode_CODE_";private static final Long VERIFICATION_CODE_TIMEOUT =60L;@Overridepublic void sendVerificationCode(String phone) {//校验手机号if (!RegexUtil.checkMobile(phone)) {throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);}//生成随机验证码String code = CaptchaUtil.getCaptcha(4);//发送验证码
// Map<String, String> map = new HashMap<>();
// map.put("code", code);
// smsUtil.sendMessage("SMS_465324787", phone, JacksonUtil.writeValueAsString(map));//第一个是模版id,第二个是手机号,第三个是模版参数JSON串//缓存验证码RedisUtil.set(VERIFICATION_CODE_PREFIX+phone, code, VERIFICATION_CODE_TIMEOUT);}@Overridepublic String getVerificationCode(String phone) {//校验手机号if (!RegexUtil.checkMobile(phone)) {throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);}return RedisUtil.get(VERIFICATION_CODE_PREFIX+phone);}
}
登录认证方式JWT
1.cookie和session只能在web下,不能跨域,集群环境下失效
2.token方式:
缺点就是考验redis了
3.基于token的JWT令牌
JWTUtil
就是生成jwt和解析jwt
先引入依赖
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --><version>0.11.5</version><scope>runtime</scope></dependency>
注意jwt就是jsonwebtoken
public class JWTUtil {private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);/*** 密钥:Base64编码的密钥*/private static final String SECRET = "SDKltwTl3SiWX62dQiSHblEB6O03FG9/vEaivFu6c6g=";/*** 生成安全密钥:将一个Base64编码的密钥解码并创建一个HMAC SHA密钥。*/private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET));/*** 过期时间(单位: 毫秒)*/private static final long EXPIRATION = 60*60*1000;/*** 生成密钥** @param claim {"id": 12, "name":"张山"}* @return*/public static String genJwt(Map<String, Object> claim){//签名算法String jwt = Jwts.builder().setClaims(claim) // 自定义内容(载荷).setIssuedAt(new Date()) // 设置签发时间.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 设置过期时间.signWith(SECRET_KEY) // 签名算法.compact();return jwt;}/*** 验证密钥*/public static Claims parseJWT(String jwt){if (!StringUtils.hasLength(jwt)){return null;}// 创建解析器, 设置签名密钥JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);Claims claims = null;try {//解析tokenclaims = jwtParserBuilder.build().parseClaimsJws(jwt).getBody();}catch (Exception e){// 签名验证失败logger.error("解析令牌错误,jwt:{}", jwt, e);}return claims;}/*** 从token中获取用户ID*/public static Integer getUserIdFromToken(String jwtToken) {Claims claims = JWTUtil.parseJWT(jwtToken);if (claims != null) {Map<String, Object> userInfo = new HashMap<>(claims);return (Integer) userInfo.get("userId");}return null;}
}
还有值得注意的就是秘钥不是随便生成的,必须用特定的方法生成才算
/**生成密钥*/@Testpublic void genKey(){// 创建了一个密钥对象,使用HS256签名算法。Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);// 将密钥编码为Base64字符串。String secretString = Encoders.BASE64.encode(key.getEncoded());System.out.println(secretString);// 结果:dVnsmy+SIX6pNptQdeclDSJ26EMSPEIhvZYKBTTug4k=}
就是这个方法
生成的秘钥才算真正的秘钥
我们这个秘钥直接就用一个常量了
所以就很OK