一、技术栈
| 层面 | 技术选型 |
|---|---|
| 框架 | Spring Boot 3.2.6 |
| JDK | Java 17 |
| ORM | MyBatis 3.0.3(纯注解方式,无 XML) |
| 数据库 | MySQL 8.x |
| 缓存 | Redis(StringRedisTemplate) |
| 消息队列 | RabbitMQ(Direct 交换机 + 死信队列) |
| 序列化 | Jackson |
| JWT | jjwt 0.11.5(HMAC-SHA) |
| 工具库 | Hutool 5.8.25(AES 加密、SHA256 摘要) |
| 短信 | 阿里云 SMS SDK |
| 邮件 | Spring Boot Starter Mail |
| 参数校验 | spring-boot-starter-validation |
| 前端 | 纯 HTML/CSS/JS(静态页面,直接放 resources/static) |
二、分层架构

三、数据库设计(5张表)
| 表名 | 作用 | 关键字段 |
|---|---|---|
user |
用户表 | user_name, email, phone_number (加密存储), password (SHA256摘要), identity (权限:ADMIN / NORMAL) |
prize |
奖品表 | name, description, price, image_url |
activity |
活动表 | activity_name, description, status (活动状态:RUNNING / COMPLETED) |
activity_prize |
活动-奖品关联表 | activity_id, prize_id, prize_amount (奖品总数量), prize_tiers (奖品等级:一等奖 / 二等奖 / 三等奖), status (关联状态:INIT / COMPLETED) |
activity_user |
活动-用户关联表 | activity_id, user_id, user_name, status (报名/参与状态:INIT / COMPLETED) |
winning_record |
中奖记录表 | activity_id, prize_id, winner_id, winner_name, winning_time... |
四、核心业务流程
1. 用户认证模块(注册与双模式登录)
实现了敏感数据的非对称/对称加密,并采用 JWT 实现无状态分布式身份鉴权
用户注册
输入信息 →参数校验 → 密码摘要化(使用 SHA256 加盐防撞) → 手机号对称加密(使用 AES 算法,确保数据库中不直接明文存储隐私数据) →落库并返回自增主键
userId密码登录
支持邮箱/手机号登录 → 后端提取对应凭证 → 进行 SHA256 比对 (前端明文与数据库密文比对) → 验证通过后,由工具类签发 JWT Token
Token 载荷 :包含
userId和identity(权限标识:ADMIN/NORMAL),设置 1h 严格过期时间短信验证码登录
输入手机号 → 接入阿里云 SMS 网关 发送 6 位验证码 → 同步缓存至 Redis (Key:
SMS_CODE_{phone}, TTL: 60s) → 用户输入验证码比对 → 验证成功后签发 JWT
2.活动创建(事务性)
活动创建涉及多张表的联动变更,属于典型的 写多于读/强一致性 场景
创建活动参数校验(用户存在性/奖品存在性/人数≥奖品数/等奖合法性)
@Transactional 闭环控制
使用 Spring 声明式事务,确保以下四大步骤要么全部成功,要么全部回滚 :
步骤 ① :向 activity 表插入活动主记录,初始化状态为 RUNNING。
步骤 ② :调用 batchInsert 批量向 activity_prize 中间表插入奖品配额,状态为 INIT。
步骤 ③ :调用 batchInsert 批量向 activity_user 关联表导入参与白名单,状态为 INIT。
步骤 ④ :组装完全体的 ActivityDetailDTO 对象,同步回种至 Redis 缓存 (Key: ACTIVITY_{id}, 过期时间 3天),实现后续抽奖流程的缓存就近读取 。
在事务开启前,进行严格的业务前置检查,防止非法请求占用数据库连接资源:
存在性检查:发起创建的用户是否存在、关联的奖品是否存在。
业务防呆:校验活动报名人数上限是否 \\ge 奖品总数。
合规检查:奖品等阶(一/二/三等奖)是否合法。
3.核心抽奖模块(RabbitMQ 异步削峰流量)
抽奖瞬间流量极高,如果直接让请求穿透到 MySQL,会导致数据库瞬间瘫痪。因此系统采用了前台快速响应、后台异步消费的 MQ 削峰设计
流量异步化(Controller 层)
前端发起抽奖 → Controller 快速接收 → Service 层不做复杂交互,直接将请求参数序列化为 JSON 字符串 → 通过
RabbitTemplate投递到 Direct 交换机【此时请求已成功脱离本地线程,Controller 立刻为前端返回"排队中/处理中",用户页面体验极度流畅,响应时间从数百毫秒压缩至数毫秒】
异步消费处理(MqReceiver 核心逻辑)
核心参数二次校验:
校验活动、奖品是否依旧在有效期内。
检查活动是否已完成。
核心防超卖:校验奖品是否已抽完、当前中奖人数是否已达到奖品上限。
MqReceiver监听队列并开始执行真正的抽奖重活状态机扭转
优雅地控制活动状态由
RUNNING→COMPLETED,以及关联表状态的变更三方服务解耦优化
中奖后的短信通知(阿里云)和邮件通知(Spring Mail)属于耗时较长的 IO 操作。
系统将其放入独立的自定义线程池中异步执行,确保不阻塞 MQ 消费主线程
4.中奖记录查询模块(高性能快照读)
用户查询中奖名单 → 优先检索 Redis 缓存 → 若缓存未命中,则穿透至 MySQL 数据库查询;拿到结果后反向写回 Redis ,并设置 2天 相对短的有效期,保证缓存的整体新鲜度
五、状态机模式(设计模式运用)
面对活动、奖品、人员三个维度的复杂状态扭转时,普通的
if-else极难维护
六、死信队列
抽奖数据涉及真实的资产发放,消息如果因异常意外丢失是绝对不可接受的。你的设计中引入了死信交换机来建立兜底机制
七、数据加密
我们使用Hutool工具包加密,引入相关依赖
1. 手机号
→ AES 对称加密(可逆):手机号写入 MySQL 之前,MyBatis 的 TypeHandler 自动用 AES 对称加密转换成密文存储;读取时自动解密回明文------可能会破解密钥,进行解密
♦涉及的表和字段:user 表的 phone_number 字段。
用户注册、短信登录、中奖通知发短信时,都经过 TypeHandler加密写入或读取存储的密文解密
2. 密码
→SHA256 哈希(不可逆):密码不需要还原,只需要验证"用户输入的是不是同一个密码"。用哈希即使数据库被拖库,攻击者也反推不出原始密码。------泄露密文也无法解密
♦彩虹攻击:暴力枚举列出所有字符串的密文,一 一对比,解出原字符串(密码)
♦涉及的表和字段:user 表的 password 字段
3.加盐加密
密文= Hash(明文密码+随机盐)
持久化存储:将"最终密文"和"随机盐"一起存入数据库的用户表
校验:用同样的加密算法,算出密文,对比数据库里的密文
4. JWT
→HMAC-SHA256 签名(不是加密):用这个密钥对 {id, identity, exp} 做签名,签名只是用来防篡改;服务器用密钥验签,确保token 没被改过
♦服务器把token发给客户端,客户端每次会话携带这个token去客户端,客户端拿着密钥校验是不是真实用户
八、接口输入输出一览表
| 接口路径 | 请求方法 | 输入数据 (Request) | 输出数据 (Response) |
|---|---|---|---|
/register |
POST |
JSON: name, mail, phoneNumber, password, identity |
{ "code": 200, "data": { "userId": 1 } } |
/verification-code/send |
GET |
Query: phoneNumber |
{ "code": 200, "data": true } |
/password/login |
POST |
JSON: loginName (邮箱/手机), password, mandatoryIdentity |
{ "code": 200, "data": { "token": "...", "identity": "..." } } |
/message/login |
POST |
JSON: loginMobile, verificationCode, mandatoryIdentity |
{ "code": 200, "data": { "token": "...", "identity": "..." } } |
/base-user/find-list |
GET |
Query: identity (ADMIN / NORMAL) |
{ "code": 200, "data": [{ "userId": 1, "userName": "...", "identity": "..." }] } |
/prize/create |
POST |
Multipart: param (CreatePrizeParam JSON) + prizePic (文件) |
{ "code": 200, "data": prizeId } |
/prize/find-list |
GET |
Query: PageParam (分页参数:page, size) |
{ "code": 200, "data": { "total": 100, "records": [...] } } |
/pic/upload |
POST |
Multipart: MultipartFile (图片文件) |
"http://.../static/image.png" (图片URL字符串) |
/activity/create |
POST |
JSON: activityName, description, activityUserList, activityPrizeList |
{ "code": 200, "data": { "activityId": 1 } } |
/activity/find-list |
GET |
Query: PageParam (分页参数) |
{ "code": 200, "data": { "total": 50, "records": [...] } } |
/activity-detail/find |
GET |
Query: activityId |
{ "code": 200, "data": { "活动基础信息": {}, "奖品列表": [], "人员列表": [] } } |
/draw-prize |
POST |
JSON: activityId, prizeId, winnerList, winningTime |
{ "code": 200, "data": true } |
/winning-records/show |
POST |
JSON: activityId, prizeId (可选) |
{ "code": 200, "data": [{中奖记录1}, {中奖记录2}] } |
用户注册



