微信小程序+SSM学生活动管理系统:报名审核与名额并发控制 📅 发布时间:2026/9/18 15:15:53 👁 浏览次数: 简介这份基于微信小程序的学生活动管理系统设计与实现文档面向计算机相关专业学生、毕业设计或课程设计开发者以及希望入门微信小程序与SSM后端开发的学习者。文档围绕学生活动管理场景展开从选题背景与研究意义入手梳理系统开发技术、需求分析与功能设计涉及微信开发者工具、MySQL数据库、Java语言及SSM框架等关键内容。系统功能覆盖管理员审核活动报名、管理活动与学生留言、维护学生信息以及学生查看活动、参与报名、浏览报名记录与活动公告等模块可帮助读者理解前后端分离的落地思路与数据库表设计方法。压缩包内共1个docx文件大小约1.63MB包含摘要、目录及绪论等章节便于直接查阅与二次整理。目前已有88人学习浏览适合需要项目选题参考、功能模块拆解及技术选型借鉴的读者使用。1. 校园活动通知发到群里没人看问题不在活动本身社团负责人把活动海报丢进 QQ 群接龙报名最后拿 Excel 手工统计——这套流程跑过一学期就知道有多难受名额超了没人知道报名名单有三个版本谁交过材料全靠翻聊天记录。基于微信小程序的学生活动管理系统要解决的就是这一段学生点开小程序看活动、在线报名、查自己的报名状态和公告管理员在后台发活动、设可报人数和报名时间、审核报名、管留言和学生。整套东西跑在 MySQL 上后端用 Java 的 SSM 三件套前端在微信开发者工具里写小程序不需要装 APP也不用用户注册新账号。适合两类人一是做毕设或课程设计、需要一份能跑通的完整链路参考的同学二是校内做信息化、想用最少运维成本把活动流程搬到手机上的开发者。真正值得花时间的不是页面好不好看而是报名名额怎么卡住、审核状态怎么流转、真机上为什么请求发不出去。2. 微信小程序 SSM MySQL 的职责边界与工程骨架2.1 小程序端不直连数据库这条线必须划清小程序代码包会被下载到用户手机上任何写在里面的数据库账号等同于公开。再加上小程序要求请求域名走 HTTPS 且需在后台配置所以结构上只有一种合理切法小程序负责页面渲染和交互所有业务规则、权限判定、状态流转都放在 Java 服务端服务端再通过 MyBatis 访问 MySQL。SSM 三个部分各管一段Spring 管 Bean 生命周期和声明式事务Spring MVC 管 URL 路由、参数绑定和 JSON 序列化MyBatis 管 SQL 与结果集映射。报名审核这种一次要改两张表的操作靠的就是 Spring 的Transactional放在 Controller 里做事务是常见的错误写法。层承担的事不该做的事小程序 page数据展示、表单收集、跳转判断名额是否已满、判断用户身份Controller参数校验、登录态读取、返回统一结构写业务分支、直接调 MapperService状态机流转、事务、名额扣减拼页面需要的展示字段Mapper / MySQL增删改查、唯一约束兜底处理业务规则2.2 工程目录与依赖我一般把后端做成单模块 Maven 工程小程序端另开一个目录两者独立发布。后端目录大致是这样activity-admin ├── pom.xml └── src/main ├── java/com/campus/activity │ ├── controller # 活动、报名、用户、公告接口 │ ├── service # 业务与事务 │ ├── mapper # MyBatis 接口 │ ├── entity # 与表一一对应 │ └── config # 拦截器、跨域、日期格式 └── resources ├── application.yml └── mapper/*.xml # SQL 映射pom.xml里真正需要的依赖并不多MyBatis 的 Spring Boot starter 会把 SqlSessionFactory 自动装配好dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesmybatis-spring-boot-starter负责把 Mapper 接口扫描成 Bean不需要再手写SqlSessionTemplatemysql-connector-j只在运行期生效编译期不参与。2.3 application.yml 与时间格式连接池和 MyBatis 的几项配置直接决定后面调不调得通server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/activity_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: trueserverTimezone不写会出现时间差 8 小时这是活动报名时间判断最容易踩的坑。map-underscore-to-camel-case让activity_name自动映射到activityName省掉大量resultMap。date-format统一返回格式否则小程序端拿到的可能是时间戳导致页面显示NaN。2.4 小程序端的请求封装不要在每个页面里重复写wx.request封一层能统一处理登录态失效和错误提示// utils/request.js const BASE_URL https://你的域名/api function request(url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method, data, header: { content-type: application/json, token: wx.getStorageSync(token) || // 登录态随请求带上 }, success(res) { if (res.statusCode ! 200) return reject(new Error(网络异常)) if (res.data.code 401) { // 登录过期统一处理 wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/login }) return reject(new Error(登录已过期)) } resolve(res.data) }, fail: reject }) }) } module.exports { request }BASE_URL必须是配置在微信公众平台「服务器域名」里的地址开发阶段可以在开发者工具「详情 - 本地设置」勾选不校验合法域名但这一步只对模拟器生效真机调试仍然会走真实校验。3. 活动、报名、学生、管理员四张核心表的落地设计3.1 从 E-R 到关系表字段类型怎么定原始设计里有四个实体活动、活动报名、学生、管理员。把它们翻成表结构时有几个决定要想清楚。活动表的活动内容用text别用varchar(255)一段带排版的说明很容易超可报人数用int而不是varchar否则后面比较剩余名额要转类型索引也用不上。报名表的学号、活动 ID 是关联字段需要单独建索引因为「查我的报名」和「按活动统计报名数」是全表最频繁的两个查询。学生表的密码绝不能存明文常见做法是存加盐后的哈希值。3.2 建表 SQLCREATE TABLE activity ( id bigint NOT NULL AUTO_INCREMENT, activity_name varchar(100) NOT NULL COMMENT 活动名称, activity_type varchar(50) DEFAULT NULL COMMENT 活动类型, cover varchar(255) DEFAULT NULL COMMENT 封面图路径, place varchar(120) DEFAULT NULL COMMENT 活动地点, sign_start datetime DEFAULT NULL COMMENT 报名开始时间, sign_end datetime DEFAULT NULL COMMENT 报名截止时间, capacity int NOT NULL DEFAULT 0 COMMENT 可报人数, activity_time datetime DEFAULT NULL COMMENT 活动举办时间, content text COMMENT 活动内容, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_type_time (activity_type, activity_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE activity_signup ( id bigint NOT NULL AUTO_INCREMENT, activity_id bigint NOT NULL COMMENT 活动ID, student_no varchar(30) NOT NULL COMMENT 学号, student_name varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, sign_date date DEFAULT NULL COMMENT 报名日期, content varchar(500) DEFAULT NULL COMMENT 报名说明, audit_status tinyint NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2驳回, audit_reply varchar(255) DEFAULT NULL COMMENT 审核回复, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_act_student (activity_id, student_no), KEY idx_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_act_student这个联合唯一索引是防重复报名的最后一道闸。业务层当然也会先查一次但两个请求同时到达时先查后插之间存在窗口唯一索引能保证数据库层面只留一条Service 捕获DuplicateKeyException转成友好提示即可。idx_student_no支撑「我的报名」列表走索引后翻页基本是毫秒级。3.3 审核状态字段的取值约定audit_status用数字而非字符串好处是索引小、比较快代价是可读性差所以必须在注释里写清取值并且前后端共用同一套常量值含义学生端可见文案是否占用名额0待审核报名已提交等待审核占用1审核通过报名成功占用2审核驳回未通过原因见回复不占用名额统计的口径要按这张表来已占名额 该活动下audit_status in (0,1)的条数。如果把驳回也算进去会出现「明明只批了 20 人却提示名额已满」的诡异现象问题是统计口径没统一。4. 报名审核链路从 wx.request 到 Service 状态流转4.1 学生端提交报名的参数与校验小程序页面拿到活动 ID 后组装参数服务端做二次校验// pages/activity/signup.js const { request } require(../../utils/request) Page({ data: { activityId: null, form: { studentNo: , studentName: , phone: , content: } }, onLoad(options) { this.setData({ activityId: Number(options.id) }) // 路由参数是字符串转成数字 }, submit() { const { form, activityId } this.data if (!form.studentNo || !form.studentName) { return wx.showToast({ title: 学号和姓名必填, icon: none }) } request(/signup/submit, POST, Object.assign({ activityId }, form)) .then(() wx.showToast({ title: 已提交等待审核 })) .catch(err wx.showToast({ title: err.message, icon: none })) } })onLoad里的options.id是字符串activityId不转数字的话后端用bigint接收时可能报参数类型不匹配。小程序端的必填校验只是体验优化真正的校验必须在服务端重做一遍否则改个请求参数就能绕过。4.2 后端报名接口与事务Service public class SignupServiceImpl implements SignupService { Autowired private ActivityMapper activityMapper; Autowired private SignupMapper signupMapper; Override Transactional(rollbackFor Exception.class) public void submit(SignupDTO dto) { Activity act activityMapper.selectById(dto.getActivityId()); if (act null) throw new BizException(活动不存在); Date now new Date(); if (act.getSignStart() ! null now.before(act.getSignStart())) throw new BizException(报名尚未开始); if (act.getSignEnd() ! null now.after(act.getSignEnd())) throw new BizException(报名已截止); // 只统计待审核和已通过驳回不占名额 int used signupMapper.countOccupied(act.getId()); if (act.getCapacity() 0 used act.getCapacity()) throw new BizException(名额已满); ActivitySignup entity new ActivitySignup(); entity.setActivityId(act.getId()); entity.setStudentNo(dto.getStudentNo()); entity.setStudentName(dto.getStudentName()); entity.setPhone(dto.getPhone()); entity.setContent(dto.getContent()); entity.setSignDate(new Date()); entity.setAuditStatus(0); try { signupMapper.insert(entity); } catch (DuplicateKeyException e) { throw new BizException(你已报名该活动请勿重复提交); } } }rollbackFor Exception.class很重要默认只对运行时异常回滚业务里抛的自定义异常如果不声明就可能留下脏数据。countOccupied对应的 SQL 用sum(case when audit_status in (0,1) then 1 else 0 end)或直接count(*) where audit_status in (0,1)两种写法都行关键是口径和 3.3 节保持一致。4.3 管理员审核接口与状态流转审核只允许从「待审核」走到「通过」或「驳回」不允许反过来任意改所以在 SQL 的where里带上原状态update idaudit UPDATE activity_signup SET audit_status #{status}, audit_reply #{reply} WHERE id #{id} AND audit_status 0 /updateService 里判断返回值等于 0 说明这条记录已经不是待审核状态了多半是管理员重复点了按钮直接返回提示而不是报错。这种「带条件的更新」比先查再改更抗并发也不需要额外加锁。4.4 名额紧张的场景怎么处理校园热门活动放出来几秒钟就会涌进上百个请求4.2 里的「先查再插」在高并发下会超卖。常见做法有两种一是把活动行加悲观锁select ... for update实现简单但会串行化该活动的所有报名二是用 Redis 做预扣减落到数据库时再校验一次。毕设级别的系统用第一种就够但要知道它的代价。// ActivityMapper.xml 中 // SELECT * FROM activity WHERE id #{id} FOR UPDATE Activity act activityMapper.selectByIdForUpdate(dto.getActivityId());加上FOR UPDATE后同一活动的并发报名会被排队执行Transactional保证锁在事务提交时释放。注意这个锁必须在事务内生效Controller 上直接调 Mapper 是不行的。5. 联调排错与上线前必须验的几件事真机调试时请求发不出去九成是域名问题。开发者工具勾选「不校验合法域名」只对模拟器有效真机走的是微信客户端的真实校验域名必须 HTTPS、已备案、并在公众平台「开发管理 - 服务器域名」里配置过。临时验证可以用开发者工具的「真机调试」它在部分情况下会放宽限制但正式发布前必须把域名配好。基础库版本是第二个高频坑。不同用户手机的微信版本不一样基础库能力也不一样wx.getUserProfile、wx.chooseMedia这类接口在低版本上表现不同。在app.json里声明最低基础库版本并在开发者工具「详情 - 本地设置」里把调试基础库切到最低版本跑一遍能提前暴露问题{ pages: [pages/index/index, pages/activity/detail, pages/signup/list], window: { navigationBarTitleText: 学生活动管理 }, requiredPrivateInfos: [], lazyCodeLoading: requiredComponents }lazyCodeLoading: requiredComponents让小程序只加载用到的组件页面多的项目首屏能快一截这是上线前很容易被忽略但收益明显的一项。第三个坑是setData的数据量。活动列表一次返回上百条再整个塞进setData低端机上会明显卡顿因为数据要从逻辑层跨线程序列化到渲染层。做法是分页拉取每页 10 到 20 条滚动到底再加载下一页。第四个是时间。后端配了date-format后返回的是yyyy-MM-dd HH:mm:ssiOS 上new Date(2025-09-01 10:00:00)会解析失败返回Invalid Date因为 iOS 只认2025/09/01 10:00:00这种带斜杠的格式。稳妥的做法是在工具函数里统一替换function parseTime(str) { if (!str) return null return new Date(str.replace(/-/g, /)) // 兼容 iOS 的日期解析 }上线前按这张单子逐项过一遍比事后看用户反馈省事得多检查项通过标准域名配置真机请求返回 200非 403 或超时最低基础库切到声明的最低版本能正常走完报名流程时间格式iOS 真机活动时间显示正常非 Invalid Date重复报名同一学号二次提交返回友好提示数据库只有一条名额并发用压测工具并发提交报名成功数不超过可报人数审核幂等连点两次审核按钮状态不被重复改写本文还有配套的精品资源点击获取