ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

微信小程序活动报名管理系统:高并发名额扣减与防重复报名实战

微信小程序活动报名管理系统:高并发名额扣减与防重复报名实战

简介:这是一套面向高校计算机相关专业学生的微信小程序毕业设计完整项目,主题为活动报名管理系统,适合正在准备毕业设计或课程设计、需要前后端一体化实战案例的开发者参考。项目采用Java语言与SSM框架搭建后端,运行于Tomcat7和JDK1.8环境,数据库使用MySQL 5.7,小程序端基于uniapp或原生小程序开发,配套HBuilder X与微信开发者工具。功能覆盖站内新闻、用户注册、活动信息展示、活动报名与个人中心等前台模块,以及管理员管理、新闻发布、报名审核、活动信息维护和注册用户管理等后台模块,业务闭环较为完整。资源包共424个文件,包含java源码、class编译文件、jar依赖、xml配置、html页面、js脚本、wxml与wxss小程序页面样式,以及sql建库脚本和说明文档,压缩包约34.59MB。已有84人学习,可作为毕业设计选题落地的参考方案,帮助读者理解SSM与小程序端的数据交互、报名审核流程及后台管理模块的实现思路。

1. 活动报名管理系统:从“能跑”到“能交”差在哪

每年到了毕业设计季,总有一批同学卡在同一个地方:选题定了“微信小程序活动报名管理系统”,代码也东拼西凑跑起来了,但真到答辩前一周,发现报名人数一多就乱、后台导出名单对不上、老师问一句“你这个并发怎么处理的”就答不上来。问题不在功能多少,而在于这套系统从“能跑”到“能交”之间,缺的是对业务链路的完整理解。

这个标题指向的是一套完整的前后端分离方案:微信小程序做用户端,服务端提供接口,MySQL 存数据,再配一份说明文档和论文材料。它解决的核心问题是——把“活动发布 → 用户报名 → 名额控制 → 名单管理 → 数据导出”这条链路跑通,并且让每一步都有据可查。适合谁?适合正在做小程序方向毕业设计、需要一套可复现、可讲清楚、能扛住答辩追问的本科或专科同学,也适合想快速搭一个活动报名类工具的前端开发者。

2. 先把数据模型定死:五张表撑起整条报名链路

很多同学一上来就写页面,写到“报名”那一步发现字段不够用,回头改表结构,前端接口全得跟着动。血泪经验是:先把表设计完,再动手写代码,能省掉一半返工。

2.1 活动表、报名表、用户表的最小字段集

一套能用的活动报名系统,核心表不超过五张。下面是我一般会用的字段设计,直接给 SQL,你可以照着建。

-- 用户表:小程序端通常用 openid 做唯一标识 CREATE TABLE `user` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信开放平台唯一标识', `nickname` VARCHAR(64) DEFAULT '' COMMENT '昵称', `avatar` VARCHAR(255) DEFAULT '' COMMENT '头像地址', `phone` VARCHAR(20) DEFAULT '' COMMENT '手机号,报名时补填', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 活动表:核心是名额和状态 CREATE TABLE `activity` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `title` VARCHAR(128) NOT NULL COMMENT '活动标题', `cover` VARCHAR(255) DEFAULT '' COMMENT '封面图', `content` TEXT COMMENT '活动详情', `location` VARCHAR(128) DEFAULT '' COMMENT '活动地点', `start_time` DATETIME NOT NULL COMMENT '活动开始时间', `end_time` DATETIME NOT NULL COMMENT '活动结束时间', `sign_start` DATETIME NOT NULL COMMENT '报名开始时间', `sign_end` DATETIME NOT NULL COMMENT '报名截止时间', `quota` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '总名额,0 表示不限', `signed_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '已报名人数,冗余字段', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1 上架 0 下架', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status_sign` (`status`, `sign_end`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 报名记录表:必须对 (activity_id, user_id) 做唯一约束 CREATE TABLE `signup` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `activity_id` INT UNSIGNED NOT NULL, `user_id` INT UNSIGNED NOT NULL, `name` VARCHAR(32) NOT NULL COMMENT '真实姓名', `phone` VARCHAR(20) NOT NULL, `remark` VARCHAR(255) DEFAULT '' COMMENT '备注', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1 已报名 2 已取消 3 已签到', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_activity_user` (`activity_id`, `user_id`), KEY `idx_activity_status` (`activity_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:activity表里放了一个signed_count冗余字段,目的是列表页展示“已报 12/50”时不用每次去 count 报名表。代价是每次报名成功要同步更新它,这个后面在事务里处理。signup表的唯一索引uk_activity_user是防重复报名的第一道防线,比在代码里查一次再插入可靠得多。

参数说明:quota为 0 表示不限名额,这个约定要在接口文档里写清楚,否则前端判断“是否已满”会出错。status字段在活动表和报名表里含义不同,命名时最好加注释,避免后期自己看混。

2.2 名额扣减为什么必须放在事务里

报名接口是整个系统最容易翻车的地方。两个人同时点“立即报名”,如果先查名额再插入,中间有时间窗口,超卖就发生了。常见做法是把“检查名额 + 插入报名记录 + 更新已报人数”包在一个事务里,并且用行锁把活动记录锁住。

START TRANSACTION; -- 锁定活动行,防止并发修改 signed_count SELECT quota, signed_count FROM activity WHERE id = ? FOR UPDATE; -- 应用层判断:quota > 0 且 signed_count < quota INSERT INTO signup (activity_id, user_id, name, phone) VALUES (?, ?, ?, ?); UPDATE activity SET signed_count = signed_count + 1 WHERE id = ?; COMMIT;

逻辑说明:FOR UPDATE会在事务提交前锁住这一行,第二个请求进来会等待,等第一个事务提交后再读到最新的signed_count,从而避免超卖。参数上要注意,MySQL 默认隔离级别是可重复读,FOR UPDATE读的是最新已提交数据,符合这里的需求。

提示:如果用的是云开发或 Serverless 数据库,可能不支持FOR UPDATE,那就退而求其次,用唯一索引兜底,插入冲突时返回“已报名”,至少保证不重复。

3. 小程序端报名页:三个接口串起完整交互

前端不需要花哨,把“列表 → 详情 → 报名 → 我的报名”这条线走通就够了。这里以原生小程序为例,给出关键页面的请求逻辑。

3.1 活动列表与详情的数据加载

列表页只需要拿到标题、封面、时间、已报/总名额这几个字段,不要把所有详情都塞进列表接口,否则数据量一大就卡。

// pages/activity/list.js Page({ data: { list: [], page: 1, hasMore: true }, onLoad() { this.loadList(); }, loadList() { if (!this.data.hasMore) return; wx.request({ url: 'https://your-domain.com/api/activity/list', data: { page: this.data.page, size: 10 }, success: (res) => { // 后端返回 { code:0, data:{ list:[], total:100 } } const { list, total } = res.data.data; this.setData({ list: this.data.list.concat(list), page: this.data.page + 1, hasMore: this.data.list.length + list.length < total }); } }); } });

逻辑说明:分页用page+size,后端返回total让前端判断是否还有下一页。参数上,size不建议超过 20,小程序列表渲染量太大会掉帧。

详情页拿到activity_id后请求详情接口,同时要判断当前用户是否已经报过名,决定按钮显示“立即报名”还是“已报名”。

// pages/activity/detail.js onLoad(options) { this.activityId = options.id; this.loadDetail(); this.checkSignup(); }, checkSignup() { wx.request({ url: 'https://your-domain.com/api/signup/check', data: { activity_id: this.activityId }, header: { 'Authorization': wx.getStorageSync('token') }, success: (res) => { this.setData({ hasSigned: res.data.data.signed }); } }); }

逻辑说明:check接口根据当前登录用户的user_id和活动 ID 查signup表,返回布尔值。参数上,Authorization头里放登录后拿到的 token,后端解析出用户身份,不要在前端传user_id,否则可以伪造。

3.2 报名提交与重复报名的前端拦截

报名按钮点击后,先做一次本地防抖,再发请求。后端返回“已报名”时,前端要给出明确提示,而不是笼统的“操作失败”。

submitSignup() { if (this.data.submitting) return; this.setData({ submitting: true }); wx.request({ url: 'https://your-domain.com/api/signup/create', method: 'POST', header: { 'Authorization': wx.getStorageSync('token') }, data: { activity_id: this.activityId, name: this.data.form.name, phone: this.data.form.phone, remark: this.data.form.remark }, success: (res) => { if (res.data.code === 0) { wx.showToast({ title: '报名成功' }); this.setData({ hasSigned: true }); } else if (res.data.code === 1001) { wx.showToast({ title: '名额已满', icon: 'none' }); } else if (res.data.code === 1002) { wx.showToast({ title: '你已报名过该活动', icon: 'none' }); } else { wx.showToast({ title: res.data.msg || '报名失败', icon: 'none' }); } }, complete: () => this.setData({ submitting: false }) }); }

逻辑说明:submitting标志位防止用户连点。后端用错误码区分“名额满”和“重复报名”,前端才能给出准确提示。参数上,name和phone要做非空和格式校验,手机号用正则简单校验即可,不必过于复杂。

4. 后台管理端:名单导出与状态流转的落地细节

后台不需要做成大而全的 CMS,但“看名单、改状态、导出 Excel”这三件事必须稳。很多同学在这里踩坑,导出中文乱码、时间格式不对、数据量大了超时。

4.1 报名名单的分页查询与筛选

后台名单接口要支持按活动 ID、报名状态、关键字(姓名/手机号)筛选,并且分页。SQL 层面用动态条件拼接,但要注意防注入。

-- 后台名单查询,status 为 null 时不筛选 SELECT s.id, s.name, s.phone, s.remark, s.status, s.create_time, u.nickname FROM signup s LEFT JOIN user u ON s.user_id = u.id WHERE s.activity_id = ? AND (? IS NULL OR s.status = ?) AND (? IS NULL OR s.name LIKE CONCAT('%', ?, '%') OR s.phone LIKE CONCAT('%', ?, '%')) ORDER BY s.create_time DESC LIMIT ?, ?;

逻辑说明:用? IS NULL OR的方式做可选条件,避免在代码里拼字符串。参数上,LIMIT的偏移量在数据量大时会变慢,如果名单超过几万条,建议改用“上一页最后一条 ID”做游标分页。

4.2 导出 Excel 的两种做法与选择

导出功能有两种常见实现:后端生成 Excel 文件返回下载地址,或者后端返回 JSON 前端用 SheetJS 生成。前者适合数据量大、需要留档的场景,后者适合数据量小、即时导出。

// 后端返回 JSON,前端用 SheetJS 导出(适合几百条以内) const XLSX = require('xlsx'); function exportExcel(list, activityTitle) { const rows = list.map((item, index) => ({ '序号': index + 1, '姓名': item.name, '手机号': item.phone, '备注': item.remark, '报名时间': item.create_time, '状态': item.status === 1 ? '已报名' : item.status === 2 ? '已取消' : '已签到' })); const ws = XLSX.utils.json_to_sheet(rows); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, '报名名单'); XLSX.writeFile(wb, `${activityTitle}_报名名单.xlsx`); }

逻辑说明:json_to_sheet直接把对象数组转成表格,列名用中文。参数上,writeFile在小程序里不可用,这是后台 Web 端的写法;如果后台也是小程序,需要把生成的 base64 传给wx.saveFile再打开。

注意:导出中文乱码通常是因为后端直接输出 CSV 时没加 BOM 头。如果走 CSV 方案,在文件开头写入\uFEFF即可。

5. 避坑与排查:那些答辩前最容易翻车的地方

这一章列的都是我见过或自己踩过的坑,每条按“现象 → 原因 → 解决”写,你对着排查能省不少时间。

5.1 报名成功但名单里查不到

现象:用户端提示报名成功,后台名单刷新后没有这条记录。原因通常是事务提交前发生了异常,或者signed_count更新了但signup插入失败。解决:检查事务边界,确保插入和更新在同一个COMMIT里;在signup表上加唯一索引,插入失败时回滚整个事务并返回明确错误码。

5.2 活动已截止但还能报名

现象:报名截止时间已过,用户仍能提交成功。原因一般是只在前端判断了时间,后端没校验。解决:报名接口里必须重新查一次活动的sign_start和sign_end,用服务器时间比较,不信任客户端时间。参数上,服务器时间用NOW()或应用层new Date()都可以,但要统一时区。

5.3 名额显示 0/50 但实际已报满

现象:列表页显示“已报 0/50”,点进去却能报名,报完变成 1/50。原因是signed_count冗余字段没有初始化或同步。解决:活动创建时signed_count默认 0;每次报名、取消、签到状态变更时同步更新;如果历史数据乱了,写一条 SQL 重算:UPDATE activity a SET signed_count = (SELECT COUNT(*) FROM signup s WHERE s.activity_id = a.id AND s.status = 1)。

5.4 导出名单手机号变成科学计数法

现象:Excel 里手机号显示成1.38E+10。原因是 Excel 把长数字当数值处理。解决:导出时把手机号字段转成字符串,并在前面加一个制表符或单引号;用 SheetJS 时设置单元格类型为s(字符串)。参数上,json_to_sheet的第二个参数可以传{ cellDates: true },但手机号仍需手动转字符串。

5.5 小程序请求跨域或域名未配置

现象:开发者工具里请求正常,真机预览报“不在以下 request 合法域名列表中”。原因是小程序正式环境要求所有请求域名在后台配置。解决:开发阶段在开发者工具里勾选“不校验合法域名”,上线前把接口域名加到小程序后台的 request 合法域名里。如果只是毕业设计演示,用开发者工具预览即可,不必折腾域名备案。

6. 让这套系统在答辩时多拿几分:三个可验证的进阶点

到这一步,功能已经能跑了。但答辩老师往往不看功能多少,而是看你有没有想过“如果”。下面三个点,任选一个做进去,都能在答辩时变成你的加分项。

第一个是报名高峰的限流。你可以在报名接口加一个简单的令牌桶,用 Redis 或内存计数都行,限制同一用户每秒最多请求一次。代码不长,但能讲清楚“为什么需要”和“怎么验证”。验证方法:用压测工具并发 100 个请求,看是否只有前 N 个成功,其余返回“请求过于频繁”。

第二个是报名数据的可视化。后台加一个简单的统计页,用 ECharts 展示每个活动的报名趋势、签到率。数据来源就是signup表的create_time和status。这个点好在直观,答辩时打开页面就能讲,而且能体现你对数据价值的理解。

第三个是操作日志。在后台加一张operation_log表,记录谁在什么时候改了哪个活动的状态、导出了哪份名单。字段就四个:user_id、action、target_id、create_time。这个点不显眼,但老师问“怎么追溯误操作”时,你能答上来。

CREATE TABLE `operation_log` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `user_id` INT UNSIGNED NOT NULL, `action` VARCHAR(64) NOT NULL COMMENT '如 export_signup, update_activity', `target_id` INT UNSIGNED DEFAULT 0, `detail` VARCHAR(255) DEFAULT '', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_time` (`user_id`, `create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

逻辑说明:每次后台敏感操作后插一条记录,detail里可以放 JSON 字符串描述变更前后。参数上,action用英文枚举,方便后续筛选;target_id存活动 ID 或报名 ID,视操作对象而定。

我自己做这类系统时有个习惯:每加一个功能,先问自己“如果数据量翻十倍,哪里会先崩”。这个习惯让我在答辩前总能提前发现几个隐藏问题。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表