项目背景
随着数字营销的兴起,企业越来越重视通过在线活动来吸引和留住客⼾。抽奖活动作为⼀种有效的营 销⼿段,能够显著提升⽤⼾参与度和品牌曝光率。于是我们就开发了以抽奖活动作为背景的Spring Boot项⽬
应用技术:利⽤MySQL、Redis、RabbitMQ等常⽤组件
项目的模块设计

项目展示





定义Jackson工具类用来做JSON和Java对象的转换
public class JacksonUtil {
public JacksonUtil(){
}
private final static ObjectMapper OBJECT_MAPPER;
static{
OBJECT_MAPPER=new ObjectMapper();
}
private static ObjectMapper getObjectMapper(){
return OBJECT_MAPPER;
}
private static <T> T tryParse(Callable<T> parser) {
return tryParse(parser, JacksonException.class);
}
private static <T> T tryParse(Callable<T> parser, Class<? extends Exception> check) {
try {
return parser.call();
} catch (Exception var4) {
if (check.isAssignableFrom(var4.getClass())) {
throw new JsonParseException(var4);
}
throw new IllegalStateException(var4);
}
}
// 序列化方法
public static String writeValueAsString(Object object){
return JacksonUtil.tryParse(()->{
return JacksonUtil.getObjectMapper().writeValueAsString(object);
});
}
// 反序列化方法
public static <T> T readValue(String str,Class<T> valueType){
return JacksonUtil.tryParse(()->{
return JacksonUtil.getObjectMapper().readValue(str,valueType);
});
}
// 反序列化List的方法
public static <T> List<T> readListValue(String str,Class<?> paramClasses){
JavaType javaType = JacksonUtil.getObjectMapper().getTypeFactory()
.constructParametricType(List.class, paramClasses);
return JacksonUtil.tryParse(()->{
return JacksonUtil.getObjectMapper().readValue(str,javaType);
});
}
}
用户板块
用户注册
约定好前后端交互的接口

设计contoller层接口
@RequestMapping("/register")
public CommonResult<UserRegisterResult> register(@RequestBody UserRegisterParam param){
logger.info("注册用户信息:{}", JacksonUtil.writeValueAsString(param));
UserRegisterDTO userRegisterDTO=userService.register(param);
UserRegisterResult result=convertUserRegisterToResult(userRegisterDTO);
return CommonResult.success(result);
}
//将UserRegisterDTO类型 转换为UserRegisterResult后进行返回
private UserRegisterResult convertUserRegisterToResult(UserRegisterDTO userRegisterDTO) {
if(userRegisterDTO==null){
throw new ControllerException(ControllerErrorCodeConstants.REGISTER_ERROR);
}
UserRegisterResult result=new UserRegisterResult();
result.setUserId(userRegisterDTO.getUserId());
return result;
}
将传递的参数定义为UserRegisterParam类,用于接受用户注册时提交的数据,并且通过@Validated 注解,来检查参数
@Data
public class UserRegisterParam implements Serializable {
/**
* 姓名
*/
@NotBlank(message = "用户姓名不能为空")
private String name;
/**
* 邮箱
*/
@NotBlank(message = "用户邮箱不能为空")
private String email;
/**
* 电话号码
*/
@NotBlank(message = "用户手机号不能为空")
private String phoneNumber;
/**
* 密码
*/
private String password;
/**
* 身份信息
*/
@NotBlank(message = "用户身份信息不能为空")
private String identity;
}
由于前后端交互接口中数据中传输的是userId
@Data
public class UserRegisterResult implements Serializable {
/**
* 用户Id
*/
private Long userId;
}
创建UserRegisterDTO用于controller和service层之间用来传输对象的,通常是因为数据库实体中可能有敏感信息,而VO更多是展示给前端的,所以他们需要一个中间层对象DTO
在用户注册的接口交互中,约定好返回给前端的数据是userId
@Data
public class UserRegisterDTO implements Serializable {
private Long userId;
}
定义好UserService接口中的register方法
public interface UserService {
UserRegisterDTO register(UserRegisterParam param);
通过UserServiceImpl进行实现
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public UserRegisterDTO register(UserRegisterParam param) {
checkUserInfo(param);
// 进行用户的添加
UserDO userDO=new UserDO();
userDO.setUserName(param.getName());
userDO.setEmail(param.getEmail());
userDO.setPhoneNumber(new Encrypt(param.getPhoneNumber()));
userDO.setIdentity(param.getIdentity());
if(StringUtils.hasText(param.getPassword())){
userDO.setPassword(DigestUtil.sha256Hex(param.getPassword()));
}
// 存储数据
userMapper.insert(userDO);
// 构造返回
UserRegisterDTO userRegisterDTO=new UserRegisterDTO();
userRegisterDTO.setUserId(userDO.getId());
return userRegisterDTO;
}
UserDO实体对象,用来存储数据,包含注册业务的相关字段,与数据库中注册后的用户信息字段一一对应
@Data
@EqualsAndHashCode(callSuper = true)//会将父类中的字段也会考虑进去
public class UserDO extends BaseDO {
private String userName;
private String email;
private Encrypt phoneNumber;
private String password;
private String identity;
}
BaseDO是抽象父类,里面通常存放所有表公共的字段
@Data
public class BaseDO implements Serializable {
private Long id;
private Date gmtCrate;
private Date gmtModified;
}
注:我们在设置密码的时候使用的Hutool中加密的摘要算法 DigestUtil.sha256Hex()详细了解请看加密解密-(Hutool-crypto)-优快云博客
在注册的时候我们需要判断注册的信息是否满足条件
public void checkUserInfo(UserRegisterParam param) {
// 检查姓名
if(param==null){
throw new ServiceException(ServiceErrorCodeConstants.REGISTER_INFO_IS_EMPTY);
}
// 检查邮箱格式
if(!RegexUtil.checkMail(param.getEmail())){
throw new ServiceException(ServiceErrorCodeConstants.MAIL_ERROR);
}
// 检查手机号码格式
if(!RegexUtil.checkMobile(param.getPhoneNumber())){
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
// 检查⾝份信息
if(UserIdentityEnum.fromName(param.getIdentity())==null){
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
// 管理员必须设置密码
if(UserIdentityEnum.fromName(param.getIdentity())==UserIdentityEnum.ADMIN && !StringUtils.hasText(param.getPassword())){
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_IS_EMPTY);
}
// 检查密码格式,最少六位
if(RegexUtil.checkPassword(param.getPassword())){
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
}
// 检查邮箱是否被使用
if(checkEmailUsed(param.getEmail())){
throw new ServiceException(ServiceErrorCodeConstants.MAIL_USED);
}
// 检查手机号是否被使用
if(checkPhoneUsed(param.getPhoneNumber())){
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_USED);
}
}
定义Service层的自定义异常
@Data
@EqualsAndHashCode(callSuper = true)
public class ServiceException extends RuntimeException{
private Integer code;
private String message;
public ServiceException() {
}
public ServiceException(Integer code, String message) {
this.code = code;
this.message = message;
}
public ServiceException(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.message = errorCode.getMsg();
}
}
通过ErrorCode对象类,用来实现service层的错误码常量,方便统一管理,用于service层手动抛出异常
@Data
public class ErrorCode {
private final Integer code;
private final String msg;
public ErrorCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
public interface ServiceErrorCodeConstants {
ErrorCode REGISTER_INFO_IS_EMPTY = new ErrorCode(100, "注册信息为空");
ErrorCode MAIL_ERROR = new ErrorCode(101, "邮箱错误");
ErrorCode PHONE_NUMBER_ERROR = new ErrorCode(102, "手机号错误");
ErrorCode IDENTITY_ERROR = new ErrorCode(103, "身份错误");
ErrorCode PASSWORD_IS_EMPTY = new ErrorCode(104, "密码为空");
ErrorCode PASSWORD_ERROR = new ErrorCode(105, "密码错误");
ErrorCode MAIL_USED = new ErrorCode(106, "邮箱被使用");
ErrorCode PHONE_NUMBER_USED = new ErrorCode(107, "手机号被使用");
ErrorCode LOGIN_INFO_NOT_EXIST = new ErrorCode(108, "登录信息不存在");
ErrorCode LOGIN_NOT_EXIST = new ErrorCode(109, "登录方式不存在");
ErrorCode USER_INFO_IS_EMPTY = new ErrorCode(110, "用户信息为空");
ErrorCode VERIFICATION_CODE_ERROR = new ErrorCode(111, "验证码校验失败");
}
在校验用户的邮箱,电话,密码格式,封装成工具类来完成
public class RegexUtil {
/**
* 邮箱:xxx@xx.xxx(形如:abc@qq.com)
*
* @param content
* @return
*/
public static boolean checkMail(String content) {
if (!StringUtils.hasText(content)) {
return false;
}
/**
* ^ 表示匹配字符串的开始。
* [a-z0-9]+ 表示匹配一个或多个小写字母或数字。
* ([._\\-]*[a-z0-9])* 表示匹配零次或多次下述模式:一个点、下划线、反斜杠或短横线,后面跟着一个或多个小写字母或数字。这部分是可选的,并且可以重复出现。
* @ 字符字面量,表示电子邮件地址中必须包含的"@"符号。
* ([a-z0-9]+[-a-z0-9]*[a-z0-9]+.) 表示匹配一个或多个小写字母或数字,后面可以跟着零个或多个短横线或小写字母和数字,然后是一个小写字母或数字,最后是一个点。这是匹配域名的一部分。
* {1,63} 表示前面的模式重复1到63次,这是对顶级域名长度的限制。
* [a-z0-9]+ 表示匹配一个或多个小写字母或数字,这是顶级域名的开始部分。
* $ 表示匹配字符串的结束。
*/
String regex = "^[a-z0-9]+([._\\\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$";
return Pattern.matches(regex, content);
}
/**
* 手机号码以1开头的11位数字
*
* @param content
* @return
*/
public static boolean checkMobile(String content) {
if (!StringUtils.hasText(content)) {
return false;
}
/**
* ^ 表示匹配字符串的开始。
* 1 表示手机号码以数字1开头。
* [3|4|5|6|7|8|9] 表示接下来的数字是3到9之间的任意一个数字。这是中国大陆手机号码的第二位数字,通常用来区分不同的运营商。
* [0-9]{9} 表示后面跟着9个0到9之间的任意数字,这代表手机号码的剩余部分。
* $ 表示匹配字符串的结束。
*/
String regex = "^1[3|4|5|6|7|8|9][0-9]{9}$";
return Pattern.matches(regex, content);
}
/**
* 密码强度正则,6到12位
*
* @param content
* @return
*/
public static boolean checkPassword(String content){
if (!StringUtils.hasText(content)) {
return false;
}
/**
* ^ 表示匹配字符串的开始。
* [0-9A-Za-z] 表示匹配的字符可以是:
* 0-9:任意一个数字(0到9)。
* A-Z:任意一个大写字母(从A到Z)。
* a-z:任意一个小写字母(从a到z)。
* {6,12} 表示前面的字符集合(数字、大写字母和小写字母)可以重复出现6到12次。
* $ 表示匹配字符串的结束。
*/
String regex= "^[0-9A-Za-z]{6,12}$";
return Pattern.matches(regex, content);
}
}
在校验身份信息,因为用户的身份信息有两种,管理员注册时候不需要输入密码,用户注册的时候需要输入密码,所以我们使用了枚举类
formName方法用于将输入的身份和枚举中的身份进行匹配
@AllArgsConstructor
@Getter
public enum UserIdentityEnum {
NORMAL(" 普通⽤⼾ "),
ADMIN(" 管理员 ");
private final String message;
public static UserIdentityEnum fromName(String name) {
for (UserIdentityEnum userIdentityEnum : UserIdentityEnum.values()) {
if (userIdentityEnum.name().equalsIgnoreCase(name)) {
return userIdentityEnum;
}
}
return null;
}
}
在检查邮箱是否已经被使用,我们需要从数据库中进行查询该邮箱是否存在
@Mapper
public interface UserMapper {
@Select("select count(*) from user where email = #{email}")
int countByMail(@Param("email") String email);
然后写一个方法,如果将数据库中拿出的数据返回定义为int类型,如果大于0,则说明该邮箱已经存在
public boolean checkEmailUsed(String email){
int count=userMapper.countByMail(email);
return count>0;
}
在检查电话号码是否被使用,我们需要从数据库中进行查询该电话是否存在
@Select("select count(*) from user where phone_number = #{phoneNumber}")
int countByPhone(@Param("phoneNumber") Encrypt phoneNumber);
然后写一个方法,如果将数据库中拿出的数据返回定义为int类型,如果大于0,则说明该电话号码已经存在
private boolean checkPhoneUsed(String phoneNumber) {
int count=userMapper.countByPhone(new Encrypt(phoneNumber));
return count>0;
}
但是特别需要注意的是:
我们新定义了一个Encrypt类,用于表示需要加密的字符串,因为电话号码我们进行加密,所以我们将电话号写入数据库时候需要进行加密,从数据库中拿出的时候需要进行解密
@Data
public class Encrypt {
private String value;
public Encrypt(){
}
public Encrypt(String value){
this.value=value;
}
}
处理加密的类型,也就是java层Encrypt类型和数据库层的VARCHAR进行转换,并且加密解密的逻辑代码
写入数据库前把 Encrypt.value 加密(变成十六进制字符串),写入 VARCHAR 列
从数据库读取值时把密文 解密 回原始字符串,并封装成 Encrypt 对象返回给 Java
@MappedTypes(Encrypt.class) //被处理的类型
@MappedJdbcTypes(JdbcType.VARCHAR)//转换后JDBC的类型
public class EncryptTypeHandle extends BaseTypeHandler<Encrypt> {
private final byte[] KEYS= "123456789abcdefg".getBytes(StandardCharsets.UTF_8);
/**设置参数
*
* @param ps SQL的预设对象
* @param i 需要赋值的索引位置
* @param parameter 原本位置i需要赋的值
* @param jdbcType jdbc 类型
* @throws SQLException
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Encrypt parameter, JdbcType jdbcType) throws SQLException {
if(parameter==null || parameter.getValue()==null){
ps.setString(i,null);
return;
}
System.out.println("加密的内容"+parameter.getValue());
AES aes= SecureUtil.aes(KEYS);
String resultEncrypt=aes.encryptHex(parameter.getValue());
ps.setString(i,resultEncrypt);
}
/**获得值
*
* @param rs 结果集
* @param columnName 索引名
* @return
* @throws SQLException
*/
@Override
public Encrypt getNullableResult(ResultSet rs, String columnName) throws SQLException {
return decrypt(rs.getString(columnName));
}
/**获得值
*
* @param rs 结果集合
* @param columnIndex 索引
* @return
* @throws SQLException
*/
@Override
public Encrypt getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return decrypt(rs.getString(columnIndex));
}
/**获得值
*
* @param cs 结果集合
* @param columnIndex 索引
* @return
* @throws SQLException
*/
@Override
public Encrypt getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return decrypt(cs.getString(columnIndex)); }
/**
* 解密
*/
public Encrypt decrypt(String s){
if(!StringUtils.hasText(s)){
return null;
}
return new Encrypt( SecureUtil.aes(KEYS).decryptStr(s));
}
}
在注册完成后,我们需要将新注册的用户添加到数据库中
userMapper.insert(userDO);
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id"),将数据库表中自增生成的 id 自动回填到 Java 对象的 id 属性中,实现对象属性和数据库列的“绑定。
useGeneratedKeys = true使用数据库的自增主键,
@Insert("insert into user (user_name, email, phone_number, password, identity)" +
" values (#{userName}, #{email}, #{phoneNumber}, #{password}, #{identity})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
void insert(UserDO userDO);
用户登录
发送验证码
我们在之前可以使用阿里云的短信服务,但是现在只能用于企业,个人已经无法使用
引入阿里云短信服务的依赖
<!--阿里云短信服务-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.24</version>
</dependency>
定义SMS短信工具类
@Component
public class SMSUtil {
private static final Logger logger = LoggerFactory.getLogger(SMSUtil.class);
@Value(value = "${sms.sign-name}")
private String signName;
@Value(value = "${sms.access-key-id}")
private String accessKeyId;
@Value(value = "${sms.access-key-secret}")
private String accessKeySecret;
/**
* 发送短信
*
* @param templateCode 模板号
* @param phoneNumbers 手机号
* @param templateParam 模板参数 {"key":"value"}
*/
public void sendMessage(String templateCode, String phoneNumbers, String templateParam) {
try {
Client client = createClient();
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new SendSmsRequest()
.setSignName(signName)
.setTemplateCode(templateCode)
.setPhoneNumbers(phoneNumbers)
.setTemplateParam(templateParam);
RuntimeOptions runtime = new RuntimeOptions();
SendSmsResponse response = client.sendSmsWithOptions(sendSmsRequest, runtime);
if (null != response.getBody()
&& null != response.getBody().getMessage()
&& "OK".equals(response.getBody().getMessage())) {
logger.info("向{}发送信息成功,templateCode={}", phoneNumbers, templateCode);
return;
}
logger.error("向{}发送信息失败,templateCode={},失败原因:{}",
phoneNumbers, templateCode, response.getBody().getMessage());
} catch (TeaException error) {
logger.error("向{}发送信息失败,templateCode={}", phoneNumbers, templateCode, error);
} catch (Exception _error) {
TeaException error = new TeaException(_error.getMessage(), _error);
logger.error("向{}发送信息失败,templateCode={}", phoneNumbers, templateCode, error);
}
}
}
配置 .properties文件
sms.access-key-id=
sms.access-key-secret=
sms.sign-name=
我们可以把验证码的生成也封装成一个工具类CaptchaUtil,也是依靠Hutool中的随机验证码功能完成的
@Configuration
public class CaptchaUtil {
// 根据Hutool中的工具生成验证码
public String getCaptcha(int length){
// 自定义纯数字的验证码(随机length位数字,可重复)
RandomGenerator randomGenerator = new RandomGenerator("0123456789", length);
LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);
lineCaptcha.setGenerator(randomGenerator);
// 重新生成code
lineCaptcha.createCode();
return lineCaptcha.getCode();
}
}
约定前后端的交互接口

设置controller层接口,调用VerificationCodeService
@RequestMapping("/verification-code/send")
public CommonResult<Boolean> sendVerificationCode(String phoneNumber){
logger.info("sendVerificationCode:{}",JacksonUtil.writeValueAsString(phoneNumber));
verificationCodeService.setVerificationCode(phoneNumber);
return CommonResult.success(Boolean.TRUE);
}
定义VerificationCodeService中的生成验证码和获得验证码两个方法,通过VerificationCodeServiceImpl来实现两个方法
public interface VerificationCodeService {
// 生成验证码
void setVerificationCode(String phoneNumber);
// 活得验证码
String getVerificationCode(String phoneNumber);
}
由于短信服务无法用于个人,所以我们无法发送验证码,只能从后端打印出验证码进行使用,将发送验证码的代码进行注释

由于将验证码通常只有几分钟有效,而且redis可以帮助我们进行快速读取,所以我们使用redis缓存进行存储验证码
@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {
private static final String VERIFICATION_CODE_PREFIX="VERIFICATION_CODE_";
private static final Long VERIFICATION_CODE_TIMEOUT=60L;
private static final String VERIFICATION_CODE_TEMPLATE_CODE="SMS_465324787";
@Autowired
private CaptchaUtil captchaUtil;
@Autowired
private SMSUtil smsUtil;
@Autowired
private RedisUtil redisUtil;
@Override
public void setVerificationCode(String phoneNumber) {
// 校验手机号
if (!RegexUtil.checkMobile(phoneNumber)){
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
// 生成随机验证码
String captcha =captchaUtil.getCaptcha(6);
System.out.println("手机号 " + phoneNumber + " 的验证码是: " + captcha);
// 因为个人无法目前发送验证码,所以使用随机数来代替
// 发送验证码
// Map<String,String> map=new HashMap<>();
// map.put("code",captcha);
// smsUtil.sendMessage(VERIFICATION_CODE_TEMPLATE_CODE,
// phoneNumber,
// JacksonUtil.writeValueAsString(map));
// 将验证码存入到redis中
redisUtil.set(VERIFICATION_CODE_PREFIX+phoneNumber,captcha,VERIFICATION_CODE_TIMEOUT);
}
@Override
public String getVerificationCode(String phoneNumber) {
// 校验手机号
if (!RegexUtil.checkMobile(phoneNumber)){
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
// 从缓存中拿出验证码
return redisUtil.get(VERIFICATION_CODE_PREFIX+phoneNumber);
}
}
记得在使用时引入redis的依赖
<!-- redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
我们在定义redis工具类时,写了五个方法,关于key的存储,读取,删除,查看是否存在,还有带有时间限制的存储,到一定的时间限制后,会自动删除。
@Configuration
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final Logger logger= LoggerFactory.getLogger(RedisUtil.class);
public boolean set(String key,String value){
try {
stringRedisTemplate.opsForValue().set(key,value);
return true;
}catch (Exception e){
logger.error("setError key,value:{},{}",key,value,e);
return false;
}
}
public boolean set(String key,String value,Long time){
try {
stringRedisTemplate.opsForValue().set(key,value,time, TimeUnit.SECONDS);
return true;
}catch (Exception e){
logger.error("setError key,value,time:{},{},{}",key,value,time,e);
return false;
}
}
public String get(String key){
try {
return StringUtils.hasText(key)?stringRedisTemplate.opsForValue().get(key):null;
}catch (Exception e){
logger.error("getError:{}",key,e);
return null;
}
}
public boolean deleteKey(String ... key){
try {
if(key!=null && key.length>0){
if(key.length==1){
stringRedisTemplate.delete(key[0]);
}else {
stringRedisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
return true;
}catch (Exception e){
logger.error("deleteError:{}",key,e);
return false;
}
}
public Boolean hasKey(String key){
try {
if(key!=null){
return stringRedisTemplate.hasKey(key);
}
return false;
}catch (Exception e){
logger.error("hasKeyError:{}",key,e);
return false;
}
}
}
设置时间限制的存储


最低0.47元/天 解锁文章
1933

被折叠的 条评论
为什么被折叠?



