
简介这是一套面向高校计算机相关专业学生的微信小程序毕业设计完整项目主题为活动报名管理系统适合正在准备毕业设计或课程设计、需要前后端一体化实战案例的开发者参考。项目采用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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 活动表核心是名额和状态 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 报名记录表必须对 (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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明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 }); } }); } });逻辑说明分页用pagesize后端返回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.38E10。原因是 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明每次后台敏感操作后插一条记录detail里可以放 JSON 字符串描述变更前后。参数上action用英文枚举方便后续筛选target_id存活动 ID 或报名 ID视操作对象而定。我自己做这类系统时有个习惯每加一个功能先问自己“如果数据量翻十倍哪里会先崩”。这个习惯让我在答辩前总能提前发现几个隐藏问题。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。