项目介绍
随着数字营销的兴起,企业越来越重视通过在线活动来吸引和留住客户。抽奖活动作为⼀种有效的营销手段,能够显著提升用户参与度和品牌曝光率。于是我们就开发了以抽奖活动作为背景的Spring Boot项目,通过这个项目提供⼀个全面、可靠、易于维护的抽奖平台,该平台将采用以下策略:
- 集成多种技术组件:利用MySQL、Redis、RabbitMQ等常用组件,构建一个稳定、高效、可扩展的抽奖系统
- 活动、奖品与人员管理:允许管理人员创建配置抽奖活动,管理奖品信息,管理人员信息
- 实现状态机的管理:通过精心设计的状态机,精确控制活动及奖品状态的转换,提高系统的可控性和客测试性
- 保障数据的一致性:通过事务管理和数据同步机制,确保数据的一致性和完整性
- 加强安全性:实施安全措施,包括数据加密,用户认证,保护用户数据和系统安全
- 降低维护成本:提供全面的日志记录和异常处理机制,简化问题诊断和系统维护
- 提高扩展性:采用模块化设计与设计模式的使用,提高系统的灵活性和扩展性


系统设计
系统架构

项目环境:
- 编程语言:java,javaScript
- 开发语言包:JDK17
- 后端框架:SpringBoot
- 数据库:MySQL
- 缓存:Redis
- 消息队列:RabbitMQ
- 日志:lobback
- 安全:JWT+加密
业务模块功能
- 人员业务模块:管理员注册、登录,以及普通用户的创建
- 活动业务模块:活动管理以及活动状态管理
- 奖品业务模块:奖品管理与奖品的分配,还包括奖品图的上传
- 通知业务模块:发送短信、邮件等业务美丽如验证码的发送,中奖通知
- 抽奖业务模块:完成抽奖动作,以及抽奖后的结果展示
数据库设计
- 用户表:存储用户信息
- 活动表:存储活动信息
- 奖品表:存储奖品信息
- 活动奖品关联表:存储一个活动下关联了哪些奖品
- 活动用户关联表:存储一个活动下设置的参与人员
- 中奖记录表:存储一个活动的中奖名单








安全设计
- 用户登录身份验证:使用JWT进行用户身份验证,需强制用户在某些页面必须进行登录操作
- 加密:敏感信息数据加密,例如手机号、用户密码登敏感数据落库需要加密
项目启动
通用处理
1. 错误码
定义错误码类型

定义全局错误码

定义业务错误码—controller层(随业务代码补充)
public interface ControllerErrorCodeConstants {
}
定义业务错误码—service层(随业务代码补充)
public interface ServiceErrorCodeConstants {
}
2. 自定义异常类
ControllerException:controller 层异常类

ServiceException:service 层异常类

3. CommonResult<T>
CommonResult作为控制器层⽅法的返回类型,封装HTTP接口调用的结果,包括成功数据、错误信息和状态码。它可以被SpringBoot框架等自动转换为JSON或其他格式的响应体,发送给客户端。

4. jackson


5. 日志处理
使用SLF4J+logback
Logback 就是 Java 应用中最常用的高性能日志框架,它实现了 SLF4J,配置简单、功能强大,是生产环境日志管理的首选方案之一
用户模块
1. 注册

约定前后端接口
[请求] /register POST
{
"name":"张三",
"mail":"451@qq.com",
"phoneNumber":"13188888888",
"password":"123456789",
"identity":"ADMIN"
}
[响应]
{
"code": 200,
"data": {
"userId": 22
},
"msg": ""
}
Controller层
实体类


业务代码

service层
实体类


业务代码


校验信息



dao层
实体类



Mapper

TypeHandler




2. 控制层通用异常处理

3. 登录
3.1发送验证码

配置
##短信##
sms.access-key-id=填写自己申请的
sms.access-key-secret=填写自己申请的
sms.sign-name=填写自己申请的
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;
/**
* 发送短信
............
RedisUtil工具类




CaptchaUtil工具类(用来生成验证码)
Hutool提供
约定前后端接口
[请求] /verification-code/send?phoneNumber=13199999999 GET
[响应]
{
"code": 200,
"data": true,
"msg": ""
}
Controller层

Service层



3.2 JWTUtil工具类


3.3管理员登录有两种方式


约定前后端交互接口
密码登录
[请求] /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": ""
}
Controller层
实体类




业务代码


Service层
实体类

接口及其实现




dao层

4. 强制登录
当用户访问非登录注册页面时,例如抽奖页面,如果用户当前尚未登陆,我们希望自动跳转到登陆页面

添加拦截器

注册拦截器

5. 用户管理
人员列表展示

约定前后端交互接口
[请求] /base-user/find-list GET
[响应]
{
"code": 200,
"data": [
{
"userId": 15,
"userName": "郭靖",
"identity": "NORMAL"
},
{
"userId": 14,
"userName": "王五",
"identity": "ADMIN"
}
],
"msg": ""
}
Controller层
实体类

业务代码

Service层
实体类

接口及其实现


Dao层


奖品模块
1. 图片上传
配置路径
## 文件上传#
# 目标路径#
pic.local-path=D:/java/PIC
# springboot3升级配置名#
spring.web.resources.static-locations=classpath:/static/,file:$(pic.local-path}
Controller层

Service层


2. 创建奖品

约定前后端交互接口
[请求] /prize/create POST
param: {"prizeName":"吹⻛机","description":"吹⻛机","price":100}
prizePic: Obj-C.jpg (FILE)
[响应]
{
"code": 200,
"data": 17,
"msg": ""
}
Controller层
实体类


Service层


Dao层


3. 奖品列表展示(翻页)

约定前后端接口
[请求] /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/R
C.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": ""
}
Controller 层



Service层




Dao层



活动模块
1. 活动创建

约定前后端接口
[请求] /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": ""
}
Controller层


Service层
















Dao层






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": ""
}
Contorller层




Service 层




Dao层

抽奖模块
1. 抽奖设计
抽奖过程是 抽奖系统中最重要的核心环节,它需要确保公平、透明且高效
前端:控制抽奖流程,确定中奖人
后端:
- 查询活动完整信息
- 抽奖:保存中奖信息、扭转对应状态、完成通知行为
- 查询中奖列表

1. 参与者注册于奖品创建
- 参与者注册:管理员通过管理端新增用户,填写必要的信息
- 奖品建立:奖品需要提前建立好
2. 抽奖活动设置
- 活动创建:管理员在系统中创建抽奖活动,输入活动名称、描述、奖品列表等信息
- 圈选人员:关联该抽奖活动的参与者
- 圈选奖品:圈选该抽奖活动的奖品,设置奖品等级、个数等
- 活动发布:活动信息发布后,系统通过管理端界面展示活动列表
3. 抽奖请求处理(重要)
- 随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的
- 请求提交:活动进行时,管理员可发起抽奖请求
- 消息队列通知:有效的抽奖请求被发送到MQ队列中,等待MQ消费者真正处理抽奖逻辑
- 请求返回:抽奖的请求处理接口不再完成任何事情,直接返回(时效性)
4. 抽奖结果公布
- 前端展示:中奖名单通过前端随机抽取的人员,公布展示出来
5. 抽奖逻辑的执行(重要)
- 消息消费:MQ消费者收到异步消息,系统开始执行以下抽奖逻辑
6. 中奖结果处理
- 请求验证:系统验证抽奖请求的有效性,如是否满足系统根据设定的规则,幂等性:若消息多发,已抽取的内容不能再次抽取
- 状态扭转:根据中奖结果扭转活动、奖品、参与者状态
- 结果记录:中奖结果被记录在数据库中,并同步更新Redis缓存
7. 中奖者通知
- 通知中奖者:通知中奖者和其他相关系统,需要保证事务的一致性
- 补救措施:抽奖行为是一次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补救措施
技术实现细节
- 异步处理:提高抽奖性能,不影响抽奖流程,将抽奖处理放入队列中进行异步处理,且保证了幂等性
- 活动状态扭转处理:状态扭转会涉及活动及奖品等横向维度扭转 ,不能避免未来不会有其他内容牵扯进活动中,因此对于状态扭转处理,需要提高扩展性(设计模式)与维护性
- 并发处理:中奖者通知,可能要通知多系统,但相互解耦,可以设计为并发处理,加快抽奖效率
- 事务处理:在抽奖逻辑执行时,如若发生异常,需要保证数据库原子性,事务一致性,因此要做好事务处理
查询活动完整信息



抽奖

2. RabbitMQ
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
..............


3. 抽奖请求处理

约定前后端接口
[请求] /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": ""
}
Controller层


Service层


4. MQ异步抽奖逻辑执行


4.1 消费MQ消息
消费者类MqReceiver实现

4.2 请求验证(核对抽奖信息有效性)





4.3 状态转换

新增转换方法


新增策略

ActivityOperator


PrizeOperator


UserOperator


责任链实现
注意:Map
类被 Spring 管理,且 operatorMap 上标注了 @Autowired Spring 容器在初始化 Bean 时,会自动将特定接口或抽象类的所有实现类注入到这个 Map 中


4.4 结果记录






4.5 中奖者通知
1. 邮件服务



2.短信服务
阿里云提供的服务

Hi,${name}。恭喜你在${activityName}活动中获得${prizeTiers},奖品为
${prizeName}。获奖时间为${winningTime},请尽快领取您的奖励!
3. 配置线程池

4. 并发通知



4.6 事务一致性-异常回滚
在业务的返回场景中,MQ消费过程里,不仅仅修改了数据库表的内容,还想redis缓存中新增了很多热点数据。例如扭转了奖品及活动的状态,还将中奖名单落入库中。在这个过程中,一旦出现异常,必须要保证该事务的特性

在proccess中,抛出异常前先catch,运行rollback方法,再抛出
抛出异常的核心目的:触发消息重试(Nack)
在 Spring-Rabbit 中,监听器方法的执行结果决定了消息的命运:
-
方法正常返回(无异常):RabbitMQ 自动发送
Ack确认,消息从队列中移除。 -
方法抛出异常(且未捕获):RabbitMQ 自动发送
Nack(否定确认),消息会重新入队(等待下次消费,直到重试次数耗尽)。
是为了告诉 MQ:“这条消息我处理失败了,请稍后重试”




ManageService层


DrawPrizeService层


WinningRecordMapper

4.7 保证消费消息成功(加入死信队列)
虽然已经保证了事务的一致性,但目前未能保证该消息被成功消费,因此加入死信队列


消费死信队列

5. 中奖名单

约定前后端接口
[请求] /winning-records/show POST
{
"activityId":23
}
[响应]
{
"code": 200,
"data": [
{
"winnerId": 15,
"winnerName": "胡⼀博",
"prizeName": "华为⼿机",
"prizeTier": "⼀等奖",
"winningTime": "2024-05-21T11:55:10.000+00:00"
},
{
"winnerId": 21,
"winnerName": "范闲",
"prizeName": "华为⼿机",
"prizeTier": "⼀等奖",
"winningTime": "2024-05-21T11:55:10.000+00:00"
}
],
"msg": ""
}
controller层
实体类



Service层



Dao层


项目成功部署
由于阿里云短信申请限制,无法成功发送验证码,其余均可正常运行






4万+

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



