声明:本文档AI辅助完成,内容仅供参考
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨ 🎯 你正在阅读「Java项目-企悦抽」系列文章 🎯 ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
- 🔌 接口测试从入门到跑路
- ☕ 一个后端的 JavaEE 续命指南
- 🛜 网络原理续命手册
- ☕ Java项目-轻聊
✨ 靠热爱去书写自己,靠勇敢去书写生活! ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🌟 博主简介: 
文章目录:
- 企悦抽-产品需求规格说明书
-
- 修订记录
- 1. 引言
-
- 1.1 编写目的
- 1.2 文档关系
- 2. 系统概述
-
- 2.1 系统名称
- 2.2 技术栈规格
- 2.3 模块划分
- 3. 系统架构规格
-
- 3.1 逻辑架构
- 3.2 抽奖异步链路规格
- 3.3 缓存规格
- 4. 数据规格
-
- 4.1 数据库表
- 4.2 枚举规格
- 4.3 字段校验规格
- 4.4 加密规格
- 5. 功能规格详述
-
- 5.1 FR-001 ~ FR-005 用户与认证
-
- 5.1.1 注册(FR-001)
- 5.1.2 发送验证码(FR-004)
- 5.1.3 密码登录(FR-002)
- 5.1.4 短信登录(FR-003)
- 5.1.5 登录拦截(FR-005)
- 5.2 FR-006 ~ FR-007 人员管理
-
- 5.2.1 创建普通用户(FR-006)
- 5.2.2 用户列表(FR-007)
- 5.3 FR-008 ~ FR-010 奖品管理
-
- 5.3.1 创建奖品(FR-008)
- 5.3.2 奖品分页列表(FR-009)
- 5.3.3 奖品全量列表(FR-010)
- 5.4 FR-011 ~ FR-013 活动管理
-
- 5.4.1 创建活动(FR-011)
- 5.4.2 活动列表(FR-012)
- 5.4.3 活动详情(FR-013)
- 5.5 FR-014 ~ FR-016 抽奖与结果
-
- 5.5.1 提交抽奖(FR-014)
- 5.5.2 查询中奖记录(FR-015)
- 5.5.3 抽奖大屏(FR-016)
- 5.6 FR-017 ~ FR-018 通知
-
- 5.6.1 中奖邮件(FR-017)
- 5.6.2 中奖短信(FR-018)
- 6. 接口规格索引
- 7. 前端页面规格
-
- 7.1 实现说明
- 7.2 页面清单
- 7.3 通用前端约定
- 8. 状态机规格
-
- 8.1 状态扭转顺序
- 8.2 抽奖正向扭转
- 8.3 异常回滚
- 9. 安全规格
- 10. 异常与容错规格
-
- 10.1 MQ 重试
- 10.2 死信队列
- 10.3 校验失败静默返回
- 11. 验收标准
-
- 11.1 功能验收
- 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 缓存规格
| 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
| 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)
处理逻辑:
输出:{ userId }
页面:register.html;管理员后台创建时 admin=false&jumpList=true
5.1.2 发送验证码(FR-004)
输入:Query 参数 phone
处理逻辑:
输出:{ code:200, data:true }
5.1.3 密码登录(FR-002)
输入:loginName(手机/邮箱)、password、mandatoryIdentity(可选)
处理逻辑:
输出:{ token, identity }
5.1.4 短信登录(FR-003)
输入:loginMobile、verificationCode、mandatoryIdentity(可选)
处理逻辑:
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
| 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)
输入:
{
"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)
输入:
{
"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:
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=true
- max-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 | 异常回滚 | 模拟落库异常后状态恢复 |
文档结束,下一文档我们将手动实现数据库的设计。






