注:项目git仓库地址:demo.lottery · 小五Z/Spring items - 码云 - 开源中国
目录
注:项目git仓库地址:demo.lottery · 小五Z/Spring items - 码云 - 开源中国
项目具体代码可参考仓库源码,本文只讲解重点代码逻辑
一、需求分析
1. 包含管理员的注册与登录。
2. 人员管理: 管理员支持创建普通用户,查看用户列表
3. 管理端支持创建奖品、奖品列表展示功能。
4. 管理端⽀持创建活动、活动列表展⽰功能。
5. 抽奖页面:
6. 通知部分: 抽奖完成需以邮件和短信⽅式通知中奖者。
二、系统设计
1.系统架构
2.项目环境
3.数据库设计
三、项目启动
四、功能模块设计
通用处理
1.错误码
2.自定义异常类
编辑
3.统一返回格式
编辑
4.Jackson实现序列化和反序列化
五、用户模块
1.注册
1.1敏感字段加密
1.2时序图
1.3 约定前后端交互接口
1.4 Controller层代码设计
UserRegisterParam
UserIdentityEnum
UserRegisterResult
1.5 Service层接口设计
UserRegisterDTO
接口实现
RegexUtil
1.6 Dao层接口设计
UserDO
BaseDO
Encrypt
EncryptTypeHandler
1.7 全局异常捕获
2.登录
发送验证码
2.2 时序图
编辑
2.3 配置application.properties
2.4 SMSUtil短信工具类
2.5 CaptchaUtil验证码工具类
2.6 约定前后端交互接口
2.7 Controlller层接口设计
2.8 Service层接口设计
2.9 Redis的配置与使用
登录
2.10 JWT令牌
2.11 登录时序图
编辑
2.12 约定前后端交互接口
2.13 Controller层接口设计
2.14 Service层接口设计
2.15 Dao层接口设计
2.16 添加拦截器
3.人员列表展示
3.1 时序图
3.2 约定前后端交互接口
3.3 Controller层接口设计
3.4 Service接口设计
3.5 Dao层接口设计
六、奖品模块
1. 图片上传
application.properties 配置上传⽂件路径
图⽚服务 PictureService 接⼝定义
2. 奖品创建
2.1 时序图
编辑
2.2 约定前后端交互接口
2.3 Controller层接口设计
2.4 Service层接口设计
2.5 Dao层接口设计
3. 奖品列表展示(翻页)
3.1 时序图
3.2 约定前后端交互接口
3.3 Controller层接口设计
3.4 Service层接口设计
3.5 Dao层接口设计
七、活动模块
1. 活动创建
1.1 需求回顾
1.2 时序图
1.3 约定前后端交互接口
1.4 Controller层接口设计
1.5 Service层接口设计
1.6 Dao层接口设计
2.活动列表展示(翻页)
2.1 时序图
2.2 约定前后端交互接口
2.3 Controller层接口设计
2.4 Service层接口设计
2.5 Dao层接口设计
八、抽奖模块
1.抽奖设计
2.RabbitMQ配置使用
3.抽奖请求处理
3.1 时序图
编辑
3.2 约定前后端交互接口
3.3 Controller层接口设计
3.4 Service层接口设计
4. MQ异步抽奖逻辑执行
4.1 时序图
编辑
4.2 消费MQ消息
4.3 请求验证(核对抽奖信息有效性)
4.4 状态转换(重点)
5. 结果记录
5.1 时序图
5.2 Service层接口设计
6. 邮件服务
项目具体代码可参考仓库源码,本文只讲解重点代码逻辑
一、需求分析
1. 包含管理员的注册与登录。
2. 人员管理: 管理员支持创建普通用户,查看用户列表
3. 管理端支持创建奖品、奖品列表展示功能。
4. 管理端⽀持创建活动、活动列表展⽰功能。
5. 抽奖页面:
6. 通知部分: 抽奖完成需以邮件和短信⽅式通知中奖者。
二、系统设计
1.系统架构
2.项目环境
3.数据库设计
E-R图:
三、项目启动

四、功能模块设计
通用处理
1.错误码
定义错误码类型:
定义全局错误码:
定义业务错误码---controller层错误:
2.自定义异常类
3.统一返回格式
4.Jackson实现序列化和反序列化
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.json.JsonParseException;
import org.springframework.util.ReflectionUtils;
import java.util.List;
import java.util.concurrent.Callable;
public class JacksonUtil {
private 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,JsonParseException.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);
}
}
/**
* 序列化
* @param object
* @return
*/
public static String writeValueAsString (Object object) {
return tryParse(()->{
return JacksonUtil.getObjectMapper().writeValueAsString(object);
});
}
/**
* 反序列化
* @param content
* @param valueType
* @return
* @param <T>
*/
public static <T> T readValue (String content,Class<T> valueType) {
return tryParse(()->{
return JacksonUtil.getObjectMapper().readValue(content,valueType);
});
}
/**
* List集合反序列化
* @param content
* @param valueType
* @return
* @param <T>
*/
public static <T> T readList (String content,Class<?> valueType) {
JavaType javaType = JacksonUtil.getObjectMapper()
.getTypeFactory()
.constructParametricType(List.class,valueType);
return tryParse(()->{
return JacksonUtil.getObjectMapper().readValue(content,javaType);
});
}
}
五、用户模块
1.注册
1.1敏感字段加密
⼀般来说,用户注册时,需要输⼊其账⼾密码及手机号,服务器应该将其保存起来,⽅便后续登录验证。但仅从道德的⻆度来说,后端不应该以明文形式存储用户密码以及其他敏感信息。
从运维层⾯看,任何操作系统漏洞、基础⼯具漏洞的发⽣,都会导致密码泄露
1.2时序图
1.3 约定前后端交互接口
[请求] /register POST
{
"name":"张三",
"mail":"451@qq.com",
"phoneNumber":"13188888888",
"password":"123456789",
"identity":"ADMIN"
}
"code": 200,
"data": {
"userId": 22
},
"msg": ""
}
1.4 Controller层代码设计
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
UserService userService;
@Autowired
VerificationCodeService verificationCodeService;
/**
* 注册
* @param userRegisterParam
* @return
*/
@RequestMapping("/register")
public CommonResult<UserRegisterResult> userRegister(
@Validated @RequestBody UserRegisterParam userRegisterParam){
logger.info("userRegister UserRegisterParam:{}", JacksonUtil.writeValueAsString(userRegisterParam));
UserRegisterDTO userRegisterDTO = userService.register(userRegisterParam);
return CommonResult.success(converToUserRegisterResult(userRegisterDTO));
}
private UserRegisterResult converToUserRegisterResult(UserRegisterDTO userRegisterDTO) {
if(null == userRegisterDTO) {
throw new ControllerException(ControllerErrorCodeConstants.REGISTER_ERROR);
}
UserRegisterResult result=new UserRegisterResult();
result.setUserId(userRegisterDTO.getUserId());
return result;
}
}
UserRegisterParam
@Data
public class UserRegisterParam implements Serializable {
/**
* 姓名
*/
@NotBlank(message = "姓名不能为空!")
private String name;
/**
* 邮箱
*/
@NotBlank(message = "邮箱不能为空!")
private String mail;
/**
* 电话
*/
@NotBlank(message = "电话不能为空!")
private String phoneNumber;
/**
* 密码
*/
private String password;
/**
* 身份信息
*/
@NotBlank(message = "身份信息不能为空!")
private String identity;
}
UserIdentityEnum
@Getter
@AllArgsConstructor
public enum UserIdentityEnum {
ADMIN("管理员"),
NORMAL("普通用户");
private final String massage;
public static UserIdentityEnum forName (String name) {
for(UserIdentityEnum userIdentifyEnum : UserIdentityEnum.values()) {
if(userIdentifyEnum.name().equalsIgnoreCase(name)) {
return userIdentifyEnum;
}
}
return null;
}
}
UserRegisterResult
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterResult {
private Long userId;
}
1.5 Service层接口设计
进行接口分离设计,这样设计有助于创建更加灵活多变的可维护,可扩展的软件系统。
public interface UserService {
/**
* 注册
* @param userRegisterParam
* @return
*/
UserRegisterDTO register(UserRegisterParam userRegisterParam);
}
UserRegisterDTO
@Data
public class UserRegisterDTO implements Serializable {
private Long userId;
}
接口实现
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
VerificationCodeService verificationCodeService;
@Override
public UserRegisterDTO register(UserRegisterParam Param) {
checkRegisterInfo(Param);
UserDO userDO=new UserDO();
userDO.setUserName(Param.getName());
userDO.setIdentity(Param.getIdentity());
userDO.setEmail(Param.getMail());
userDO.setPhoneNumber(new Encrypt(Param.getPhoneNumber()));
if(StringUtils.hasText(Param.getPassword())) {
userDO.setPassword(DigestUtil.sha256Hex(Param.getPassword()));
}
userMapper.insert(userDO);
UserRegisterDTO userRegisterDTO = new UserRegisterDTO();
userRegisterDTO.setUserId(userDO.getId());
return userRegisterDTO;
}
private void checkRegisterInfo(UserRegisterParam userRegisterParam) {
/**
* 注册信息为空
*/
if(null == userRegisterParam) {
throw new ServiceException(ServiceErrorCodeConstants.REGISTER_INFO_IS_EMPTY);
}
//检验邮箱格式
if(!RegexUtil.checkMail(userRegisterParam.getMail())) {
throw new ServiceException(ServiceErrorCodeConstants.MAIL_ERROR);
}
//检验手机号格式
if(!RegexUtil.checkMobile(userRegisterParam.getPhoneNumber())) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
//校验身份信息
if(null == UserIdentityEnum.forName(userRegisterParam.getIdentity())) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
//管理员密码不能为空
if(userRegisterParam.getIdentity().equalsIgnoreCase(UserIdentityEnum.ADMIN.name())
&& !StringUtils.hasText(userRegisterParam.getPassword())) {
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_IS_EMPTY);
}
//校验密码,至少六位
if(StringUtils.hasText(userRegisterParam.getPassword())
&&!RegexUtil.checkPassword(userRegisterParam.getPassword())) {
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
}
//检验邮箱是否被使用
if(checkMailUsed(userRegisterParam.getMail())) {
throw new ServiceException(ServiceErrorCodeConstants.MAIL_USED);
}
//校验手机号是否被使用
if(checkPhoneNumberUsed(userRegisterParam.getPhoneNumber())) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_USED);
}
}
private boolean checkPhoneNumberUsed(String phoneNumber) {
int count = userMapper.countByPhoneNumber(new Encrypt(phoneNumber));
return count > 0;
}
private boolean checkMailUsed(String mail) {
int ret=userMapper.countByMail(mail);
return ret > 0;
}
}
其中校验⽤⼾信息,例如邮箱、电话、密码格式的内容,我们封装成了⼀个 util 来完成:
RegexUtil
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);
}
}
这里将手机号类型设置为自定义的Encrypt,是为了让我们存取手机号不需要每次都进行手动加密解密的复杂操作,所以我们使用MyBatis的TypeHandler来处理。
TypeHandler:简单理解就是当处理某些特定字段时,我们可以实现一些方法,让Mybatis遇到这些字段可以自动运行处理。
1.6 Dao层接口设计
@Mapper
public interface UserMapper {
/**
* 获取邮箱绑定的用户数
* @param email
* @return
*/
@Select("select count(*) from user where email=#{email}")
int countByMail(@Param("email") String email);
/**
* 获取手机号绑定的用户数
* @param phoneNumber
* @return
*/
@Select("select count(*) from user where phone_number=#{phoneNumber}")
int countByPhoneNumber(@Param("phoneNumber") Encrypt phoneNumber);
/**
* 注册用户
* @param userDO
*/
@Insert("insert into user (user_name,email,phone_number,identity,password)"+
" values (#{userName},#{email},#{phoneNumber},#{identity},#{password})")
@Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
void insert(UserDO userDO);
}
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 gmtCreate;
//修改时间
private Date gmtModified;
}
Encrypt
@Data
public class Encrypt {
private String value;
public Encrypt(){}
public Encrypt(String value) {
this.value = value;
}
}
EncryptTypeHandler
@MappedTypes(Encrypt.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class EncryptTypeHandler extends BaseTypeHandler<Encrypt> {
private final byte[] KEY = "123456789abcdefg".getBytes();
/**
* 设置参数
* @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);
}
System.out.println("加密的内容:"+parameter.getValue());
AES aes = SecureUtil.aes(KEY);
String str = aes.encryptHex(parameter.getValue());
ps.setString(i,str);
}
/**
* 获取值
* @param rs 结果集合
* @param columnName 索引名
* @return
* @throws SQLException
*/
@Override
public Encrypt getNullableResult(ResultSet rs, String columnName) throws SQLException {
System.out.println("获取值得到的加密内容:" + rs.getString(columnName));
return decrypt(rs.getString(columnName));
}
/**
* 获取值
* @param rs 结果集合
* @param columnIndex 索引位置
* @return
* @throws SQLException
*/
@Override
public Encrypt getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
System.out.println("获取值得到的加密内容:" + rs.getString(columnIndex));
return decrypt(rs.getString(columnIndex));
}
/**
*
* @param cs 结果集合
* @param columnIndex 索引位置
* @return
* @throws SQLException
*/
@Override
public Encrypt getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
System.out.println("获取值得到的加密内容:" + cs.getString(columnIndex));
return decrypt(cs.getString(columnIndex));
}
/**
* 解密
* @return
*/
private Encrypt decrypt (String str) {
if(!StringUtils.hasText(str)) {
return null;
}
return new Encrypt(SecureUtil.aes(KEY).decryptStr(str));
}
}
1.7 全局异常捕获
@RestControllerAdvice
public class GlobalExceptionHandler {
private final static Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(value = ServiceException.class)
public CommonResult<?> serviceException (ServiceException e) {
log.error("ServiceException:",e);
return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()
, e.getMassage());
}
@ExceptionHandler(value = ControllerException.class)
public CommonResult<?> controllerException (ControllerException e) {
log.error("ControllerException:",e);
return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()
, e.getMassage());
}
@ExceptionHandler(value = Exception.class)
public CommonResult<?> exception (Exception e) {
log.error("服务异常:",e);
return CommonResult.error(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR);
}
}
2.登录
发送验证码
2.2 时序图
2.3 配置application.properties
## 短信 ##
sms.access-key-id=填写⾃⼰申请的
sms.access-key-secret=填写⾃⼰申请的
sms.sign-name=填写⾃⼰申请的
如果不想申请短信服务可以直接砍掉这个功能
2.4 SMSUtil短信工具类
@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);
}
}
/**
* 使用AK&SK初始化账号Client
* @return Client
*/
private Client createClient() throws Exception {
// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
// 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html。
Config config = new Config()
.setAccessKeyId(accessKeyId)
.setAccessKeySecret(accessKeySecret);
// Endpoint 请参考 https://api.aliyun.com/product/Dysmsapi
config.endpoint = "dysmsapi.aliyuncs.com";
return new Client(config);
}
}
2.5 CaptchaUtil验证码工具类
依靠Hutool中的随机验证码功能实现。
public class CaptchaUtil {
/**
* 生成随机验证码
*
* @param length 几位
* @return
*/
public static String getCaptcha(int length) {
// 自定义纯数字的验证码(随机4位数字,可重复)
RandomGenerator randomGenerator = new RandomGenerator("0123456789", length);
LineCaptcha lineCaptcha = cn.hutool.captcha.CaptchaUtil.createLineCaptcha(200, 100);
lineCaptcha.setGenerator(randomGenerator);
// 重新生成code
lineCaptcha.createCode();
return lineCaptcha.getCode();
}
}
2.6 约定前后端交互接口
[请求] /verification-code/send?phoneNumber=13199999999 GET
[响应]
{
"code": 200,
"data": true,
"msg": ""
}
2.7 Controlller层接口设计
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
UserService userService;
@Autowired
VerificationCodeService verificationCodeService;
/**
* 发送验证码
* @param phoneNumber
* @return
*/
@RequestMapping("/verification-code/send")
public CommonResult<Boolean> sendVerificationCode(String phoneNumber) {
logger.info("userRegister UserRegisterParam:{}", phoneNumber);
verificationCodeService.sendVerificationCodd(phoneNumber);
return CommonResult.success(Boolean.TRUE);
}
}
2.8 Service层接口设计
public interface VerificationCodeService {
/**
* 发送验证码
* @param phoneNumber
*/
void sendVerificationCodd(String phoneNumber);
/**
* 获取验证码
* @param phoneNumber
* @return
*/
String getVerificationCode(String phoneNumber);
}
接口实现
@Service
public class VerificationCodeServiceImpl implements VerificationCodeService {
@Autowired
SMSUtil smsUtil;
@Autowired
RedisUtil redisUtil;
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_476785514";
@Override
public void sendVerificationCodd(String phoneNumber) {
if(!RegexUtil.checkMobile(phoneNumber)) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
//生成随机验证码
String code = CaptchaUtil.getCaptcha(4);
Map<String,String> map = new HashMap<>();
map.put("code",code);
smsUtil.sendMessage(VERIFICATION_CODE_TEMPLATE_CODE,phoneNumber,
JacksonUtil.writeValueAsString(map));
redisUtil.set(VERIFICATION_CODE_PREFIX+phoneNumber,code,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);
}
}
2.9 Redis的配置与使用
配置 application.properties:
## redis ##
spring.data.redis.host=localhost
spring.data.redis.port=8888
spring.data.redis.timeout=60s
spring.data.redis.lettuce.pool.max-active=8
spring.data.redis.lettuce.pool.max-idle=8
spring.data.redis.lettuce.pool.min-idle=0
spring.data.redis.lettuce.pool.max-wait=5s
这里博主的Redis客户端部署在了云服务器上,配置了隧道。 剩下的配置信息含义分别为:
连接的超时时间为60s,Redis连接池中允许的最大活动连接数为 8,同时存在的Redis连接池中最大空闲量为8,最小为0,当连接池耗尽时,新的请求将等待可用连接的最大时间为 5 秒。
redis工具类编写:
@Configuration
public class RedisUtil {
/**
* RedisTemplate : 先将被存储的数据转换成 字节数组(不可读),再存储到redis中,读取的时候按照字节数组读取
* StringRedisTemplate : 直接存放的就是 string (可读)
* 项目背景:String,String
*/
private final static Logger logger = LoggerFactory.getLogger(RedisUtil.class);
@Autowired
StringRedisTemplate stringRedisTemplate;
//---------String-------------
/**
* 设置值
* @param key
* @param value
* @return
*/
public boolean set(String key , String value) {
try {
stringRedisTemplate.opsForValue().set(key , value);
return true;
}catch (Exception e) {
logger.error("RedisUtil error, set({},{})",key,value,e);
return false;
}
}
/**
* 设置带有时限的值
* @param key
* @param value
* @param time
* @return
*/
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("RedisUtil error, set({},{},{})",key,value,time,e);
return false;
}
}
/**
* 获取值
* @param key
* @return
*/
public String get(String key) {
try {
return StringUtils.hasText(key) ? stringRedisTemplate.opsForValue().get(key) : null;
}catch (Exception e) {
logger.error("RedisUtil error, get({})",key,e);
return null;
}
}
/**
* 删除元素
* @param key
* @return
*/
public boolean del(String... key) {
try {
if (null!=key&&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("RedisUtil error, del({})", key, e);
return false;
}
}
public boolean hasKey(String key) {
try {
return StringUtils.hasText(key) ?
stringRedisTemplate.hasKey(key) : false;
}catch (Exception e) {
logger.error("RedisUtil error, hasKey({})", key, e);
return false;
}
}
}
登录
2.10 JWT令牌
JWT工具类:
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 {
//解析token
claims = 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;
}
}
2.11 登录时序图
2.12 约定前后端交互接口
密码登录
[请求] /password/login POST
{
"loginName":"13199999999",
"password":"123456",
"mandatoryIdentity":"ADMIN"
}
[响应]
{
"code": 200,
"data": {
"token":
"eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxN
jI2MjI5OCwiZXhwIjoxNzE2MjY0MDk4fQ.QfiZmZcfzd5ls_t8lg7bsTF7kA0daK-psjUt1QRj9d4",
"identity": "ADMIN"
},
"msg": ""
}
验证码登录
[请求] /message/login POST
{
"loginMobile":"13199999999",
"verificationCode":"0475",
"mandatoryIdentity":"ADMIN"
}
[响应]
{
"code": 200,
"data": {
"token":
"eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxN
jI2MjUyMywiZXhwIjoxNzE2MjY0MzIzfQ.XEuwO8AvNcqstbOrkI9kWaMhbN-HN2DfnUYGhJthA3I",
"identity": "ADMIN"
},
"msg": ""
}
2.13 Controller层接口设计
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
UserService userService;
@Autowired
VerificationCodeService verificationCodeService;
@RequestMapping("/password/login")
public CommonResult<UserLoginResult> userPasswordLogin (
@Validated @RequestBody UserPasswordLoginParam param) {
logger.info("UserController userPasswordLogin({})"
,JacksonUtil.writeValueAsString(param));
UserLoginDTO userLoginDTO = userService.login(param);
return CommonResult.success(converToUserLoginResult(userLoginDTO));
}
@RequestMapping("/message/login")
public CommonResult<UserLoginResult> shortMessageLogin (
@Validated @RequestBody ShortMessageLoginParam param) {
logger.info("UserController shortMessageLogin({})"
,JacksonUtil.writeValueAsString(param));
UserLoginDTO userLoginDTO = userService.login(param);
return CommonResult.success(converToUserLoginResult(userLoginDTO));
}
private UserLoginResult converToUserLoginResult(UserLoginDTO userLoginDTO) {
if(null==userLoginDTO) {
throw new ControllerException(ControllerErrorCodeConstants.LOGIN_ERROR);
}
UserLoginResult result = new UserLoginResult();
result.setToken(userLoginDTO.getToken());
result.setIdentity(userLoginDTO.getIdentity().name());
return result;
}
UserLoginParam
@Data
public class UserLoginParam implements Serializable {
/**
* 强制某身份登录,不填不限制身份
* @see UserIdentityEnum#name()
*/
private String mandatoryIdentity;
}
ShortMessageLoginParam:
@Data
@EqualsAndHashCode(callSuper = true)
public class ShortMessageLoginParam extends UserLoginParam{
/**
* 电话
*/
@NotBlank(message = "电话不能为空!")
private String loginMobile;
/**
* 验证码
*/
@NotBlank(message = "验证码不能为空!")
private String verificationCode;
}
UserPasswordLoginParam:
@Data
@EqualsAndHashCode(callSuper = true)
public class UserPasswordLoginParam extends UserLoginParam{
/**
* 手机或邮箱
*/
@NotBlank(message = "手机或邮箱不能为空!")
private String loginName;
/**
* 密码
*/
@NotBlank(message = "密码不能为空!")
private String password;
}
UserLoginResult :
@Data
public class UserLoginResult implements Serializable {
/**
* jwt令牌
*/
private String token;
/**
* 用户身份信息
*/
private String identity;
}
2.14 Service层接口设计
public interface UserService {
/**
* 登录
* @param param
* @return
*/
UserLoginDTO login(UserLoginParam param);
}
接口实现:
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
VerificationCodeService verificationCodeService;
@Override
public UserLoginDTO login(UserLoginParam param) {
UserLoginDTO userLoginDTO = null;
if(param instanceof UserPasswordLoginParam loginParam) {
//密码登录
userLoginDTO = loginByUserPassword(loginParam);
} else if (param instanceof ShortMessageLoginParam loginParam) {
//验证码登录
userLoginDTO = loginByShortMessage(loginParam);
}else {
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_INFO_NOT_EXIST);
}
return userLoginDTO;
}
/**
* 验证码登录
* @param loginParam
* @return
*/
private UserLoginDTO loginByShortMessage(ShortMessageLoginParam loginParam) {
if(!RegexUtil.checkMobile(loginParam.getLoginMobile())) {
throw new ServiceException(ServiceErrorCodeConstants.PHONE_NUMBER_ERROR);
}
//获取用户数据
UserDO userDO = userMapper.selectByPhoneNumber(
new Encrypt(loginParam.getLoginMobile()));
if(null == userDO) {
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);
} else if (StringUtils.hasText(userDO.getIdentity())
&&!userDO.getIdentity().equalsIgnoreCase(loginParam.getVerificationCode())) {
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
}
//校验验证码
String code = verificationCodeService.getVerificationCode(
loginParam.getLoginMobile());
if (!loginParam.getVerificationCode().equals(code)) {
throw new ServiceException(ServiceErrorCodeConstants.VERIFICATION_CODE_ERROR);
}
// 塞入返回值(JWT)
Map<String, Object> claim = new HashMap<>();
claim.put("id", userDO.getId());
claim.put("identity", userDO.getIdentity());
String token = JWTUtil.genJwt(claim);
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
}
/**
* 密码登录
* @param loginParam
* @return
*/
private UserLoginDTO loginByUserPassword(UserPasswordLoginParam loginParam) {
UserDO userDO = null;
if(RegexUtil.checkMobile(loginParam.getLoginName())) {
//手机号登录
userDO = userMapper.selectByPhoneNumber(new Encrypt(loginParam.getLoginName()));
} else if (RegexUtil.checkMail(loginParam.getLoginName())) {
//邮箱登录
userDO = userMapper.selectByMail(loginParam.getLoginName());
}else {
throw new ServiceException(ServiceErrorCodeConstants.LOGIN_NOT_EXIST);
}
//校验登录信息
if(null==userDO) {
throw new ServiceException(ServiceErrorCodeConstants.USER_INFO_IS_EMPTY);
} else if (StringUtils.hasText(userDO.getIdentity())
&&!loginParam.getMandatoryIdentity()
.equalsIgnoreCase(userDO.getIdentity())) {
//校验身份信息不通过
throw new ServiceException(ServiceErrorCodeConstants.IDENTITY_ERROR);
} else if (!DigestUtil.sha256Hex(loginParam.getPassword())
.equals(userDO.getPassword())) {
//校验密码不通过
throw new ServiceException(ServiceErrorCodeConstants.PASSWORD_ERROR);
}
//生成jwt
Map<String, Object> claim = new HashMap<>();
claim.put("id", userDO.getId());
claim.put("identity", userDO.getIdentity());
String token = JWTUtil.genJwt(claim);
UserLoginDTO userLoginDTO = new UserLoginDTO();
userLoginDTO.setToken(token);
userLoginDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
return userLoginDTO;
}
}
UserLoginDTO:
@Data
public class UserLoginDTO implements Serializable {
/**
* JWT 令牌
*/
private String token;
/**
* 登录人员身份
*/
private UserIdentityEnum identity;
}
2.15 Dao层接口设计
@Mapper
public interface UserMapper {
@Select("select * from user where phone_number=#{phoneNumber}")
UserDO selectByPhoneNumber(@Param("phoneNumber") Encrypt phoneNumber);
@Select("select * from user where email=#{email}")
UserDO selectByMail(@Param("email") String email);
}
2.16 添加拦截器
采⽤拦截器来完成校验 token 的合法性
@Component
public class LoginInterceptor implements HandlerInterceptor {
private final static Logger logger = LoggerFactory.getLogger(LoginInterceptor.class);
/**
* 预处理,请求之前调用
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request
, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("user_token");
logger.info("请求token:{}",token);
logger.info("请求路径:{}",request.getPathInfo());
//解析token
Claims claims = JWTUtil.parseJWT(token);
if(null==claims) {
logger.error("解析失败!");
return false;
}
logger.info("解析成功");
return true;
}
}
添加配置类 :
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Autowired
LoginInterceptor loginInterceptor;
private final List<String> excludes = Arrays.asList(
"/**/*.html",
"/css/**",
"/js/**",
"/pic/**",
"/*.jpg",
"/*.png",
"/favicon.ico",
"/**/login",
"/register",
"/verification-code/send",
"/winning-records/show"
);
/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(excludes);
}
}
3.人员列表展示
3.1 时序图
3.2 约定前后端交互接口
[请求] /base-user/find-list GET
[响应]
{
"code": 200,
"data": [
{
"userId": 15,
"userName": "郭靖",
"identity": "NORMAL"
},
{
"userId": 14,
"userName": "王五",
"identity": "ADMIN"
}
],
"msg": ""
}
3.3 Controller层接口设计
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@Autowired
UserService userService;
@Autowired
VerificationCodeService verificationCodeService;
@RequestMapping("/base-user/find-list")
public CommonResult<List<BaseUserInfoResult>> findBaseUserInfo(String identity) {
logger.info("UserController findBaseUSerInfo({})",identity);
List<UserDTO> userDTOList = userService.findUserInfo(
UserIdentityEnum.forName(identity));
return CommonResult.success(converToList(userDTOList));
}
private List<BaseUserInfoResult> converToList(List<UserDTO> userDTOList) {
return userDTOList.stream()
.map(userDTO -> {
BaseUserInfoResult result = new BaseUserInfoResult();
result.setUserName(userDTO.getUserName());
result.setIdentity(userDTO.getIdentity().name());
result.setUserId(userDTO.getUserId());
return result;
}).collect(Collectors.toList());
}
}
UserBaseInfoResult:
@Data
public class BaseUserInfoResult {
/**
* 人员id
*/
private Long userId;
/**
* 姓名
*/
private String userName;
/**
* 身份信息
*/
private String identity;
}
3.4 Service接口设计
public interface UserService {
/**
* 查找人员列表
* @param identity
* @return
*/
List<UserDTO> findUserInfo(UserIdentityEnum identity);
}
接口实现:
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper userMapper;
@Autowired
VerificationCodeService verificationCodeService;
@Override
public List<UserDTO> findUserInfo(UserIdentityEnum identity) {
String identityString = null == identity ? null : identity.name();
List<UserDO> userDOList = userMapper.selectUserListByIdentity(identityString);
List<UserDTO> userDTOList = userDOList.stream()
.map(userDO ->{
UserDTO userDTO = new UserDTO();
userDTO.setUserName(userDO.getUserName());
userDTO.setUserId(userDO.getId());
userDTO.setIdentity(UserIdentityEnum.forName(userDO.getIdentity()));
userDTO.setPhoneNumber(userDO.getPhoneNumber().getValue());
userDTO.setEmail(userDO.getEmail());
return userDTO;
}).collect(Collectors.toList());
return userDTOList;
}
}
3.5 Dao层接口设计
@Mapper
public interface UserMapper {
@Select("<script>" +
" select * from user" +
" <if test=\"identity!=null\">" +
" where identity = #{identity}" +
" </if>" +
" order by id desc" +
" </script>")
List<UserDO> selectUserListByIdentity(@Param("identity") String identity);
}
六、奖品模块
1. 图片上传
application.properties 配置上传⽂件路径
## 图⽚服务 ##
pic.local-path=D:/PIC
# spring boot3 升级配置名
spring.web.resources.static-locations=classpath:/static/,file:${pic.local-path}
图⽚服务 PictureService 接⼝定义
public interface PictureService {
/**
* 保存图片
* @param multipartFile :上传文件的工具类
* @return 索引 上传后的文件名
*/
String savePicture (MultipartFile multipartFile);
}
接口实现:
@Service
public class PictureServiceImpl implements PictureService {
@Value("${pic.local-path}")
private String localPath;
@Override
public String savePicture(MultipartFile multipartFile) {
//创建目录
File dir = new File(localPath);
if(!dir.exists()) {
dir.mkdirs();
}
//创建索引
String filename=multipartFile.getOriginalFilename();
assert null!=filename;
String suffix = filename.substring(filename.lastIndexOf("."));
filename = UUID.randomUUID() +suffix;
//图片保存
try {
multipartFile.transferTo(new File(localPath + "/" + filename));
} catch (IOException e) {
throw new ServiceException(ServiceErrorCodeConstants.PIC_UPLOAD_ERROR);
}
return filename;
}
}
二进制文件上传配置类:
@Component
public class MultipartJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
protected MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper) {
// MediaType.APPLICATION_OCTET_STREAM 表示这个转换器用于处理二进制流数据,通常用于文件上传。
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// 转换器不用于写入(即不用于响应的序列化)
return false;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}
2. 奖品创建
2.1 时序图
2.2 约定前后端交互接口
[请求] /prize/create POST
param: {"prizeName":"吹⻛机","description":"吹⻛机","price":100}
prizePic: Obj-C.jpg (FILE)
[响应]
{
"code": 200,
"data": 17,
"msg": ""
}
2.3 Controller层接口设计
@RestController
public class PrizeController {
private final static Logger logger = LoggerFactory.getLogger(PrizeController.class);
@Autowired
private PrizeService prizeService;
@Autowired
private PictureService pictureService;
@RequestMapping("/pic/upload")
public String uploadPic(MultipartFile multipartFile) {
return pictureService.savePicture(multipartFile);
}
/**
* 创建奖品
* RequestPart:用于接收表单数据的 multipart/form-data
*
* @param param
* @param multipartFile
* @return
*/
@RequestMapping("/prize/create")
public CommonResult<Long> createPrize(@Validated @RequestPart("param") CreatePrizeParam param
,@RequestPart("prizePic") MultipartFile multipartFile) {
logger.info("PrizeController createPrize({})"
, JacksonUtil.writeValueAsString(param));
return CommonResult.success(prizeService.createPrize(param,multipartFile));
}
}
CreatePrizeParam:
@Data
public class CreatePrizeParam implements Serializable {
/**
* 奖品名
*/
@NotBlank(message = "奖品名不能为空!")
private String prizeName;
/**
* 描述
*/
private String description;
/**
* 价格
*/
@NotNull(message = "奖品价格不能为空!")
private BigDecimal price;
}
2.4 Service层接口设计
public interface PrizeService {
/**
* 创建奖品
* @param param
* @param picFile
* @return
*/
Long createPrize(CreatePrizeParam param, MultipartFile picFile);
}
接口实现:
@Service
public class PrizeServiceImpl implements PrizeService {
@Autowired
PictureService pictureService;
@Autowired
PrizeMapper prizeMapper;
@Override
public Long createPrize(CreatePrizeParam param, MultipartFile picFile) {
//上传图片
String fileName = pictureService.savePicture(picFile);
//存表
PrizeDO prizeDO = new PrizeDO();
prizeDO.setName(param.getPrizeName());
prizeDO.setDescription(param.getDescription());
prizeDO.setImageUrl(fileName);
prizeDO.setPrice(param.getPrice());
prizeMapper.insert(prizeDO);
return prizeDO.getId();
}
}
2.5 Dao层接口设计
@Mapper
public interface PrizeMapper {
@Insert("insert into prize (name,description,price,image_url) " +
"values (#{name},#{description},#{price},#{imageUrl})")
@Options(useGeneratedKeys = true,keyProperty = "id",keyColumn = "id")
void insert(PrizeDO prizeDO);
}
PrizeDO:
@Data
@EqualsAndHashCode(callSuper = true)
public class PrizeDO extends BaseDO{
/**
* 奖品名
*/
private String name;
/**
* 图片索引
*/
private String imageUrl;
/**
* 价格
*/
private BigDecimal price;
/**
* 描述
*/
private String description;
}
3. 奖品列表展示(翻页)
3.1 时序图
3.2 约定前后端交互接口
[请求] /prize/find-list?currentPage=1&pageSize=10 GET
[响应]
{
"code": 200,
"data": {
"total": 3,
"records": [
{
"prizeId": 17,
"prizeName": "吹⻛机",
"description": "吹⻛机",
"price": 100,
"imageUrl": "d11fa79c-9cfb-46b9-8fb6-3226ba1ff6d6.jpg"
},
{
"prizeId": 13,
"prizeName": "华为⼿机",
"description": "华为⼿机",
"price": 5000,
"imageUrl": "5a85034b-91b7-48fe-953d-67aef2bdcc2d.jpg"
},
{
"prizeId": 12,
"prizeName": "咖啡机",
"description": "家⽤咖啡机",
"price": 3000,
"imageUrl": "https://ts1.cn.mm.bing.net/th/id/RC.59493f741a4d956f354d241ec1034624?
rik=JpdNO%2bfC3NMONw&riu=http%3a%2f%2fcdn02.ehaier.com%2fproduct%2f5600fa6c1a0a
2ebc278b47e8_1200_1200.jpg&ehk=8MptQ5r5ILWiL4v%2f5mn3s0%2f1H05r1yp%2fL6feezFw89
Q%3d&risl=&pid=ImgRaw&r=0"
}
]
},
"msg": ""
}
3.3 Controller层接口设计
@RestController
public class PrizeController {
private final static Logger logger = LoggerFactory.getLogger(PrizeController.class);
@Autowired
private PrizeService prizeService;
@Autowired
private PictureService pictureService;
@RequestMapping("/prize/find-list")
public CommonResult<FindPrizeListResult> findPrizeList(PageParam param) {
logger.info("PrizeController findPrizeList({})", param);
PageListDTO<PrizeDTO> pageListDTO = prizeService.findPrizeList(param);
return CommonResult.success(convertToFindPrizeListResult(pageListDTO));
}
private FindPrizeListResult convertToFindPrizeListResult(PageListDTO<PrizeDTO> pageListDTO) {
if(null == pageListDTO) {
throw new ControllerException(ControllerErrorCodeConstants.FIND_PRIZE_LIST_ERROR);
}
FindPrizeListResult result = new FindPrizeListResult();
result.setTotal(pageListDTO.getTotal());
result.setRecords(
pageListDTO.getRecords().stream()
.map(prizeDTO -> {
FindPrizeListResult.PrizeInfo prizeInfo = new FindPrizeListResult.PrizeInfo();
prizeInfo.setPrizeName(prizeDTO.getName());
prizeInfo.setPrizeId(prizeDTO.getPrizeId());
prizeInfo.setDescription(prizeDTO.getDescription());
prizeInfo.setPrice(prizeDTO.getPrice());
return prizeInfo;
}).collect(Collectors.toList())
);
return result;
}
}
PageParam:
@Data
public class PageParam implements Serializable {
/**
* 当前页
*/
private Integer currentPage = 1;
/**
* 需要展示的数据数
*/
private Integer pageSize = 10;
public Integer getOffset() {
return (currentPage-1)*pageSize;
}
}
FindPrizeListResult:
@Data
public class FindPrizeListResult implements Serializable {
/**
* 总量
*/
private Integer total;
/**
* 当前列表
*/
private List<PrizeInfo> records;
@Data
public static class PrizeInfo implements Serializable {
/**
* 奖品id
*/
private Long prizeId;
/**
* 名称
*/
private String prizeName;
/**
* 描述
*/
private String description;
/**
* 价值
*/
private BigDecimal price;
/**
* 奖品图
*/
private String imageUrl;
}
}
3.4 Service层接口设计
public interface PrizeService {
/**
* 创建奖品
* @param param
* @param picFile
* @return
*/
Long createPrize(CreatePrizeParam param, MultipartFile picFile);
}
接口实现:
@Service
public class PrizeServiceImpl implements PrizeService {
@Autowired
PictureService pictureService;
@Autowired
PrizeMapper prizeMapper;
@Override
public PageListDTO<PrizeDTO> findPrizeList(PageParam param) {
//总量
int total = prizeMapper.count();
//查询当前列表
List<PrizeDTO> prizeDTOList = new ArrayList<>();
List<PrizeDO> prizeDOList = prizeMapper.selectPrizeList(param.getOffset()
,param.getPageSize());
for (PrizeDO prizeDO : prizeDOList) {
PrizeDTO prizeDTO = new PrizeDTO();
prizeDTO.setPrizeId(prizeDO.getId());
prizeDTO.setName(prizeDO.getName());
prizeDTO.setDescription(prizeDO.getDescription());
prizeDTO.setImageUrl(prizeDO.getImageUrl());
prizeDTO.setPrice(prizeDO.getPrice());
prizeDTOList.add(prizeDTO);
}
return new PageListDTO<>(total, prizeDTOList);
}
}
PrizeDTO:
@Data
public class PrizeDTO {
/**
* 奖品Id
*/
private Long prizeId;
/**
* 奖品名
*/
private String name;
/**
* 图片索引
*/
private String imageUrl;
/**
* 价格
*/
private BigDecimal price;
/**
* 描述
*/
private String description;
}
PageListDTO <T>:
@Data
@AllArgsConstructor
public class PageListDTO <T>{
/**
* 总量
*/
private Integer total;
/**
* 当前页列表
*/
private List<T> records;
}
3.5 Dao层接口设计
@Mapper
public interface PrizeMapper {
@Select("select count(*) from prize")
int count();
@Select("select * from prize order by id desc limit #{offset},#{pageSize}")
List<PrizeDO> selectPrizeList(@Param("offset") Integer offset
,@Param("pageSize") Integer pageSize);
}
七、活动模块
1. 活动创建
1.1 需求回顾
创建的活动信息包含:活动名称 , 活动描述 , 关联的一批奖品同时需要选择奖品等级(一二三等奖)以及奖品库存 , 圈选一批人员参与抽奖。
1.2 时序图
1.3 约定前后端交互接口
[请求] /activity/create POST
{
"activityName": "抽奖测试",
"description": "年会抽奖活动",
"activityPrizeList": [
{
"prizeId": 13,
"prizeAmount": 1,
"prizeTiers": "FIRST_PRIZE"
},
{
"prizeId": 12,
"prizeAmount": 1,
"prizeTiers": "SECOND_PRIZE"
}
],
"activityUserList": [
{
"userId": 25,
"userName": "郭靖"
},
{
"userId": 23,
"userName": "杨康"
}
]
}
[响应]
{
"code": 200,
"data": {
"activityId": 23
},
"msg": ""
}
1.4 Controller层接口设计
@RestController
public class ActivityController {
private final static Logger logger = LoggerFactory.getLogger(ActivityController.class);
@Autowired
ActivityService activityService;
/**
* 创建活动
*
* @param param
* @return
*/
@RequestMapping("/activity/create")
public CommonResult<CreateActivityResult> createActivity(
@Validated @RequestBody CreateActivityParam param) {
logger.info("ActivityController createActivity({})"
, JacksonUtil.writeValueAsString(param));
return CommonResult.success(
convertToCreateActivityResult(activityService.createActivity(param)));
}
private CreateActivityResult convertToCreateActivityResult(CreateActivityDTO activity) {
if(null == activity) {
throw new
ControllerException(ControllerErrorCodeConstants.CREATE_ACTIVITY_ERROR);
}
CreateActivityResult result = new CreateActivityResult();
result.setActivityId(activity.getActivityId());
return result;
}
}
CreateActivityParam:
@Data
public class CreateActivityParam implements Serializable {
/**
* 活动名称
*/
@NotBlank(message = "活动名称不能为空!")
private String activityName;
/**
* 描述
*/
@NotBlank(message = "活动描述不能为空!")
private String description;
/**
* 活动关联奖品列表
*/
@NotEmpty(message = "活动关联奖品列表不能为空!")
@Valid
private List<CreatePrizeByActivityParam> activityPrizeList;
/**
* 活动关联人员列表
*/
@NotEmpty(message = "活动关联人员列表不能为空!")
@Valid
private List<CreateUserByActivityParam> activityUserList;
}
CreateActivityResult:
@Data
public class CreateActivityResult implements Serializable {
/**
* 创建的活动id
*/
private Long activityId;
}
1.5 Service层接口设计
public interface ActivityService {
/**
* 创建活动
* @param param
* @return
*/
CreateActivityDTO createActivity(CreateActivityParam param);
}
接口实现:
@Service
public class ActivityServiceImpl implements ActivityService {
/**
* 活动缓存前置,为了区分业务
*/
private final String ACTIVITY_PREFIX = "ACTIVITY_";
/**
* 活动缓存过期时间
*/
private final Long ACTIVITY_TIMEOUT = 60 * 60 * 24 * 3L;
Logger logger = LoggerFactory.getLogger(ActivityServiceImpl.class);
@Autowired
RedisUtil redisUtil;
@Autowired
ActivityMapper activityMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private PrizeMapper prizeMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Autowired
private ActivityUserMapper activityUserMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public CreateActivityDTO createActivity(CreateActivityParam param) {
//校验活动信息是否正确
checkActivityInfo(param);
//保存活动信息
ActivityDO activityDO = new ActivityDO();
activityDO.setActivityName(param.getActivityName());
activityDO.setDescription(param.getDescription());
activityDO.setStatus(ActivityStatusEnum.RUNNING.name());
activityMapper.insert(activityDO);
//保存相关联的奖品信息
List<CreatePrizeByActivityParam> prizeByActivityParam = param.getActivityPrizeList();
List<ActivityPrizeDO> prizeDOList = prizeByActivityParam.stream()
.map(prizeParam -> {
ActivityPrizeDO result = new ActivityPrizeDO();
result.setActivityId(activityDO.getId());
result.setPrizeId(prizeParam.getPrizeId());
result.setPrizeAmount(prizeParam.getPrizeAmount());
result.setPrizeTiers(prizeParam.getPrizeTiers());
result.setStatus(ActivityPrizeStatusEnum.INIT.name());
return result;
}).collect(Collectors.toList());
activityPrizeMapper.batchInsert(prizeDOList);
//保存相关联的人员信息
List<CreateUserByActivityParam> userByActivityParam = param.getActivityUserList();
List<ActivityUserDO> userDOList = userByActivityParam.stream()
.map(userParam -> {
ActivityUserDO result = new ActivityUserDO();
result.setActivityId(activityDO.getId());
result.setUserId(userParam.getUserId());
result.setUserName(userParam.getUserName());
result.setStatus(ActivityUserStatusEnum.INIT.name());
return result;
}).collect(Collectors.toList());
activityUserMapper.batchInsert(userDOList);
//整合相关活动信息,存放redis
List<Long> prizeIds = param.getActivityPrizeList().stream()
.map(CreatePrizeByActivityParam::getPrizeId).collect(Collectors.toList());
List<PrizeDO> prizeDOList1 = prizeMapper.batchSelectByIds(prizeIds);
ActivityDetailDTO detailDTO = convertToActivityDetailDTO(activityDO, userDOList,
prizeDOList1, prizeDOList);
cacheActivity(detailDTO);
//构造返回
CreateActivityDTO activityDTO = new CreateActivityDTO();
activityDTO.setActivityId(activityDO.getId());
return activityDTO;
}
private ActivityDetailDTO convertToActivityDetailDTO(
ActivityDO activityDO, List<ActivityUserDO> activityUserDOList
, List<PrizeDO> prizeDOList, List<ActivityPrizeDO> activityPrizeDOList) {
ActivityDetailDTO detailDTO = new ActivityDetailDTO();
detailDTO.setActivityId(activityDO.getId());
detailDTO.setActivityName(activityDO.getActivityName());
detailDTO.setDesc(activityDO.getDescription());
detailDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));
List<ActivityDetailDTO.PrizeDTO> prizeDTOList = activityPrizeDOList
.stream()
.map(apDO -> {
ActivityDetailDTO.PrizeDTO prizeDTO = new ActivityDetailDTO.PrizeDTO();
prizeDTO.setPrizeId(apDO.getPrizeId());
Optional<PrizeDO> optionalPrizeDO = prizeDOList.stream()
.filter(prizeDO -> prizeDO.getId().equals(apDO.getPrizeId()))
.findFirst();
// 如果PrizeDO为空,不执行当前方法,不为空才执行
optionalPrizeDO.ifPresent(prizeDO -> {
prizeDTO.setName(prizeDO.getName());
prizeDTO.setImageUrl(prizeDO.getImageUrl());
prizeDTO.setPrice(prizeDO.getPrice());
prizeDTO.setDescription(prizeDO.getDescription());
});
prizeDTO.setTiers(ActivityPrizeTiersEnum.forName(apDO.getPrizeTiers()));
prizeDTO.setPrizeAmount(apDO.getPrizeAmount());
prizeDTO.setStatus(ActivityPrizeStatusEnum.forName(apDO.getStatus()));
return prizeDTO;
}).collect(Collectors.toList());
detailDTO.setPrizeDTOList(prizeDTOList);
List<ActivityDetailDTO.UserDTO> userDTOList = activityUserDOList.stream()
.map(auDO -> {
ActivityDetailDTO.UserDTO userDTO = new ActivityDetailDTO.UserDTO();
userDTO.setUserId(auDO.getUserId());
userDTO.setUserName(auDO.getUserName());
userDTO.setStatus(ActivityUserStatusEnum.forName(auDO.getStatus()));
return userDTO;
}).collect(Collectors.toList());
detailDTO.setUserDTOList(userDTOList);
return detailDTO;
}
private void cacheActivity(ActivityDetailDTO detailDTO) {
// key: ACTIVITY_12
// value: ActivityDetailDTO(json)
if (null == detailDTO || null == detailDTO.getActivityId()) {
logger.warn("要缓存的活动信息不存在!");
return;
}
try {
redisUtil.set(ACTIVITY_PREFIX + detailDTO.getActivityId(),
JacksonUtil.writeValueAsString(detailDTO),
ACTIVITY_TIMEOUT);
} catch (Exception e) {
logger.error("缓存活动异常,ActivityDetailDTO={}",
JacksonUtil.writeValueAsString(detailDTO),
e);
}
}
/**
* 校验活动有效性
*
* @param param
*/
private void checkActivityInfo(CreateActivityParam param) {
if (null == param) {
throw new ServiceException(ServiceErrorCodeConstants.CREATE_ACTIVITY_INFO_IS_EMPTY);
}
//查看人员相关信息是否存在
List<Long> userIds = param.getActivityUserList().stream()
.map(CreateUserByActivityParam::getUserId)
.distinct().collect(Collectors.toList());
List<Long> existUserIds = userMapper.selectExistByIds(userIds);
if (CollectionUtils.isEmpty(existUserIds)) {
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_USER_ERROR);
}
userIds.forEach(id -> {
if (!existUserIds.contains(id)) {
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_USER_ERROR);
}
});
//查看奖品相关信息是否存在
List<Long> prizeIds = param.getActivityPrizeList().stream()
.map(CreatePrizeByActivityParam::getPrizeId)
.collect(Collectors.toList());
List<Long> existPrizeIds = prizeMapper.selectExistByIds(prizeIds);
if (CollectionUtils.isEmpty(existPrizeIds)) {
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_ERROR);
}
prizeIds.forEach(id -> {
if(!existPrizeIds.contains(id)) {
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_ERROR);
}
});
}
}
优点与难点:利用Spring框架事务的特性,用@Transactional注解保证了整个活动创建过程的原子性, 利用Redis缓存活动信息减轻了数据库负担。
活动/活动奖品/活动⼈员状态 枚举类(Enum)
@AllArgsConstructor
@Getter
public enum ActivityPrizeStatusEnum {
INIT(1, "初始化"),
COMPLETED(2, "已被抽取");
private final Integer code;
private final String message;
public static ActivityPrizeStatusEnum forName(String name) {
for (ActivityPrizeStatusEnum activityPrizeStatusEnum : ActivityPrizeStatusEnum.values()) {
if (activityPrizeStatusEnum.name().equalsIgnoreCase(name)) {
return activityPrizeStatusEnum;
}
}
return null;
}
}
@AllArgsConstructor
@Getter
public enum ActivityPrizeTiersEnum {
FIRST_PRIZE(1, "一等奖"),
SECOND_PRIZE(2, "二等奖"),
THIRD_PRIZE(3, "三等奖");
private final Integer code;
private final String message;
public static ActivityPrizeTiersEnum forName(String name) {
for (ActivityPrizeTiersEnum activityPrizeTiersEnum : ActivityPrizeTiersEnum.values()) {
if (activityPrizeTiersEnum.name().equalsIgnoreCase(name)) {
return activityPrizeTiersEnum;
}
}
return null;
}
}
@AllArgsConstructor
@Getter
public enum ActivityStatusEnum {
RUNNING(1, "活动进行中"),
COMPLETED(2, "活动已完成");
private final Integer code;
private final String message;
public static ActivityStatusEnum forName(String name) {
for (ActivityStatusEnum activityStatusEnum : ActivityStatusEnum.values()) {
if (activityStatusEnum.name().equalsIgnoreCase(name)) {
return activityStatusEnum;
}
}
return null;
}
}
@Getter
@AllArgsConstructor
public enum ActivityUserStatusEnum {
INIT(1, "初始化"),
COMPLETED(2, "已被抽取");
private final Integer code;
private final String message;
public static ActivityUserStatusEnum forName(String name) {
for (ActivityUserStatusEnum activityUserStatusEnum : ActivityUserStatusEnum.values()) {
if (activityUserStatusEnum.name().equalsIgnoreCase(name)) {
return activityUserStatusEnum;
}
}
return null;
}
}
1.6 Dao层接口设计
@Mapper
public interface ActivityMapper {
@Insert("insert into activity(activity_name,description,status)" +
"values (#{activityName},#{description},#{status})")
@Options(useGeneratedKeys = true, keyProperty ="id", keyColumn ="id")
void insert(ActivityDO activityDO);
}
@Mapper
public interface ActivityPrizeMapper {
@Insert("<script>" +
" insert into activity_prize (activity_id, prize_id, prize_amount, prize_tiers, status)" +
" values <foreach collection = 'items' item='item' index='index' separator=','>" +
" (#{item.activityId}, #{item.prizeId}, #{item.prizeAmount}, #{item.prizeTiers}, #{item.status})" +
" </foreach>" +
" </script>")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
void batchInsert(@Param("items") List<ActivityPrizeDO> prizeDOList);
}
@Mapper
public interface ActivityUserMapper {
@Insert("<script>" +
" insert into activity_user (activity_id, user_id, user_name, status)" +
" values <foreach collection = 'items' item='item' index='index' separator=','>" +
" (#{item.activityId}, #{item.userId}, #{item.userName}, #{item.status})" +
" </foreach>" +
" </script>")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
void batchInsert(@Param("items") List<ActivityUserDO> userDOList);
}
@Mapper
public interface PrizeMapper {
@Select("<script>" +
" select id from prize" +
" where id in" +
" <foreach collection='items' item='item' open='(' separator=',' close=')'>" +
" #{item}" +
" </foreach>" +
" </script>")
List<Long> selectExistByIds(@Param("items") List<Long> prizeIds);
@Select("<script>" +
" select * from prize" +
" where id in" +
" <foreach collection='items' item='item' open='(' separator=',' close=')'>" +
" #{item}" +
" </foreach>" +
" </script>")
List<PrizeDO> batchSelectByIds(@Param("items") List<Long> ids);
@Select("select * from prize where id=#{prizeId}")
PrizeDO selectById(@Param("prizeId") Long prizeId);
}
@Mapper
public interface UserMapper {
@Select("<script>" +
" select id from user" +
" where id in" +
" <foreach collection='items' item='item' open='(' separator=',' close=')'>" +
" #{item}" +
" </foreach>" +
" </script>")
List<Long> selectExistByIds(@Param("items") List<Long> userIds);
@Select("<script>" +
" select * from user" +
" where id in" +
" <foreach collection='items' item='item' open='(' separator=',' close=')'>" +
" #{item}" +
" </foreach>" +
" </script>")
List<UserDO> batchSelectByIds(@Param("items") List<Long> userIds);
}
2.活动列表展示(翻页)
2.1 时序图
2.2 约定前后端交互接口
[请求] /activity/find-list?currentPage=1&pageSize=10 GET
[响应]
{
"code": 200,
"data": {
"total": 10,
"records": [
{
"activityId": 23,
"activityName": "抽奖测试",
"description": "年会抽奖活动",
"valid": true
},
{
"activityId": 22,
"activityName": "抽奖测试",
"description": "年会抽奖活动",
"valid": true
},
{
"activityId": 21,
"activityName": "节⽇抽奖",
"description": "年会抽奖活动",
"valid": true
}
]
},
"msg": ""
}
2.3 Controller层接口设计
@RestController
public class ActivityController {
private final static Logger logger = LoggerFactory.getLogger(ActivityController.class);
@Autowired
ActivityService activityService;
@RequestMapping("/activity/find-list")
public CommonResult<FindActivityListResult> findActivityList(PageParam param) {
logger.info("ActivityController findActivityList({})",param);
return CommonResult.success(
convertToFindActivityResult(activityService.findActivityList(param))
);
}
private FindActivityListResult convertToFindActivityResult(
PageListDTO<ActivityDTO> activityList) {
if(null == activityList) {
throw new ControllerException(ControllerErrorCodeConstants.FIND_PRIZE_LIST_ERROR);
}
FindActivityListResult result = new FindActivityListResult();
result.setTotal(activityList.getTotal());
result.setRecords(activityList.getRecords().stream()
.map(activityDTO -> {
FindActivityListResult.ActivityInfo activityInfo = new FindActivityListResult.ActivityInfo();
activityInfo.setActivityId(activityDTO.getActivityId());
activityInfo.setActivityName(activityDTO.getActivityName());
activityInfo.setDescription(activityDTO.getDescription());
activityInfo.setValid(activityDTO.valid());
return activityInfo;
}).collect(Collectors.toList()));
return result;
}
}
2.4 Service层接口设计
public interface ActivityService {
/**
* 获取活动列表
* @param param
* @return
*/
PageListDTO<ActivityDTO> findActivityList(PageParam param);
}
接口实现:
@Service
public class ActivityServiceImpl implements ActivityService {
/**
* 活动缓存前置,为了区分业务
*/
private final String ACTIVITY_PREFIX = "ACTIVITY_";
/**
* 活动缓存过期时间
*/
private final Long ACTIVITY_TIMEOUT = 60 * 60 * 24 * 3L;
Logger logger = LoggerFactory.getLogger(ActivityServiceImpl.class);
@Autowired
RedisUtil redisUtil;
@Autowired
ActivityMapper activityMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private PrizeMapper prizeMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Autowired
private ActivityUserMapper activityUserMapper;
@Override
public PageListDTO<ActivityDTO> findActivityList(PageParam param) {
//获取总量
int total = activityMapper.count();
//获取当前页列表
List<ActivityDO> activityDOList = activityMapper.selectActivityList(
param.getOffset(),param.getPageSize());
List<ActivityDTO> activityDTOList = activityDOList.stream()
.map(activityDO -> {
ActivityDTO activityDTO = new ActivityDTO();
activityDTO.setActivityName(activityDO.getActivityName());
activityDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));
activityDTO.setDescription(activityDO.getDescription());
activityDTO.setActivityId(activityDO.getId());
return activityDTO;
}).collect(Collectors.toList());
return new PageListDTO<>(total, activityDTOList);
}
}
2.5 Dao层接口设计
@Mapper
public interface ActivityMapper {
@Select("select count(1) from activity")
int count();
@Select("select * from activity order by id desc limit #{offset},#{pageSize}")
List<ActivityDO> selectActivityList(@Param("offset") Integer offset
, @Param("pageSize") Integer pageSize);
}
八、抽奖模块
1.抽奖设计

2.RabbitMQ配置使用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
application.properties
## mq ##
spring.rabbitmq.host=你的云服务器端口号或者将RabbitMQ下载到本地
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
#消息确认机制,默认auto
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#设置失败重试 5次
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=5
DirectRabbitConfig 配置类
@Configuration
public class DirectRabbitConfig {
public static final String QUEUE_NAME = "DirectQueue";
public static final String EXCHANGE_NAME = "DirectExchange";
public static final String ROUTING = "DirectRouting";
public static final String DLX_QUEUE_NAME = "DlxDirectQueue";
public static final String DLX_EXCHANGE_NAME = "DlxDirectExchange";
public static final String DLX_ROUTING = "DlxDirectRouting";
/**
* 队列 起名:DirectQueue
*
* @return
*/
@Bean
public Queue directQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("DirectQueue",true,true,false);
// 一般设置一下队列的持久化就好,其余两个就是默认false
// return new Queue(QUEUE_NAME,true);
// 普通队列绑定死信交换机
return QueueBuilder.durable(QUEUE_NAME)
.deadLetterExchange(DLX_EXCHANGE_NAME)
.deadLetterRoutingKey(DLX_ROUTING).build();
}
/**
* Direct交换机 起名:DirectExchange
*
* @return
*/
@Bean
DirectExchange directExchange() {
return new DirectExchange(EXCHANGE_NAME,true,false);
}
/**
* 绑定 将队列和交换机绑定, 并设置用于匹配键:DirectRouting
*
* @return
*/
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(directQueue())
.to(directExchange())
.with(ROUTING);
}
/**
* 死信队列
*
* @return
*/
@Bean
public Queue dlxQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("DirectQueue",true,true,false);
// 一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue(DLX_QUEUE_NAME,true);
}
/**
* 死信交换机
*
* @return
*/
@Bean
DirectExchange dlxExchange() {
return new DirectExchange(DLX_EXCHANGE_NAME,true,false);
}
/**
* 绑定死信队列与交换机
*
* @return
*/
@Bean
Binding bindingDlx() {
return BindingBuilder.bind(dlxQueue())
.to(dlxExchange())
.with(DLX_ROUTING);
}
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
}
3.抽奖请求处理
3.1 时序图
3.2 约定前后端交互接口
[请求] /draw-prize POST
{
"winnerList":[
{
"userId":15,
"userName":"陈"
},
{
"userId":21,
"userName":"范闲"
}
],
"activityId":23,
"prizeId":13,
"prizeTiers":"FIRST_PRIZE",
"winningTime":"2024-05-21T11:55:10.000Z"
}
[响应]
{
"code": 200,
"data": true,
"msg": ""
}
3.3 Controller层接口设计
@RestController
public class DrawPrizeController {
private static final Logger logger = LoggerFactory.getLogger(DrawPrizeController.class);
@Autowired
private DrawPrizeService drawPrizeService;
@RequestMapping("/draw-prize")
public CommonResult<Boolean> drawPrize(
@Validated @RequestBody DrawPrizeParam param) {
logger.info("drawPrize DrawPrizeParam:{}", param);
// service
drawPrizeService.drawPrize(param);
return CommonResult.success(true);
}
}
3.4 Service层接口设计
public interface DrawPrizeService {
/**
* 异步抽奖接口
* @param param
*/
void drawPrize(DrawPrizeParam param);
}
接口实现:
@Service
public class DrawPrizeServiceImpl implements DrawPrizeService {
private final String WINNING_RECORDS_PREFIX = "WINNING_RECORDS_";
private final Long WINNING_RECORDS_TIMEOUT = 60 * 60 * 24 * 2L;
private final static Logger logger = LoggerFactory.getLogger(DrawPrizeServiceImpl.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ActivityMapper activityMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Autowired
private PrizeMapper prizeMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private WinningRecordMapper winningRecordMapper;
@Autowired
private RedisUtil redisUtil;
@Override
public void drawPrize(DrawPrizeParam param) {
Map<String,String> map = new HashMap<>();
map.put("messageId",String.valueOf(UUID.randomUUID()));
map.put("messageData", JacksonUtil.writeValueAsString(param));
//发消息,交换机,绑定的key,消息体
rabbitTemplate.convertAndSend(EXCHANGE_NAME,ROUTING,map);
logger.info("mq 消息发送成功,map:{}",JacksonUtil.writeValueAsString(map));
}
}
4. MQ异步抽奖逻辑执行
4.1 时序图
4.2 消费MQ消息
消费者MqReceiver实现:
@Component
@RabbitListener(queues = QUEUE_NAME)
public class MqReceiver {
private static final Logger logger = LoggerFactory.getLogger(MqReceiver.class);
@Autowired
private DrawPrizeService drawPrizeService;
@Autowired
private ActivityStatusManager activityStatusManager;
@Qualifier("asyncServiceExecutor")
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Autowired
private MailUtil mailUtil;
@Autowired
private SMSUtil smsUtil;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Autowired
private WinningRecordMapper winningRecordMapper;
@RabbitHandler
public void process(Map<String,String> map) {
logger.info("mq成功接收到消息,message({})"
, JacksonUtil.writeValueAsString(map));
String paramString = map.get("messageData");
DrawPrizeParam param = JacksonUtil.readValue(paramString, DrawPrizeParam.class);
try {
//处理抽奖流程
drawPrizeService.checkDrawPrizeParam(param);
//状态扭转处理
statusConvert(param);
//保存中奖者名单
List<WinningRecordDO> winningRecordDOList =
drawPrizeService.saveWinnerRecords(param);
//通知中奖者(短信,邮件)
//异步(并发)处理
syncExecute(winningRecordDOList);
}catch (ServiceException e) {
logger.error("处理MQ 消息异常,{},{}",e.getCode(),e.getMassage(),e);
//需要保证事务一致性(回滚)
rollback(param);
//抛出异常
throw e;
}catch (Exception e) {
logger.error("处理MQ 消息异常");
//需要保证事务一致性(回滚)
rollback(param);
//抛出异常
throw e;
}
}
/**
* 处理抽奖异常的回滚行为:恢复处理请求之前的库表状态
*
* @param param
*/
private void rollback(DrawPrizeParam param) {
// 1、回滚状态:活动、奖品、人员
// 状态是否需要回滚
if (!statusNeedRollback(param)) {
// 不需要:return
return;
}
// 需要回滚: 回滚
rollbackStatus(param);
// 2、回滚中奖者名单
// 是否需要回滚
if (!winnerNeedRollback(param)) {
// 不需要:return
return;
}
// 需要: 回滚
rollbackWinner(param);
}
/**
* 回滚中奖记录:删除奖品下的中奖者
*
* @param param
*/
private void rollbackWinner(DrawPrizeParam param) {
drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
}
private boolean winnerNeedRollback(DrawPrizeParam param) {
// 判断活动中的奖品是否存在中奖者
int count = winningRecordMapper.countByAPId(param.getActivityId(), param.getPrizeId());
return count > 0;
}
/**
* 恢复相关状态
*
* @param param
*/
private void rollbackStatus(DrawPrizeParam param) {
// 涉及状态的恢复,使用 ActivityStatusManager
ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
convertActivityStatusDTO.setActivityId(param.getActivityId());
convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
convertActivityStatusDTO.setPrizeId(param.getPrizeId());
convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.INIT);
convertActivityStatusDTO.setUserIds(
param.getWinnerList().stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList())
);
convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.INIT);
activityStatusManager.rollbackHandlerEvent(convertActivityStatusDTO);
}
private boolean statusNeedRollback(DrawPrizeParam param) {
// 判断活动+奖品+人员表相关状态是否已经扭转(正常思路)
// 扭转状态时,保证了事务一致性,要么都扭转了,要么都没扭转(不包含活动):
// 因此,只用判断人员/奖品是否扭转过,就能判断出状态是否全部扭转
// 不能判断活动是否已经扭转
// 结论:判断奖品状态是否扭转,就能判断出全部状态是否扭转
ActivityPrizeDO activityPrizeDO =
activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
// 已经扭转了,需要回滚
return activityPrizeDO.getStatus()
.equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());
}
/**
* 并发处理抽奖后续流程
*
* @param winningRecordDOList
*/
private void syncExecute(List<WinningRecordDO> winningRecordDOList) {
// 通过线程池 threadPoolTaskExecutor
// 扩展:加入策略模式或者其他设计模式来完成后续的异步操作
// 短信通知
threadPoolTaskExecutor.execute(()->sendMessage(winningRecordDOList));
// 邮件通知
threadPoolTaskExecutor.execute(()->sendMail(winningRecordDOList));
}
/**
* 发邮件
*
* @param winningRecordDOList
*/
private void sendMail(List<WinningRecordDO> winningRecordDOList) {
if (CollectionUtils.isEmpty(winningRecordDOList)) {
logger.info("中奖列表为空,不用发邮件!");
return;
}
for (WinningRecordDO winningRecordDO : winningRecordDOList) {
// Hi,陈。恭喜你在抽奖活动活动中获得二等奖:吹风机。获奖奖时间为18:18:44,请尽快领取您的奖励
String context = "Hi," + winningRecordDO.getWinnerName() + "。恭喜你在"
+ winningRecordDO.getActivityName() + "活动中获得"
+ ActivityPrizeTiersEnum.forName(winningRecordDO.getPrizeTier()).getMessage()
+ ":" + winningRecordDO.getPrizeName() + "。获奖时间为"
+ DateUtil.formatTime(winningRecordDO.getWinningTime()) + ",请尽快领 取您的奖励!";
mailUtil.sendSampleMail(winningRecordDO.getWinnerEmail(),
"中奖通知", context);
}
}
/**
* 发短信
*
* @param winningRecordDOList
*/
private void sendMessage(List<WinningRecordDO> winningRecordDOList) {
if (CollectionUtils.isEmpty(winningRecordDOList)) {
logger.info("中奖列表为空,不用发短信!");
return;
}
for (WinningRecordDO winningRecordDO : winningRecordDOList) {
Map<String, String> map = new HashMap<>();
map.put("name", winningRecordDO.getWinnerName());
map.put("activityName", winningRecordDO.getActivityName());
map.put("prizeTiers", ActivityPrizeTiersEnum.forName(winningRecordDO.getPrizeTier()).getMessage());
map.put("prizeName", winningRecordDO.getPrizeName());
map.put("winningTime", DateUtil.formatTime(winningRecordDO.getWinningTime()));
smsUtil.sendMessage("SMS_476785514",
winningRecordDO.getWinnerPhoneNumber().getValue(),
JacksonUtil.writeValueAsString(map));
}
}
/**
* 状态扭转
* @param param
*/
private void statusConvert(DrawPrizeParam param) {
ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
convertActivityStatusDTO.setActivityId(param.getActivityId());
convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.COMPLETED);
convertActivityStatusDTO.setPrizeId(param.getPrizeId());
convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.COMPLETED);
convertActivityStatusDTO.setUserIds(
param.getWinnerList().stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList())
);
convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.COMPLETED);
activityStatusManager.handlerEvent(convertActivityStatusDTO);
}
}
4.3 请求验证(核对抽奖信息有效性)
Service编写:
public interface DrawPrizeService {
/**
* 校验抽奖请求
* @param param
*/
void checkDrawPrizeParam(DrawPrizeParam param);
}
接口实现:
@Service
public class DrawPrizeServiceImpl implements DrawPrizeService {
private final String WINNING_RECORDS_PREFIX = "WINNING_RECORDS_";
private final Long WINNING_RECORDS_TIMEOUT = 60 * 60 * 24 * 2L;
private final static Logger logger = LoggerFactory.getLogger(DrawPrizeServiceImpl.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ActivityMapper activityMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Autowired
private PrizeMapper prizeMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private WinningRecordMapper winningRecordMapper;
@Autowired
private RedisUtil redisUtil;
@Override
public void checkDrawPrizeParam(DrawPrizeParam param) {
ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId()
,param.getPrizeId());
//活动是否存在
if(null == activityDO||null == activityPrizeDO) {
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY);
}
//活动是否有效
if(activityDO.getStatus()
.equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())){
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_COMPLETED);
}
//奖品是否有效
if(activityPrizeDO.getStatus()
.equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name())){
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED);
}
//中奖者人数和奖品数量是否一致
if(activityPrizeDO.getPrizeAmount()!=param.getWinnerList().size()) {
throw new ServiceException(ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR);
}
}
4.4 状态转换(重点)
(1)活动/奖品/参与者状态转换设计
(2)问题与解决
采用一般写法的话存在以下问题:
解决方案:
代码实现:
public interface ActivityStatusManager {
/**
* 处理活动相关状态扭转
* @param convertActivityStatusDTO
*/
void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO);
/**
* 回滚处理活动相关状态
*
* @param convertActivityStatusDTO
*/
void rollbackHandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO);
}
//策略模式+责任链模式
@Service
public class ActivityStatusManagerImpl implements ActivityStatusManager {
private static final Logger logger = LoggerFactory.getLogger(ActivityStatusManagerImpl.class);
@Autowired
ActivityService activityService;
@Autowired
private final Map<String, AbstractActivityOperator> operatorMap = new HashMap<>();
@Transactional(rollbackFor = Exception.class)
@Override
public void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
if(CollectionUtils.isEmpty(operatorMap)) {
logger.warn("operatorMap为空");
return;
}
Map<String, AbstractActivityOperator> currMap = new HashMap<>(operatorMap);
Boolean update = false;
//先处理人员、奖品
update = processConvertStatus(convertActivityStatusDTO,currMap,1);
//在处理活动
update = processConvertStatus(convertActivityStatusDTO,currMap,2)||update;
//更新缓存
if(update) {
activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
}
}
@Override
public void rollbackHandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
// operatorMap:活动、奖品、人员
// 活动是否需要回滚?? 绝对需要,
// 原因:奖品都恢复成INIT,那么这个活动下的奖品绝对没抽完
for (AbstractActivityOperator operator : operatorMap.values()) {
operator.convert(convertActivityStatusDTO);
}
// 缓存更新
activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
}
/**
* 扭转状态
* @param convertActivityStatusDTO
* @param currMap
* @param sequence
* @return
*/
private Boolean processConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO,
Map<String, AbstractActivityOperator> currMap,
int sequence) {
Boolean update = false;
// 遍历currMap
Iterator<Map.Entry<String, AbstractActivityOperator>> iterator = currMap.entrySet().iterator();
while (iterator.hasNext()) {
AbstractActivityOperator operator = iterator.next().getValue();
// Operator 是否需要转换
if (operator.sequence() != sequence
|| !operator.needConvert(convertActivityStatusDTO)) {
continue;
}
// 需要转换:转换
if (!operator.convert(convertActivityStatusDTO)) {
logger.error("{}状态转换失败!", operator.getClass().getName());
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_STATUS_CONVERT_ERROR);
}
// currMap 删除当前 Operator
iterator.remove();
update = true;
}
// 返回
return update;
}
}
@Component
public abstract class AbstractActivityOperator {
/**
* 控制处理顺序
* @return
*/
public abstract Integer sequence();
/**
* 是否需要转换
* @param convertActivityStatusDTO
* @return
*/
public abstract Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO);
/**
* 转换方法
*
* @param convertActivityStatusDTO
* @return
*/
public abstract Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO);
}
@Component
public class ActivityOperator extends AbstractActivityOperator {
@Autowired
private ActivityMapper activityMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Override
public Integer sequence() {
return 2;
}
@Override
public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
Long activityId = convertActivityStatusDTO.getActivityId();
ActivityStatusEnum targetStatus = convertActivityStatusDTO.getTargetActivityStatus();
if (null == activityId
|| null == targetStatus) {
return false;
}
ActivityDO activityDO = activityMapper.selectById(activityId);
if (null == activityDO) {
return false;
}
// 当前活动状态与传入的状态一致,不处理
if (targetStatus.name().equalsIgnoreCase(activityDO.getStatus())) {
return false;
}
// 需要判断奖品是否全部抽完
// 查询 INIT 状态的奖品个数
int count = activityPrizeMapper.countPrize(activityId, ActivityPrizeStatusEnum.INIT.name());
if (count > 0) {
return false;
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
try {
activityMapper.updateStatus(convertActivityStatusDTO.getActivityId(),
convertActivityStatusDTO.getTargetActivityStatus().name());
return true;
} catch (Exception e) {
return false;
}
}
}
@Component
public class PrizeOperator extends AbstractActivityOperator {
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Override
public Integer sequence() {
return 1;
}
@Override
public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
Long activityId = convertActivityStatusDTO.getActivityId();
Long prizeId = convertActivityStatusDTO.getPrizeId();
ActivityPrizeStatusEnum targetPrizeStatus = convertActivityStatusDTO.getTargetPrizeStatus();
if (null == activityId
|| null == prizeId
|| null == targetPrizeStatus) {
return false;
}
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(activityId, prizeId);
if (null == activityPrizeDO) {
return false;
}
// 判断当前奖品状态和目标状态是否一致
if (targetPrizeStatus.name().equalsIgnoreCase(activityPrizeDO.getStatus())) {
return false;
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
Long activityId = convertActivityStatusDTO.getActivityId();
Long prizeId = convertActivityStatusDTO.getPrizeId();
ActivityPrizeStatusEnum targetPrizeStatus = convertActivityStatusDTO.getTargetPrizeStatus();
try {
activityPrizeMapper.updateStatus(activityId, prizeId, targetPrizeStatus.name());
return true;
} catch (Exception e) {
return false;
}
}
}
@Component
public class UserOperator extends AbstractActivityOperator {
@Autowired
private ActivityUserMapper activityUserMapper;
@Override
public Integer sequence() {
return 1;
}
@Override
public Boolean needConvert(ConvertActivityStatusDTO convertActivityStatusDTO) {
Long activityId = convertActivityStatusDTO.getActivityId();
List<Long> userIds = convertActivityStatusDTO.getUserIds();
ActivityUserStatusEnum targetUserStatus = convertActivityStatusDTO.getTargetUserStatus();
if (null == activityId
|| CollectionUtils.isEmpty(userIds)
|| null == targetUserStatus) {
return false;
}
List<ActivityUserDO> activityUserDOList =
activityUserMapper.batchSelectByAUIds(activityId, userIds);
if (CollectionUtils.isEmpty(activityUserDOList)) {
return false;
}
for (ActivityUserDO auDO : activityUserDOList) {
if (auDO.getStatus()
.equalsIgnoreCase(targetUserStatus.name())) {
return false;
}
}
return true;
}
@Override
public Boolean convert(ConvertActivityStatusDTO convertActivityStatusDTO) {
Long activityId = convertActivityStatusDTO.getActivityId();
List<Long> userIds = convertActivityStatusDTO.getUserIds();
ActivityUserStatusEnum targetUserStatus = convertActivityStatusDTO.getTargetUserStatus();
try {
activityUserMapper.batchUpdateStatus(activityId, userIds, targetUserStatus.name());
return true;
} catch (Exception e) {
return false;
}
}
}
5. 结果记录
5.1 时序图
5.2 Service层接口设计
消息队列消费者的逻辑在上文中已经展示这里只展示Service的代码:
@Service
public class DrawPrizeServiceImpl implements DrawPrizeService {
private final String WINNING_RECORDS_PREFIX = "WINNING_RECORDS_";
private final Long WINNING_RECORDS_TIMEOUT = 60 * 60 * 24 * 2L;
private final static Logger logger = LoggerFactory.getLogger(DrawPrizeServiceImpl.class);
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ActivityMapper activityMapper;
@Autowired
private ActivityPrizeMapper activityPrizeMapper;
@Autowired
private PrizeMapper prizeMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private WinningRecordMapper winningRecordMapper;
@Autowired
private RedisUtil redisUtil;
@Override
public List<WinningRecordDO> saveWinnerRecords(DrawPrizeParam param) {
// 查询相关信息:活动、人员、奖品、活动关联奖品
ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
List<UserDO> userDOList = userMapper.batchSelectByIds(
param.getWinnerList()
.stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList())
);
PrizeDO prizeDO = prizeMapper.selectById(param.getPrizeId());
ActivityPrizeDO activityPrizeDO =
activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
// 构造中奖者记录,保存
List<WinningRecordDO> winningRecordDOList = userDOList.stream()
.map(userDO -> {
WinningRecordDO winningRecordDO = new WinningRecordDO();
winningRecordDO.setActivityId(activityDO.getId());
winningRecordDO.setActivityName(activityDO.getActivityName());
winningRecordDO.setPrizeId(prizeDO.getId());
winningRecordDO.setPrizeName(prizeDO.getName());
winningRecordDO.setPrizeTier(activityPrizeDO.getPrizeTiers());
winningRecordDO.setWinnerId(userDO.getId());
winningRecordDO.setWinnerName(userDO.getUserName());
winningRecordDO.setWinnerEmail(userDO.getEmail());
winningRecordDO.setWinnerPhoneNumber(userDO.getPhoneNumber());
winningRecordDO.setWinningTime(param.getWinningTime());
return winningRecordDO;
}).collect(Collectors.toList());
winningRecordMapper.batchInsert(winningRecordDOList);
// 缓存中奖者记录
// 1、缓存奖品维度中奖记录(WinningRecord_activityId_prizeId, winningRecordDOList(奖品维度的中奖名单))
cacheWinningRecords(param.getActivityId() + "_" + param.getPrizeId(),
winningRecordDOList,
WINNING_RECORDS_TIMEOUT);
// 2、缓存活动维度中奖记录(WinningRecord_activityId, winningRecordDOList(活动维度的中奖名单))
// 当活动已完成再去存放活动维度中奖记录
if (activityDO.getStatus()
.equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())) {
// 查询活动维度的全量中奖记录
List<WinningRecordDO> allList = winningRecordMapper.selectByActivityId(param.getActivityId());
cacheWinningRecords(String.valueOf(param.getActivityId()),
allList,
WINNING_RECORDS_TIMEOUT);
}
return winningRecordDOList;
}
}
6. 邮件服务
pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
application.properties:
## 邮件 ##
spring.mail.host=smtp.qq.com
spring.mail.username=发送者邮箱
# 你的授权码:邮箱设置-》第三⽅服务-》开启IMAP/SMTP服务-》获取授权码
spring.mail.password=你的授权码
spring.mail.default-encoding=UTF-8
邮件工具类编写:
@Component
public class MailUtil {
private static final Logger logger = LoggerFactory.getLogger(MailUtil.class);
@Value(value = "${spring.mail.username}")
private String from;
@Autowired
private JavaMailSender mailSender;
/**
* 发邮件
*
* @param to: 目标邮箱地址
* @param subject: 标题
* @param context: 正文
* @return
*/
public Boolean sendSampleMail(String to, String subject, String context) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(context);
try {
mailSender.send(message);
} catch (Exception e) {
logger.error("向{}发送邮件失败!", to, e);
return false;
}
return true;
}
}
由于本文过长,剩下的查看中奖者名单功能和查看活动详情的功能都较为简单这边不过多赘述。有疑问的可以去博主git仓库翻看源码进行学习。