声明:本文档AI辅助完成,内容仅供参考
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨🎯 你正在阅读「Java项目-企悦抽」系列文章 🎯
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
- 🔌 接口测试从入门到跑路
- ☕ 一个后端的 JavaEE 续命指南
- 🛜 网络原理续命手册
- ☕ Java项目-轻聊
✨ 靠热爱去书写自己,靠勇敢去书写生活!
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🌟 博主简介:

文章目录:
- 企悦抽-产品需求规格说明书
-
- 修订记录
- [1. 引言](#1. 引言)
-
- [1.1 编写目的](#1.1 编写目的)
- [1.2 文档关系](#1.2 文档关系)
- [2. 系统概述](#2. 系统概述)
-
- [2.1 系统名称](#2.1 系统名称)
- [2.2 技术栈规格](#2.2 技术栈规格)
- [2.3 模块划分](#2.3 模块划分)
- [3. 系统架构规格](#3. 系统架构规格)
-
- [3.1 逻辑架构](#3.1 逻辑架构)
- [3.2 抽奖异步链路规格](#3.2 抽奖异步链路规格)
- [3.3 缓存规格](#3.3 缓存规格)
- [4. 数据规格](#4. 数据规格)
-
- [4.1 数据库表](#4.1 数据库表)
- [4.2 枚举规格](#4.2 枚举规格)
- [4.3 字段校验规格](#4.3 字段校验规格)
- [4.4 加密规格](#4.4 加密规格)
- [5. 功能规格详述](#5. 功能规格详述)
-
- [5.1 FR-001 ~ FR-005 用户与认证](#5.1 FR-001 ~ FR-005 用户与认证)
-
- [5.1.1 注册(FR-001)](#5.1.1 注册(FR-001))
- [5.1.2 发送验证码(FR-004)](#5.1.2 发送验证码(FR-004))
- [5.1.3 密码登录(FR-002)](#5.1.3 密码登录(FR-002))
- [5.1.4 短信登录(FR-003)](#5.1.4 短信登录(FR-003))
- [5.1.5 登录拦截(FR-005)](#5.1.5 登录拦截(FR-005))
- [5.2 FR-006 ~ FR-007 人员管理](#5.2 FR-006 ~ FR-007 人员管理)
-
- [5.2.1 创建普通用户(FR-006)](#5.2.1 创建普通用户(FR-006))
- [5.2.2 用户列表(FR-007)](#5.2.2 用户列表(FR-007))
- [5.3 FR-008 ~ FR-010 奖品管理](#5.3 FR-008 ~ FR-010 奖品管理)
-
- [5.3.1 创建奖品(FR-008)](#5.3.1 创建奖品(FR-008))
- [5.3.2 奖品分页列表(FR-009)](#5.3.2 奖品分页列表(FR-009))
- [5.3.3 奖品全量列表(FR-010)](#5.3.3 奖品全量列表(FR-010))
- [5.4 FR-011 ~ FR-013 活动管理](#5.4 FR-011 ~ FR-013 活动管理)
-
- [5.4.1 创建活动(FR-011)](#5.4.1 创建活动(FR-011))
- [5.4.2 活动列表(FR-012)](#5.4.2 活动列表(FR-012))
- [5.4.3 活动详情(FR-013)](#5.4.3 活动详情(FR-013))
- [5.5 FR-014 ~ FR-016 抽奖与结果](#5.5 FR-014 ~ FR-016 抽奖与结果)
-
- [5.5.1 提交抽奖(FR-014)](#5.5.1 提交抽奖(FR-014))
- [5.5.2 查询中奖记录(FR-015)](#5.5.2 查询中奖记录(FR-015))
- [5.5.3 抽奖大屏(FR-016)](#5.5.3 抽奖大屏(FR-016))
- [5.6 FR-017 ~ FR-018 通知](#5.6 FR-017 ~ FR-018 通知)
-
- [5.6.1 中奖邮件(FR-017)](#5.6.1 中奖邮件(FR-017))
- [5.6.2 中奖短信(FR-018)](#5.6.2 中奖短信(FR-018))
- [6. 接口规格索引](#6. 接口规格索引)
- [7. 前端页面规格](#7. 前端页面规格)
-
- [7.1 实现说明](#7.1 实现说明)
- [7.2 页面清单](#7.2 页面清单)
- [7.3 通用前端约定](#7.3 通用前端约定)
- [8. 状态机规格](#8. 状态机规格)
-
- [8.1 状态扭转顺序](#8.1 状态扭转顺序)
- [8.2 抽奖正向扭转](#8.2 抽奖正向扭转)
- [8.3 异常回滚](#8.3 异常回滚)
- [9. 安全规格](#9. 安全规格)
- [10. 异常与容错规格](#10. 异常与容错规格)
-
- [10.1 MQ 重试](#10.1 MQ 重试)
- [10.2 死信队列](#10.2 死信队列)
- [10.3 校验失败静默返回](#10.3 校验失败静默返回)
- [11. 验收标准](#11. 验收标准)
-
- [11.1 功能验收](#11.1 功能验收)
- [11.2 非功能验收](#11.2 非功能验收)
企悦抽-产品需求规格说明书
| 文档属性 | 内容 |
|---|---|
| 产品名称 | 企悦抽(QiYueChou) |
| 文档类型 | 产品需求规格说明书(PRS) |
| 文档版本 | V1.0 |
| 编写日期 | 2025-07-03 |
| 上游文档 | AI赋能产品需求文档 |
| 关联代码 | lottery-system |
| 文档状态 | 已定稿 |
修订记录
| 版本 | 日期 | 修订说明 |
|---|---|---|
| V1.0 | 2025-07-03 | 首版,由 PRD 拆解为可开发/可测试规格 |
1. 引言
1.1 编写目的
本文档在 PRD 基础上,将产品需求转化为可实施、可验证的功能规格、数据规格、状态规则与验收标准,供研发与测试直接使用。
1.2 文档关系
PRD(为什么做、做什么)
↓
PRS(怎么做、做到什么标准) ← 本文档
↓
接口设计文档(接口级定义)
↓
接口测试需求文档(测试用例输入)
2. 系统概述
2.1 系统名称
- 产品名:企悦抽
- 工程名:
lottery-system - 默认端口(开发用):
8080
2.2 技术栈规格
| 层次 | 技术 | 版本/说明 |
|---|---|---|
| 后端框架 | Spring Boot | 3.2.6 |
| 语言 | Java | 17 |
| ORM | MyBatis | 3.0.3(注解方式) |
| 数据库 | MySQL | 库名 lottery_system |
| 缓存 | Redis | Lettuce,默认端口配置 8888 |
| 消息队列 | RabbitMQ | Direct 交换机 + 死信队列 |
| 认证 | JWT | jjwt 0.11.5,Header: user_token |
| 前端 | HTML + jQuery + CSS | 静态资源 /static;UI 借助 AI 辅助实现,主打美观效果 |
| 日志 | SLF4J + Logback | logback-spring.xml |
| 短信 | Spug 推送 | SMSUtil HTTP 调用 |
| 邮件 | Spring Mail | QQ SMTP |
2.3 模块划分
| 模块 | 包路径/组件 | 职责 |
|---|---|---|
| 用户模块 | UserController、UserServiceImpl |
注册、登录、用户列表 |
| 验证码模块 | VerificationCodeServiceImpl |
验证码生成、Redis 缓存、Spug 发送 |
| 奖品模块 | PrizeController、PrizeServiceImpl |
奖品 CRUD、图片上传 |
| 活动模块 | ActivityController、ActivityServiceImpl |
活动创建、列表、详情、Redis 缓存 |
| 抽奖模块 | DrawPrizeController、DrawPrizeServiceImpl |
发 MQ、校验、落库、查记录 |
| MQ 消费 | MqReceiver、DlxReceiver |
异步抽奖、回滚、死信重投 |
| 状态管理 | ActivityStatusManagerImpl + Operator |
活动/奖品/用户状态扭转 |
| 通用 | LoginInterceptor、GlobalException |
鉴权、统一响应 |
3. 系统架构规格
3.1 逻辑架构

3.2 抽奖异步链路规格
| 步骤 | 组件 | 行为 |
|---|---|---|
| 1 | DrawPrizeController |
接收 JSON,调用 drawPrize() |
| 2 | DrawPrizeServiceImpl.drawPrize |
构造 {messageId, messageData} 发 MQ |
| 3 | Controller | 立即返回 {code:200, data:true} |
| 4 | MqReceiver.process |
消费消息 |
| 5 | --- | checkDrawPrizeParam 校验 |
| 6 | --- | ActivityStatusManager.handlerEvent 扭转状态 |
| 7 | --- | saveWinnerRecords 写库 + 缓存 |
| 8 | --- | 线程池异步发邮件(短信占位) |
| 9 | 异常 | 回滚 + 抛异常 → MQ 重试 → 死信 |
3.3 缓存规格
| 键前缀 | 内容 | TTL |
|---|---|---|
VERIFICATION_CODE_{phone} |
4 位验证码 | 60 秒 |
ACTIVITY_{activityId} |
活动详情 JSON | 3 天 |
WINNING_RECORDS_{activityId} |
活动全量中奖记录 | 2 天 |
WINNING_RECORDS_{activityId}_{prizeId} |
单奖品中奖记录 | 2 天 |
4. 数据规格
4.1 数据库表
| 表名 | 说明 | 关键约束 |
|---|---|---|
user |
用户 | 邮箱唯一、手机号唯一 |
prize |
奖品库 | --- |
activity |
活动 | status: RUNNING/COMPLETED |
activity_prize |
活动-奖品 | uk(activity_id, prize_id) |
activity_user |
活动-用户 | uk(activity_id, user_id) |
winning_record |
中奖记录 | uk(winner_id, activity_id, prize_id) |
4.2 枚举规格
用户身份 UserIdentityEnum
| 值 | 含义 |
|---|---|
| ADMIN | 管理员 |
| NORMAL | 普通用户 |
活动状态 ActivityStatusEnum
| 值 | 含义 |
|---|---|
| RUNNING | 进行中 |
| COMPLETED | 已完成 |
活动奖品状态 ActivityPrizeStatusEnum
| 值 | 含义 |
|---|---|
| INIT | 未抽取 |
| COMPLETED | 已抽取 |
活动用户状态 ActivityUserStatusEnum
| 值 | 含义 |
|---|---|
| INIT | 未中奖 |
| COMPLETED | 已中奖 |
奖品等级 ActivityPrizeTiersEnum
| 值 | code | 中文 |
|---|---|---|
| FIRST_PRIZE | 1 | 一等奖 |
| SECOND_PRIZE | 2 | 二等奖 |
| THIRD_PRIZE | 3 | 三等奖 |
4.3 字段校验规格
| 字段 | 规则 | 实现 |
|---|---|---|
| 邮箱 | 小写邮箱格式 | RegexUtil.checkMail |
| 手机号 | 1 开头 11 位 | RegexUtil.checkMobile |
| 密码 | 6~12 位字母数字 | RegexUtil.checkPassword |
| 管理员密码 | 必填 | 注册/创建时校验 |
4.4 加密规格
| 数据 | 算法 | 说明 |
|---|---|---|
| 密码 | SHA256 Hex | 注册/登录时 DigestUtil.sha256Hex |
| 手机号 | AES | EncryptTypeHandler,密钥 16 字节 |
5. 功能规格详述
5.1 FR-001 ~ FR-005 用户与认证
5.1.1 注册(FR-001)
输入 :name、mail、phoneNumber、password(管理员必填)、identity(ADMIN/NORMAL)
处理逻辑:
- JSR303 非空校验
- 邮箱/手机号格式校验
- 身份合法性校验
- 管理员密码必填且强度校验
- 邮箱/手机号唯一性校验
- 密码 SHA256 后入库;手机号 AES 加密入库
输出 :{ userId }
页面 :register.html;管理员后台创建时 admin=false&jumpList=true
5.1.2 发送验证码(FR-004)
输入 :Query 参数 phone
处理逻辑:
- 手机号格式校验
MyCaptchaUtil.getCaptcha(4)生成 4 位数字SMSUtil.sendSms(phone, code)调用 Spug- Redis 存储,键
VERIFICATION_CODE_{phone},60 秒
输出 :{ code:200, data:true }
5.1.3 密码登录(FR-002)
输入 :loginName(手机/邮箱)、password、mandatoryIdentity(可选)
处理逻辑:
- 按格式查用户(手机需 Encrypt 包装)
- 校验用户存在、身份匹配、密码 SHA256 比对
- JWT Claims:
{ id, identity },有效期 1 小时
输出 :{ token, identity }
5.1.4 短信登录(FR-003)
输入 :loginMobile、verificationCode、mandatoryIdentity(可选)
处理逻辑:
- 查用户、校验身份
- 从 Redis 取验证码比对
- 签发 JWT
5.1.5 登录拦截(FR-005)
白名单(无需 Token):
- 静态资源
/**/*.html、/css/**、/js/**、/pic/**等 /**/login(含/password/login、/message/login)/register/verification-code/send/winning-records/show
鉴权失败:HTTP 401,无 JSON Body
5.2 FR-006 ~ FR-007 人员管理
5.2.1 创建普通用户(FR-006)
与注册共用 /register,identity=NORMAL,不传密码。
5.2.2 用户列表(FR-007)
输入 :Query identity(可选,ADMIN/NORMAL,空则全部)
输出 :[{ userId, userName, identity }]
5.3 FR-008 ~ FR-010 奖品管理
5.3.1 创建奖品(FR-008)
Content-Type :multipart/form-data
| Part 名 | 类型 | 说明 |
|---|---|---|
| param | JSON 字符串 | { prizeName, description, price } |
| prizePic | File | 奖品图片,最大 10MB |
处理 :图片存本地 pic.local-path,文件名 UUID+后缀;imageUrl 存文件名
输出:奖品 ID(Long)
5.3.2 奖品分页列表(FR-009)
输入 :currentPage(默认 1)、pageSize(默认 10)
输出 :{ total, records:[{ prizeId, prizeName, description, price, imageUrl }] }
5.3.3 奖品全量列表(FR-010)
输出 :[{ prizeId, prizeName }]
5.4 FR-011 ~ FR-013 活动管理
5.4.1 创建活动(FR-011)
输入:
json
{
"activityName": "string, 必填",
"description": "string, 必填",
"activityPrizeList": [
{ "prizeId": "long", "prizeAmount": "long", "prizeTiers": "FIRST_PRIZE|SECOND_PRIZE|THIRD_PRIZE" }
],
"activityUserList": [
{ "userId": "long", "userName": "string" }
]
}
校验:
- 奖品 ID、用户 ID 均存在于主表
- 参与人数 ≥ 奖品总数
- 奖品等级合法
事务:写 activity + activity_prize + activity_user,初始状态分别为 RUNNING、INIT、INIT
缓存 :组装 ActivityDetailDTO 写入 Redis
输出 :{ activityId }
5.4.2 活动列表(FR-012)
输出字段 valid :status == RUNNING 为 true
前端行为:
- valid=true → 「活动进行中,去抽奖」
- valid=false → 「活动已完成,查看中奖名单」
5.4.3 活动详情(FR-013)
输入 :Query activityId
输出:
| 字段 | 说明 |
|---|---|
| valid | 活动是否进行中 |
| prizes\[\].valid | 奖品是否未抽取(INIT=true) |
| prizes\[\].prizeTierName | 等级中文名 |
| prizes | 按等级 code 升序排列 |
| users\[\].valid | 用户是否未中奖(INIT=true) |
读取顺序:Redis → MySQL 多表组装 → 回写 Redis
5.5 FR-014 ~ FR-016 抽奖与结果
5.5.1 提交抽奖(FR-014)
输入:
json
{
"activityId": "long, 必填",
"prizeId": "long, 必填",
"winningTime": "Date, 必填",
"winnerList": [
{ "userId": "long, 必填", "userName": "string, 必填" }
]
}
同步行为:仅发送 RabbitMQ 消息,不做业务落库
MQ 消费校验(失败则 return 或回滚):
- 活动、活动奖品存在
- 活动非 COMPLETED
- 奖品非 COMPLETED
winnerList.size() == prizeAmount
5.5.2 查询中奖记录(FR-015)
输入 :activityId(必填)、prizeId(可选)
逻辑:
- 无 prizeId:查活动维度全量
- 有 prizeId:查该奖品维度
读取:Redis → MySQL → 回写 Redis
免登录:在白名单中
5.5.3 抽奖大屏(FR-016)
URL 参数 :activityId、activityName、valid(true/false)
权限:
- valid=true 且 localStorage
user_identity=ADMIN:可抽奖 - valid=true 且非 ADMIN:弹窗提示,不可操作
- valid=false:直接展示全量中奖名单
刷新恢复 :已抽完奖品(valid=false)点击开始 → 直接查 /winning-records/show?prizeId= 展示名单
分享 :复制链接,附加 valid=false&hideButton=true,隐藏操作按钮
5.6 FR-017 ~ FR-018 通知
5.6.1 中奖邮件(FR-017)
触发:MQ 消费成功后,线程池异步执行
模板(实际代码):
Hi,{winnerName}!
有个好消息要告诉你:你在【{activityName}】活动中,获得了{prizeTier中文}的惊喜福利:{prizeName}!
福利发放时间:{HH:mm:ss},快来领取你的专属惊喜吧~
收件人:中奖用户邮箱
5.6.2 中奖短信(FR-018)
状态 :V1.0 未实现,MqReceiver.sendMessage 仅打印日志
6. 接口规格索引
| 序号 | 方法 | 路径 | 鉴权 | 详细定义 |
|---|---|---|---|---|
| 1 | POST | /register | 否 | 见接口设计文档 §4.1 |
| 2 | GET | /verification-code/send | 否 | 见接口设计文档 §4.2 |
| 3 | * | /password/login | 否 | 见接口设计文档 §4.3 |
| 4 | * | /message/login | 否 | 见接口设计文档 §4.4 |
| 5 | GET | /base-user/find-list | 是 | 见接口设计文档 §4.5 |
| 6 | * | /pic/upload | 是 | 见接口设计文档 §5.1 |
| 7 | * | /prize/create | 是 | 见接口设计文档 §5.2 |
| 8 | GET | /prize/find-list | 是 | 见接口设计文档 §5.3 |
| 9 | GET | /prize/find-listAll | 是 | 见接口设计文档 §5.4 |
| 10 | * | /activity/create | 是 | 见接口设计文档 §6.1 |
| 11 | GET | /activity/find-list | 是 | 见接口设计文档 §6.2 |
| 12 | GET | /activity-detail/find | 是 | 见接口设计文档 §6.3 |
| 13 | * | /draw-prize | 是 | 见接口设计文档 §7.1 |
| 14 | * | /winning-records/show | 否 | 见接口设计文档 §7.2 |
注:标注
*的接口 Controller 使用@RequestMapping,未限定 HTTP 方法;前端实际使用 POST(JSON)或 GET(Query)。
7. 前端页面规格
7.1 实现说明
本项目 Web 前端(src/main/resources/static)在页面结构、样式与部分交互实现上 借助 AI 辅助完成 ,设计取向为 美观、现代化视觉与现场展示效果,而非传统 B 端「功能优先、样式从简」风格。典型体现包括:
- 登录/注册页:分栏布局、渐变背景与表单视觉优化
- 管理后台:iframe 导航 + 统一卡片/表格样式
- 抽奖大屏(
draw.html):全屏背景、奖品展示、人名滚动动效、中奖确认交互
规格边界 :本文档对前端的约束以 页面路由、功能行为、Token 传递、接口调用 为准;颜色、间距、动效细节以当前 AI 辅助产出为基线,不作为像素级设计稿验收项。后端 REST 接口规格不受 UI 实现方式影响。
7.2 页面清单
| 页面 | 路径 | 功能 |
|---|---|---|
| 后台登录 | /blogin.html |
密码/验证码 Tab 登录 |
| 管理后台 | /admin.html |
iframe 框架 |
| 注册用户 | /register.html |
管理员/普通用户注册 |
| 人员列表 | /user-list.html |
用户列表 |
| 创建奖品 | /create-prizes.html |
表单 + 图片 |
| 奖品列表 | /prizes-list.html |
分页 |
| 创建活动 | /create-activity.html |
圈选奖品/人员 |
| 活动列表 | /activities-list.html |
分页 + 跳转 draw |
| 抽奖大屏 | /draw.html |
核心抽奖交互 |
7.3 通用前端约定
Token 存储 :localStorage.user_token、localStorage.user_identity
请求头 :user_token: {JWT}
8. 状态机规格
8.1 状态扭转顺序
ActivityStatusManagerImpl.handlerEvent:
- Sequence 1 :
PrizeOperator、UserOperator(并行遍历,先奖品/用户) - Sequence 2 :
ActivityOperator(全部奖品 COMPLETED 后才转活动)
8.2 抽奖正向扭转
| 对象 | 原状态 | 目标状态 |
|---|---|---|
| activity_prize | INIT | COMPLETED |
| activity_user(中奖者) | INIT | COMPLETED |
| activity | RUNNING | COMPLETED(全部奖品抽完后) |
8.3 异常回滚
| 对象 | 回滚目标 |
|---|---|
| activity_prize | INIT |
| activity_user | INIT |
| activity | RUNNING |
| winning_record | 删除对应记录 + 清缓存 |
9. 安全规格
| 编号 | 规格 |
|---|---|
| SEC-01 | JWT Header 名称固定为 user_token |
| SEC-02 | Token 解析失败返回 HTTP 401 |
| SEC-03 | 业务异常统一 { code:500, msg:业务消息 } |
| SEC-04 | 图片上传限制 10MB |
| SEC-05 | 手机号不得明文落库 |
10. 异常与容错规格
10.1 MQ 重试
spring.rabbitmq.listener.simple.retry.enabled=truemax-attempts=5
10.2 死信队列
- 正常队列:
DirectQueue→ 死信交换机DlxDirectExchange - 死信消费者
DlxReceiver重新投递到正常队列
10.3 校验失败静默返回
checkDrawPrizeParam 返回 false 时,MqReceiver 直接 return,不抛异常(消息被 ACK)
11. 验收标准
11.1 功能验收
| 编号 | 验收项 | 通过标准 |
|---|---|---|
| AC-01 | 管理员注册登录 | 可进入 admin.html |
| AC-02 | 创建普通用户 | 列表可见 NORMAL 用户 |
| AC-03 | 创建奖品 | 列表展示图片与信息 |
| AC-04 | 创建活动 | 返回 activityId,详情正确 |
| AC-05 | 完整抽奖流程 | 每轮落库,最终活动 COMPLETED |
| AC-06 | 刷新恢复 | 已抽奖品不重复抽 |
| AC-07 | 分享链接 | 仅展示结果,无操作按钮 |
| AC-08 | 邮件通知 | 中奖者收到邮件 |
| AC-09 | 未登录拦截 | 受保护接口返回 401 |
11.2 非功能验收
| 编号 | 验收项 | 通过标准 |
|---|---|---|
| AC-N01 | 抽奖接口响应 | 500ms 内返回(P95) |
| AC-N02 | 缓存命中 | 二次查活动详情走 Redis |
| AC-N03 | 异常回滚 | 模拟落库异常后状态恢复 |
文档结束,下一文档我们将手动实现数据库的设计。