简介:微信小程序高校体育场管理系统是基于SSM框架开发的完整课程设计源码包,面向高校软件工程、计算机相关专业学生及需要完成微信小程序+Java后端项目的开发者。系统覆盖场地预约、扫码签到、设施维护、活动发布、健康数据分析、会员积分、智能安防监控及后台管理等功能,前端为微信小程序,后端采用Spring、Spring MVC、MyBatis,适合用于毕业设计、课程实训或项目二次开发。压缩包共1299个文件,约50.11MB,主要包含java后端源码、vue后台管理页面、微信小程序wxml/wxss/js文件、png图标及jpg素材,并附带sql数据库脚本和安装运行脚本(1-install.bat、2-run.bat),目录结构清晰,便于直接导入开发工具运行调试。已有191人学习下载。资源不仅给出完整可运行项目,还涵盖前后端分离实现思路、后台管理界面、数据表设计及配置文件,能帮助读者快速理解SSM与小程序对接过程,节省从零搭建的时间,适合边看边练、按需拆解学习。
1. 用微信小程序 + SSM 做高校体育场管理,先想清楚这三件事
高校体育场管理的核心矛盾从来不是“场地不够”,而是“信息不同步”。学生不知道哪个场子空着,管理员不知道谁来核销,周末羽毛球馆排队长龙,工作日篮球场空置一整天。用微信小程序做前端、SSM 做后端接口,本质上是在解决一条预约链路里三个角色的协作问题:学生在线查场、选时、预约;管理员审核、排期、核对入场;系统本身处理冲突检测和状态流转。
这套技术选型在高校场景里比 Vue + Spring Boot 更“顺手”的原因有三个:第一,微信小程序天然覆盖学生群体,不用装 App、不用注册额外账号,wx.login 就能拿到 openid;第二,SSM 虽然“老”,但 MyBatis 对复杂 SQL 的控制力极强,场地时间段冲突检测这种逻辑写在 XML 里比写在 ORM 注解里直观得多;第三,高校机房和毕设服务器普遍是低配 Tomcat,SSM 的部署成本远低于微服务全家桶。如果你是学生要复现这个项目,或者刚入职接手类似系统,下面这套方案就是最直接的落地路径。
2. SSM 后端:先把场地、预约、用户三张表设计对
2.1 为什么不绕过 SSM 直接用 Spring Boot
很多人的第一反应是:都什么年代了还 SSM?确实,Spring Boot 在配置上碾压 Spring MVC + XML,但高校体育场管理系统这种项目用 SSM 有它不可替代的理由。一是很多高校的选修课、毕设指导仍然以 SSM 为教学蓝本,代码评审时导师看的就是三层架构拆得清不清楚;二是我见过不少校内服务器还跑着 JDK 8 + Tomcat 8,Spring Boot 2.7 在这些环境里兼容性反而没 SSM 稳。
SSM 的分层在体育场预约场景里对应得很清晰:Spring 管 Service 层的事务边界,SpringMVC 管 Controller 层的参数绑定和异常处理,MyBatis 管数据访问。事务尤其关键——用户提交预约时要做“查时段 + 锁记录 + 插入订单”三步,这三步必须在一个事务里。Spring 的@Transactional直接标在 Service 实现类上,比 Spring Boot 的声明式事务更直观,排查问题时一眼能看出边界在哪。
2.2 三张核心表:场地表、订单表、用户表的字段取舍
体育场管理系统的表结构核心是这五张:venue(场地)、venue_schedule(可预约时段)、reservation(预约单)、user(用户)、notice(公告)。场地表别只存名称和类型,要带open_time和close_time,散场打扫时间也要留一个字段maintain_duration,否则后续做时段生成时会发现保洁时间没法处理。订单表的状态字段用 tinyint 而不是 varchar,0=待审核、1=已确认、2=已核销、3=已取消、4=已过期,比存字符串省空间,也方便 SQL 里直接做统计。
CREATE TABLE `reservation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号,格式:日期+场地编号+随机数', `user_id` bigint(20) NOT NULL, `venue_id` bigint(20) NOT NULL, `schedule_id` bigint(20) NOT NULL COMMENT '对应venue_schedule表', `date` date NOT NULL COMMENT '预约日期', `start_time` time NOT NULL, `end_time` time NOT NULL, `status` tinyint(4) NOT NULL DEFAULT '0', `create_time` datetime NOT NULL, `audit_time` datetime DEFAULT NULL, `audit_user` varchar(50) DEFAULT NULL, `cancel_reason` varchar(200) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_date` (`user_id`, `date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里的schedule_id不是必须的,但加上它有个实际好处:后台排期时如果调整了某个时段的起止时间,已预约但未核销的订单可以通过关联 schedule 直接批量变更,不用逐条改时间。唯一索引建在order_no上防重复提交,idx_user_date是为“我的预约列表”这页准备的,避免 date 条件触发全表扫描。
2.3 MyBatis 里写冲突检测 SQL,比在 Java 里做内存判断可靠
预定场地的核心并发问题是:两个用户同时提交同一个时间段的预约,数据库在事务隔离级别下会有幻读。解决方式是在 MyBatis 的 XML 里写一条带锁的查询,用SELECT ... FOR UPDATE锁住目标时段的排期记录,然后再执行插入。
<select id="checkConflict" resultType="int"> SELECT COUNT(*) FROM reservation WHERE venue_id = #{venueId} AND date = #{date} AND status IN (0, 1) AND #{startTime} < end_time AND start_time < #{endTime} FOR UPDATE </select>这段 SQL 的逻辑是判断新预约的时间区间是否与已存在的有效预约重叠:新开始时间小于已有结束时间、且新结束时间大于已有开始时间,两个条件同时成立即为冲突。状态只查 0(待审核)和 1(已确认),已取消和已过期的订单不参与冲突判断。FOR UPDATE是行级锁,事务提交后释放。配合 Service 层的@Transactional,就能在低并发场景下做到不超卖。这里的关键点是<是 XML 里的转义写法,直接写<会报 XML 解析错误。
3. 微信小程序端:从 tabBar 到预约页面的完整实现
3.1 小程序项目结构和 tabBar 配置,别用 HBuilderX 硬套模板
微信小程序端的目录结构按照页面功能划分:pages/index(首页)、pages/venue(场地列表)、pages/reserve(预约)、pages/my(个人中心)、pages/admin(仅管理员可见)。如果你先写的是 uniapp,编译到微信小程序时要注意rpx和px的混用问题——直接改 app.json 里的tabBar最稳。tabBar 的图标尺寸严格限制在 81px * 81px,超过会警告但不报错,实际显示会变形。
{ "pages": [ "pages/index/index", "pages/venue/venue", "pages/reserve/reserve", "pages/my/my" ], "tabBar": { "color": "#999999", "selectedColor": "#1AAD19", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/venue/venue", "text": "场地" }, { "pagePath": "pages/reserve/reserve", "text": "预约" }, { "pagePath": "pages/my/my", "text": "我的" } ] } }tabBar只能是页面级文件,不能指向组件页面。如果你把预约页做成了分包加载,切记不要放进 tabBar,分包页面作为 tabBar 项是直接报错的。另外pages/reserve/reserve这种路径写全,不要试图用相对路径../reserve/reserve。
3.2 wx.login 拿到 code 后,后端必须做的事
小程序登录的核心流程是:wx.login拿临时code,发送到后端,后端用code+appid+secret调微信的jscode2session接口,换取openid和session_key。openid是你系统里用户的唯一身份,session_key只是用来解密用户手机号的钥匙,不需要持久化。
后端拿到 openid 后第一件事是查user表里有没有这个用户,没有就自动创建一条记录,默认角色设为学生。然后生成你自己的token,用 UUID 或者 JWT 都行,返回给小程序端。小程序端把这个 token 存进wx.setStorageSync('token', ...),之后所有请求在 header 里带Authorization: token。别直接把 openid 返回给前端,一旦被抓包,openid 泄露等于用户身份被冒用。
@PostMapping("/login") @ResponseBody public Result login(@RequestBody Map<String, String> params) { String code = params.get("code"); String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; // 用 HttpClient 发送 GET 请求,拿回 openid String openid = getOpenidFromWx(url); User user = userMapper.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setRole(0); // 0=学生, 1=管理员 userMapper.insert(user); } String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set(token, openid, 2, TimeUnit.HOURS); return Result.success(token); }这一步里appid和secret不要硬编码在代码里,请放在配置文件,用@Value注入。回调地址要保持和微信公众平台后台配置的域名一致,本地调试时可以在开发者工具里勾选“不校验合法域名”,但真机预览必须配 HTTPS 域名。
3.3 预约页面的时间选择:Uniapp 和原生组件的差异处理
如果你用的是原生小程序,picker组件的mode=date和mode=time是最直接的方案。但高校体育场预约通常要按场地类型不同显示不同时段,比如羽毛球馆每 40 分钟一个场次、篮球场每 2 小时一个场次,这就要动态渲染时段列表,而不是用picker。
用 uniapp 开发时要注意uni-datetime-picker在 iOS 上的渲染 bug——它如果被放在scroll-view里,快速滚动时可能出现选完日期后弹层不消失的问题。解决办法是不在scroll-view内嵌这个组件,把日历弹层通过v-model绑定到页面根节点,或者干脆自己写一个时段 grid。你去看网上那些“uniapp 微信小程序兼容”的帖子,讨论最多的就是这类弹层组件在 WebView 和原生渲染下的行为差异。最省事的方式:后端一次性返回该场地未来 7 天的可预约时段数组,前端用普通 view 渲染成卡片,用户点选。
4. 前后端联调:Session、日期解析、滚动加载的实战处理
4.1 别用 Session 存登录态,改 Redis,否则小程序请求拦不住
SSM 传统做法是用 HttpSession 保存用户状态,但小程序端每次请求都是独立的 HTTPS 请求,Cookie 在部分场景下不会被正确携带,尤其是用户关闭小程序再打开后,Session 恢复是个大坑。常见做法是后端写一个拦截器,从 header 里取 token,查 Redis——为什么不是查数据库?因为用户表命中的频率远高于业务表,如果用数据库,每次请求多一次查询,高峰期能把数据库连接池打满。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } String openid = redisTemplate.opsForValue().get(token); if (openid == null) { response.setStatus(401); return false; } request.setAttribute("openid", openid); return true; } }拦截器要在 spring-mvc.xml 里注册<mvc:interceptors>,同时把/login路径排除掉,白名单里再加一个/notice/list,公告列表不需要登录就能看。Redis 的 key 过期时间设为 2 小时,学生一次体育课时长基本够了,管理员操作频率低可以单独延长。
4.2 日期参数绑定:@DateTimeFormat 处理小程序传的时间字符串
小程序端传日期一般有两种格式:2024-05-20和2024-05-20 14:00:00。如果你的接口用Date类型接收,不加@DateTimeFormat会直接报 400 错误。这个坑几乎每个写 SSM + 小程序联调的人都会踩一次。
@GetMapping("/available") @ResponseBody public Result getAvailableTime(@RequestParam Long venueId, @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date date) { List<TimeSlot> slots = venueService.getSlots(venueId, date); return Result.success(slots); }@DateTimeFormat只处理入参的字符串转 Date,出参返回给小程序时用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")标注在实体字段上。这里还有个常见错误:@DateTimeFormat和@JsonFormat一个是 Spring 的、一个是 Jackson 的,混用时要搞清楚你项目里 fastjson 和 Jackson 谁生效,否则这边转了那边又变了。
4.3 场地列表滚动加载:onReachBottom 不是唯一解
体育场列表如果只有十几个场地,一次性返回没问题。但如果是大型高校,场地可能分校区、分运动类型,列表超过 30 条就需要分页。小程序端用onReachBottom触发加载下一页,是原生的做法。配合 SSM 后端的分页插件 PageHelper,写法上有个约定要遵守:PageHelper 的startPage必须紧跟着 Mapper 的查询方法,中间不能有任何其他 SQL 操作,否则分页会串到别的查询上。
@Override public List<Venue> getVenueList(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); return venueMapper.selectAll(); }前端在onReachBottom里判断hasMore字段,为 true 才累加页码重新请求。注意下拉刷新用enablePullDownRefresh,在微信开发者工具里模拟下拉是鼠标按住顶部向下拖,真机是手势操作,这两者触发的行为不完全一致,测试时要分开测,别只在开发者工具里点一下就结束。
4.4 预约成功后的反馈:微信订阅消息还是公众号模板消息
场地审核通过后,需要通知学生。微信在 2020 年后收紧了模板消息,现在小程序只能用订阅消息,而且需要用户主动点击授权按钮才能发送一次。常见做法是:预约提交成功后弹出一个授权面板,用户点“允许”后存下form_id,等管理员审核通过后,后端拿着这个form_id调发送接口。
wx.requestSubscribeMessage({ tmplIds: ['模板ID在这里'], success(res) { if (res['模板ID在这里'] === 'accept') { // 用户授权了一次,可以发送一条订阅消息 } } })这里有个细节:订阅消息一次性授权只能发一次,学生如果约了 10 次球,每次都要点一次授权。所以在预约流程里,授权弹窗要放在“提交预约”按钮之前还是之后,是个产品决策。放在之前转化率高但体验突兀,放在之后审核通过时才发现没授权就发不了。我一般会放在提交成功后的结果页,作为补发动作而不是必选动作。
5. 管理员端的 SSM 实现:审核、排期、统计三个接口的写法
5.1 管理员的角色控制:拦截器里做减法,Service 里做校验
管理员功能不建议单独做一个小程序,同一个微信小程序里通过 role 字段做页面级控制就够了。后端在 AuthInterceptor 里已经存了 openid,再来一个AdminInterceptor继承它,在 preHandle 里额外查一次用户角色。
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String openid = (String) request.getAttribute("openid"); User user = userMapper.findByOpenid(openid); if (user == null || user.getRole() != 1) { response.setStatus(403); return false; } return true; }不要把角色判断写进每一个 Controller 的 if 里,拦截器统一处理,新加管理接口时只要注册 URL 规则就行,不容易漏。URL 规则建议/admin/**全部拦截,Controller 里的管理接口统一放在/admin路径下。
5.2 时段生成:用 date 和 schedule 两张表组合出可预约时段
场馆的开放时间是固定的,比如周一至周五 18:00-22:00,周六日 09:00-21:00。生成可预约时段不需要手动一条条插入,写一个定时任务,每天凌晨生成未来 7 天的时段记录。
@Scheduled(cron = "0 0 2 * * ?") public void generateSchedule() { Date today = DateUtils.truncate(new Date(), Calendar.DAY_OF_MONTH); for (int i = 1; i <= 7; i++) { Date date = DateUtils.addDays(today, i); if (isWeekend(date)) { venueScheduleMapper.insertBatch(date, weekendStart, weekendEnd, 60); } else { venueScheduleMapper.insertBatch(date, weekdayStart, weekdayEnd, 60); } } }时段粒度按 60 分钟切分,这个值要能配置。如果场地类型不同,羽毛球馆要 40 分钟的间隔,篮球场要 120 分钟,venue表里就要加一个slot_minutes字段,而不是写死。定时任务用 Spring 的@Scheduled,在 spring-mvc.xml 里开<task:annotation-driven/>,注意 SSM 项目中这个命名空间的缺失会导致@Scheduled完全不执行,没有任何报错,排查时很难发现。
5.3 预约统计的 SQL:按天、按场地的使用率计算
管理后台常常要一个“今日场地使用情况”的数据面板。不用引入额外的报表工具,MyBatis 里写一条按场地和日期分组统计的 SQL 就够。
SELECT v.venue_name, COUNT(r.id) AS total_orders, SUM(CASE WHEN r.status = 1 THEN 1 ELSE 0 END) AS confirmed_orders, ROUND(SUM(CASE WHEN r.status = 1 THEN 1 ELSE 0 END) / COUNT(r.id) * 100, 2) AS usage_rate FROM venue v LEFT JOIN reservation r ON v.id = r.venue_id AND r.date = #{date} GROUP BY v.id ORDER BY usage_rate DESCLEFT JOIN 保证了没有预约记录的场地也能出现在列表里,usage_rate 为 0。ROUND(x, 2)保留两位小数,前端展示时不需要再格式化。这条 SQL 的数据量级在高校场景里极小,即使全表关联也没压力,不用优化。真正要优化的是按月份统计预约趋势,那时数据量上来了,建议在reservation表上加date的索引单独查。
6. 上线部署前必查清单与三个隐藏瓶颈
小程序项目在开发者工具里跑通只代表逻辑正确,真正上线前有十个点必须逐项过:后端域名必须是 HTTPS 且备案,小程序后台配置 request 合法域名;SSL 证书别用免费的单域名证书,腾讯云的免费证书一年一换,到期后小程序直接请求失败,而你看后端日志啥报错都没有;wx.login的 code 只能用一次,前端重复调用会报invalid code,必要时在每次onShow重置登录态;数据库连接池的maxActive要按 Tomcat 默认线程数 200 来配,设成 50 的后果是高峰期连接池被占满,Tomcat 线程池还在排队,处理时长直接翻几倍;MyBatis 的<cache/>二级缓存默认不开,开了之后场地列表和预约列表会出现脏读,因为reservation表频繁更新,缓存失效策略很难精确控制;定时任务的时区要统一用Asia/Shanghai,服务器默认 UTC 的话凌晨 2 点生成的是北京时间 10 点的数据;小程序端的地图组件如果要做位置打卡功能,需要声明requiredPrivateInfos,否则wx.getLocation返回隐私授权错误;图片上传要压缩到 200KB 以下,微信服务器对上行包体限制是 1MB,学生上传篮球场实拍图很容易超限;接口返回的null字段在小程序端setData时会报Cannot read property,后端要统一Result包装类兜底空数组;本地调试时 charles 抓包要安装证书,真机预览时抓包则要在微信开发者工具里设置代理,否则只看得到 TCP 层的 HTTP 请求,看不到业务响应体。
这三个隐藏瓶颈分别出现在场地列表首次加载慢、审核操作偶发超时、定时生成时段偶尔跳过一天。第一个瓶颈是场地列表接口在 PageHelper 分页时,count 查询执行了一次全表,解决方式是手写 count SQL;第二个瓶颈是审核接口事务里查了 Redis 又查了 MySQL,两次网络往返,把 Redis 的 token 校验提前到 Controller 层;第三个瓶颈是服务器休眠策略导致 JVM 时钟偏差,@Scheduled用的是系统时间而非数据库时间,把生成时段的基准时间从new Date()改成查数据库的SELECT NOW(),一次改动解决所有时间漂移问题。
最后验证方法超过瘾:拿两台手机真机同时抢同一个场地同一个时段的最后一个位置,看是不是只有一个能提交成功,另一个收到“该时段已被预约”的提示。这个测试通过,你的事务和锁就算真正落地了。再测一遍把手机时间改到预约日期的前一天,看小程序端会不会出现日期选择器可选的日期不包括今天。这几个测试跑完,系统离上线就只差后续运维了。
本文还有配套的精品资源,点击获取