新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Boot+Vue抽签系统:可审计、可配置、高并发的业务实现

发布时间:2026/9/14 9:20:12来源:尧图网络
Spring Boot+Vue抽签系统:可审计、可配置、高并发的业务实现
简介这是一套面向Java与前端初学者的全栈抽签系统实战项目适用于教学演示、活动抽奖开发或Spring BootVue技术栈入门学习。资源完整覆盖后端API、前端交互与数据库设计解决随机抽取、名单管理、结果展示等典型业务场景。压缩包共69个文件含34个Java后端逻辑与配置类、11个Vue组件及业务JS脚本、7个XML配置与Mapper映射文件、5个CSS样式及3个HTML页面另含SQL建表语句、图片与字体资源整体仅1.24MB轻量易部署。已有163人学习下载开发者可直接运行调试深入理解RESTful接口设计、Vue组件通信、JPA数据库操作及前后端分离架构实践尤其适合对照源码掌握抽签业务状态流转与权限控制实现细节。1. 抽签不是随机数生成器而是状态可追溯、规则可配置、结果可审计的业务系统很多人第一反应是“抽签不就是 Math.random() 吗”但真实业务场景里一次抽奖活动可能涉及百人名单导入、多轮次分组抽取、中奖资格校验、结果导出留痕、后台权限分级甚至要对接短信通知或微信模板消息。用 Spring Boot Vue 实现的抽签系统本质是一个轻量级但结构完整的业务中台后端负责数据持久化、事务控制与接口契约前端承担交互逻辑、状态管理与可视化呈现数据库则承载名单、规则、历史记录三类核心实体。它不是玩具 Demo而是能直接用于部门团建、教学分组、评优初筛、会议座次分配等低频但高确定性要求的场景。适合刚掌握 Spring Boot 基础 CRUD 和 Vue 组件通信的开发者通过这个项目打通前后端分离开发全流程——从 MySQL 表设计、MyBatis-Plus 动态查询、Vue Router 路由守卫到 Axios 请求拦截、Element Plus 表单验证、ECharts 中奖分布图渲染。标题里的“源码数据库”不是噱头而是指包含完整 DDL 脚本、预置测试数据、可一键启动的工程结构。2. 用 Spring Boot 构建可扩展的抽签业务模型从实体定义到事务边界控制2.1 核心数据库表设计与字段语义约束抽签系统的数据模型必须支撑“名单可回溯、规则可复用、结果可重演”三大能力。不能只建一张draw_record表存结果而应拆分为三张主表并建立明确外键关系-- 抽签活动主表每次发起一个新抽签任务 CREATE TABLE draw_activity ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 活动名称如2024年度优秀员工初选, status TINYINT DEFAULT 1 COMMENT 状态1-待开始2-进行中3-已结束4-已取消, rule_id BIGINT NOT NULL COMMENT 关联抽签规则ID, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 抽签规则表支持复用如不重复抽取、按部门分组抽取 CREATE TABLE draw_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 规则名称, type VARCHAR(20) NOT NULL COMMENT 规则类型SINGLE单次、MULTI多轮、GROUP分组, is_repeatable BOOLEAN DEFAULT FALSE COMMENT 是否允许重复中奖, max_count INT DEFAULT 0 COMMENT 最大中奖人数0表示不限, description TEXT COMMENT 规则说明, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 参与者名单表支持批量导入含状态标记 CREATE TABLE draw_participant ( id BIGINT PRIMARY KEY AUTO_INCREMENT, activity_id BIGINT NOT NULL COMMENT 所属活动ID, name VARCHAR(50) NOT NULL, code VARCHAR(30) COMMENT 唯一编码如工号/学号, department VARCHAR(50) COMMENT 所属部门/班级, status TINYINT DEFAULT 1 COMMENT 状态1-有效2-已剔除3-已中奖, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_activity_status (activity_id, status) );提示draw_participant.status字段是关键设计点。它避免了物理删除导致历史不可查也规避了“已中奖者再次被抽中”的逻辑漏洞。实际抽取时SQL 查询需显式加上WHERE status 1且更新中奖状态必须用UPDATE ... SET status 3 WHERE id ? AND status 1防止并发重复更新。2.2 Spring Boot 层级划分与关键注解使用项目采用标准分层controller → service → mapper → entity。重点在于 Service 层对事务和并发的处理Service public class DrawService { Autowired private DrawActivityMapper activityMapper; Autowired private DrawParticipantMapper participantMapper; // Transactional 是必须的确保“查可用名单→随机取→更新状态→写结果”原子性 Transactional(rollbackFor Exception.class) public DrawResultDTO executeDraw(Long activityId) { // 1. 校验活动状态 DrawActivity activity activityMapper.selectById(activityId); if (!Objects.equals(activity.getStatus(), 2)) { throw new BusinessException(活动未开始或已结束); } // 2. 获取当前可用参与者status1 ListDrawParticipant candidates participantMapper.selectList( new QueryWrapperDrawParticipant() .eq(activity_id, activityId) .eq(status, 1) ); if (candidates.isEmpty()) { throw new BusinessException(无可抽取人员); } // 3. 随机抽取注意不是 Math.random()而是 SecureRandom 更安全 SecureRandom random new SecureRandom(); int index random.nextInt(candidates.size()); DrawParticipant winner candidates.get(index); // 4. 更新中奖状态带乐观锁条件 int updated participantMapper.update( new DrawParticipant().setStatus((byte) 3), new UpdateWrapperDrawParticipant() .eq(id, winner.getId()) .eq(status, 1) // 确保只更新未中奖状态 ); if (updated 0) { throw new BusinessException(并发冲突该人员已被他人抽中); } // 5. 记录抽取结果可选写入 draw_result 表 DrawResult result new DrawResult(); result.setActivityId(activityId); result.setParticipantId(winner.getId()); result.setWinnerName(winner.getName()); result.setCreatedAt(new Date()); drawResultMapper.insert(result); return new DrawResultDTO(winner.getName(), winner.getCode()); } }2.2.1 为什么用SecureRandom而非Math.random()Math.random()基于线性同余生成器LCG周期短、可预测不适合业务场景SecureRandom使用操作系统熵池Linux 下为/dev/urandom满足密码学安全强度。Spring Boot 默认已集成无需额外依赖。2.2.2 并发控制的两种实现路径对比方式实现要点适用场景缺点乐观锁推荐在UPDATE语句中加WHERE status 1条件检查影响行数中奖率低、并发量中等100 QPS高并发下失败重试次数增加Redis 分布式锁SET draw:lock:{activityId} 1 EX 10 NX Lua 脚本释放活动热度极高、需强一致性引入 Redis 依赖运维成本上升本项目默认采用乐观锁因其零依赖、易理解、符合 ACID 原则。若后续需支持万人级团建抽签再平滑升级为 Redis 锁。3. Vue 前端实现动态交互与状态驱动从名单上传到结果可视化3.1 基于 Element Plus 的抽签流程组件化拆分整个抽签界面不应是一个巨型单文件组件而应拆分为可复用、可测试的子模块DrawUpload.vue支持 Excel 文件拖拽上传、解析预览、错误行高亮DrawConfig.vue规则选择器单抽/多轮/分组、中奖人数输入、部门筛选下拉DrawPanel.vue实时显示剩余人数、已抽中名单、倒计时按钮防误触DrawResultChart.vueECharts 渲染中奖部门分布饼图需后端提供/api/draw/result/statistics接口每个组件通过v-model或defineEmits与父组件DrawPage.vue通信避免状态污染。3.2 Excel 名单解析与后端校验协同策略前端上传 Excel 时不能仅做格式校验如列名是否为“姓名、工号、部门”必须与后端二次校验联动// DrawUpload.vue 中的上传逻辑 const handleUpload async (file) { const formData new FormData(); formData.append(file, file); try { const res await axios.post(/api/draw/participant/upload, formData, { headers: { Content-Type: multipart/form-data } }); if (res.data.code 200) { // 后端返回解析结果成功条数、失败行号及原因 const { successCount, failedRows } res.data.data; ElMessage.success(成功导入 ${successCount} 条); if (failedRows.length 0) { // 显示表格展示失败明细支持导出错误报告 showFailedTable(failedRows); } loadParticipantList(); // 刷新名单列表 } } catch (e) { ElMessage.error(上传失败 e.response?.data?.message || 网络错误); } };注意后端/api/draw/participant/upload接口需使用RequestParam MultipartFile file接收并用 Apache POI 解析。关键点在于——前端只做轻量级格式提示如“第5行工号为空”所有业务规则校验如“工号重复”、“部门不存在”必须由后端执行并返回结构化错误信息。这是防止绕过前端校验的根本防线。3.3 抽签按钮的防抖与状态锁定机制用户点击“开始抽签”按钮时必须阻断重复提交且需体现操作过程template el-button :loadingisDrawing :disabled!canDraw clickstartDraw {{ isDrawing ? 正在抽取... : 开始抽签 }} /el-button /template script setup import { ref, computed } from vue import { ElMessage } from element-plus const isDrawing ref(false) const participantList ref([]) // 已加载的参与者列表 // 计算是否可抽有可用人员且活动状态为“进行中” const canDraw computed(() { return participantList.value.some(p p.status 1) }) const startDraw async () { if (isDrawing.value || !canDraw.value) return isDrawing.value true try { const res await axios.post(/api/draw/execute, { activityId: currentActivityId.value }) ElMessage.success(恭喜 ${res.data.winnerName} 中奖) // 触发列表刷新中奖者 status 变为 3 loadParticipantList() } catch (e) { ElMessage.error(e.response?.data?.message || 抽签失败) } finally { isDrawing.value false } } /script3.3.1 为什么不用lodash.debouncedebounce适用于搜索框输入防抖但抽签是一次性事务操作。用户点击后必须立即响应哪怕失败而非延迟执行。此处isDrawing的作用是 UI 层状态锁配合后端事务回滚共同构成“一次点击一次生效”的用户体验。4. 数据库初始化与环境隔离本地开发、测试、生产三套配置落地4.1 使用 Flyway 实现数据库版本化迁移手动执行 SQL 脚本易出错、难追溯。本项目集成 Flyway将建表语句、初始数据、索引优化封装为可版本管理的迁移脚本src/main/resources/db/migration/ ├── V1__create_tables.sql # 创建 draw_activity, draw_rule, draw_participant ├── V2__add_index_to_participant.sql # 添加 activity_id status 复合索引 └── V3__insert_default_rules.sql # 插入默认规则单次抽取、不可重复在application.yml中启用spring: flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true # 首次运行时自动 baseline启动应用时Flyway 自动检测未执行的脚本并按序执行确保不同环境数据库结构一致。开发人员只需关注V{version}__desc.sql文件内容无需记忆 SQL 执行顺序。4.2 多环境配置与敏感信息隔离application-dev.yml、application-prod.yml必须严格区分# application-dev.yml开发环境 spring: datasource: url: jdbc:mysql://localhost:3306/draw_dev?useSSLfalseserverTimezoneAsia/Shanghai username: dev_user password: dev_pass # application-prod.yml生产环境 spring: datasource: url: ${DB_URL:jdbc:mysql://prod-db:3306/draw_prod?...} username: ${DB_USER:prod_user} password: ${DB_PASS:prod_pass}提示生产环境的数据库密码绝不能硬编码在配置文件中必须通过环境变量如 Docker 的-e DB_PASSxxx或 Kubernetes Secret 注入。Spring Boot 会优先读取环境变量值覆盖配置文件默认值。4.3 初始化测试数据的两种方式方式实现适用阶段Flyway V3 脚本INSERT INTO draw_rule (...) VALUES (...);所有环境统一用于基础规则Spring BootPostConstruct在DrawInitService中判断spring.profiles.activedev后插入 10 条测试名单仅开发环境避免污染测试/生产数据示例代码Component Profile(dev) public class DrawInitService { Autowired private DrawParticipantMapper participantMapper; PostConstruct public void initTestData() { if (participantMapper.selectCount(null) 0) { ListDrawParticipant testData IntStream.range(1, 11) .mapToObj(i - new DrawParticipant() .setName(测试人员 i) .setCode(TEST String.format(%03d, i)) .setDepartment(技术部) .setActivityId(1L) ).collect(Collectors.toList()); participantMapper.insertBatchSomeColumn(testData); } } }5. 抽签结果的可审计性保障日志记录、操作留痕与导出合规5.1 关键操作全链路日志埋点抽签不是匿名行为每一次抽取都必须留下可追溯的操作痕迹。Spring Boot 日志需记录三要素谁操作人、何时时间戳、做了什么参数结果Slf4j Service public class DrawService { Transactional public DrawResultDTO executeDraw(Long activityId, Long operatorId) { // ... 抽签核心逻辑 ... // 记录审计日志异步避免阻塞主流程 CompletableFuture.runAsync(() - { AuditLog log new AuditLog(); log.setOperatorId(operatorId); log.setOperationType(DRAW_EXECUTE); log.setTargetId(activityId); log.setDetail(String.format(抽中人员%s%s, winner.getName(), winner.getCode())); log.setCreatedAt(new Date()); auditLogMapper.insert(log); }); return resultDTO; } }对应audit_log表结构CREATE TABLE audit_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, operator_id BIGINT NOT NULL COMMENT 操作人ID, operation_type VARCHAR(30) NOT NULL COMMENT 操作类型DRAW_EXECUTE, ACTIVITY_START, PARTICIPANT_IMPORT, target_id BIGINT COMMENT 目标ID如 activity_id, detail TEXT COMMENT 操作详情JSON 或文本, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_operator_time (operator_id, created_at) );5.2 结果导出功能的性能与安全边界导出 Excel 时数据量过大如万人名单会导致内存溢出或超时。必须做两层防护后端分页导出不查全量而是按activity_id分批查询每页 1000 条前端限流提示导出前校验总记录数超过 5000 条弹窗确认。GetMapping(/export) public void exportResults(RequestParam Long activityId, HttpServletResponse response) throws IOException { // 1. 校验活动存在且已结束 DrawActivity activity activityMapper.selectById(activityId); if (!Objects.equals(activity.getStatus(), 3)) { throw new BusinessException(仅支持导出已结束活动的结果); } // 2. 获取总记录数避免全表扫描 long totalCount drawResultMapper.selectCount( new QueryWrapperDrawResult().eq(activity_id, activityId) ); if (totalCount 10000) { throw new BusinessException(结果数量超过 10000 条请联系管理员导出); } // 3. 使用 SXSSFWorkbook 流式写入避免 OOM SXSSFWorkbook workbook new SXSSFWorkbook(1000); Sheet sheet workbook.createSheet(抽签结果); // ... 写入表头与数据行 ... response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamedraw_result_ activityId .xlsx); workbook.write(response.getOutputStream()); workbook.dispose(); // 必须调用释放临时文件 }5.2.1SXSSFWorkbook与XSSFWorkbook的关键区别特性XSSFWorkbookSXSSFWorkbook内存占用全量加载到内存大数据量易 OOM仅保留指定行数如1000在内存其余刷盘导出速度快纯内存略慢涉及磁盘IO适用场景小数据量1000行大数据量1000行本项目默认使用SXSSFWorkbook因抽签结果导出属于低频但可能大数据量的操作稳定性优先于极致速度。5.3 前端导出按钮的权限与状态联动并非所有用户都能导出。需结合 Vue Router 元信息与后端接口鉴权!-- DrawPanel.vue -- el-button v-ifhasPermission(draw:export) clickexportResults :disabledexporting {{ exporting ? 导出中... : 导出结果 }} /el-button权限判断逻辑// utils/permission.js export function hasPermission(permission) { const permissions localStorage.getItem(user_permissions) || [] return JSON.parse(permissions).includes(permission) } // 路由定义中声明所需权限 const routes [ { path: /draw, component: () import(/views/DrawPage.vue), meta: { permissions: [draw:view, draw:execute, draw:export] } } ]后端接口/api/draw/export同样需校验 JWT Token 中的permissions字段实现前后端双重校验。这是防止前端篡改权限标识的根本手段。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

React/Next.js 应用级初始化只执行一次:Plate 仓库中 init-once 模式的规则解析与实践 2026/9/14 9:59:24

React/Next.js 应用级初始化只执行一次:Plate 仓库中 init-once 模式的规则解析与实践

React/Next.js 应用级初始化只执行一次:Plate 仓库中 init-once 模式的规则解析与实践 【免费下载链接】plate Rich-text editor with AI and shadcn/ui 项目地址: https://gitcode.com/GitHub_Trending/pl/plate 本篇围绕 Plate 仓库内置的 React 最佳实践技…

阅读更多 →
IEEE 802.15.4超帧深度解析:从CSMA/CA困境到工程实践 2026/9/14 9:59:24

IEEE 802.15.4超帧深度解析:从CSMA/CA困境到工程实践

上个月在园区做无线传感器网络改造,20多个采集节点只要一到整点就会集体“罢工”,后台缺数据缺得厉害。一开始我怀疑是天线布局和干扰,后来用抓包工具一看,问题不在信号,而在接入机制本身——所有节点醒来后都挤在同一…

阅读更多 →
brpc 全览:工业级 C++ RPC 框架的设计哲学、核心能力与实战入门 2026/9/14 9:59:24

brpc 全览:工业级 C++ RPC 框架的设计哲学、核心能力与实战入门

brpc 全览:工业级 C RPC 框架的设计哲学、核心能力与实战入门 【免费下载链接】brpc brpc is an Industrial-grade RPC framework using C Language, which is often used in high performance system such as Search, Storage, Machine learning, Advertisement, R…

阅读更多 →
长尾词SEO优化实战:工具、技巧与避坑指南 2026/9/14 9:59:24

长尾词SEO优化实战:工具、技巧与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
45个BUG到165个:操作系统开发中的持久化、USB与自举硬仗 2026/9/14 9:59:24

45个BUG到165个:操作系统开发中的持久化、USB与自举硬仗

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
深度解读 ClickHouse v22.7.4.16-stable 变更日志:哈希计算、ALTER 重算索引、DNS 解析等 5 个关键缺陷修复 2026/9/14 9:56:24

深度解读 ClickHouse v22.7.4.16-stable 变更日志:哈希计算、ALTER 重算索引、DNS 解析等 5 个关键缺陷修复

深度解读 ClickHouse v22.7.4.16-stable 变更日志:哈希计算、ALTER 重算索引、DNS 解析等 5 个关键缺陷修复 【免费下载链接】ClickHouse ClickHouse is a real-time analytics database management system 项目地址: https://gitcode.com/GitHub_Trending/cli/Cl…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞