简介面向毕业设计场景的场地预约小程序完整项目基于微信小程序SSMMySql技术栈开发适合计算机相关专业学生作为选题参考、功能扩展或二次开发起点。系统实现场地类型管理、场地管理、预约管理、取消申请与押金退回等功能分为管理员和用户双角色覆盖从查询场地、在线预约、支付费用到使用场地、退回押金的完整闭环流程解决传统体育馆现场预约费时费力的问题。压缩包共936个文件约42.64MB以vue前端页面、java后端逻辑、SQL数据库脚本、mp4演示视频及doc论文文档为主附含开题报告、毕业论文和视频演示可直接用于答辩准备与功能演示。已有291人学习下载资源目录结构清晰便于按源码、数据库、文档模块检索学习同时提供运行脚本简化环境配置省去从零搭建的时间成本。1. 场地预约小程序到底要做什么一个毕设题目的真实拆解毕设题目定了“场地预约小程序”之后很多人第一反应是“不就是做个预约吗”。直到开始拆需求才发现这个题目同时牵扯三块东西微信小程序要管用户点选和页面跳转SSM 要做后端接口和业务规则MySQL 要把场地、场次、预约记录全部落库。三块之间还夹着并发冲突、场次锁定、管理员审核这些躲不掉的细节。这篇笔记按“先画表结构再写后端再写小程序最后集中排坑”的顺序展开适合有 Java 基础、正拿这个题目准备毕设答辩或者想用一个完整案例把 SSM 和小程序串起来的开发者参考。2. 整体架构与数据库设计先画表结构再写接口动手写代码前先把表结构定下来。SSM 项目改接口比改数据库省事得多数据库设计一旦返工Mapper、Service、页面全都得跟着动。开题报告里的技术路线图通常也是按这套架构画的用户在小程序端发起请求后端 SSM 接收并处理数据最终落到 MySQL。2.1 技术栈职责边界小程序、SSM、MySQL 各管什么小程序是纯粹的“壳”只负责页面渲染和用户交互它不能直连数据库。所有数据都要通过 wx.request 发 HTTP 请求到后端接口再把返回的 JSON 渲染到页面上。这意味着后端接口的数量和语义直接决定小程序端的开发量。接口设计得乱小程序页面写起来就很痛苦。SSM 是三个框架的组合Spring 统一管理对象和事务SpringMVC 负责把 URL 映射到 Controller 方法MyBatis 负责把 Java 方法调用翻译成 SQL。三者串起来以后业务上要做的事就变成了“接收参数—校验—写库—返回结果”。MySQL 是最底层的数据存储场地信息、场次安排、预约记录、用户身份都存放在这里。那这个题目为什么不用 SpringBoot常见原因是高校课程和毕设模板对 SSM 更熟悉答辩时老师问“Spring 跟 SpringMVC 各负责什么”能指着项目一层层讲清楚这本身就是加分项。如果只是自己私下练手换 SpringBoot 没问题如果是交毕设按学校的既定技术栈走更稳不要为了追新给自己埋雷。2.2 四张核心表场地、场次、预约、用户怎么设计这类系统最少需要四张表用户表存微信用户身份场地表存场地基础信息场次表存“某天某个时间段”的预约单元预约表记录谁在哪个场次约了什么场地。表名关键字段说明userid, openid, nickname, rolerole 用 0 表示普通用户1 表示管理员courtid, name, location, image_url, description, statusstatus 用 1 启用、0 停用slotid, court_id, date, start_time, end_time, statusstatus 用 0 可约、1 已锁定bookingid, booking_no, user_id, slot_id, court_id, status, create_timestatus 用 0 待审核、1 已确认、2 已取消、3 已完成为什么要把场次单独拆一张表而不是在预约表里直接存开始时间和结束时间因为一个场地一天会分成多个固定时间段把时间段独立成表之后场地列表接口可以直接查“某天还有哪些场次可约”预约表只需要记录 slot_id 就行查询路径会短很多。booking 表里冗余了一个 court_id目的是让“我的预约”页面少做一次 join这个字段多占一点空间但换来的查询效率是值得的。字段类型上所有主键都用 BIGINT 自增不要用 VARCHAR 当主键自增主键在索引维护和分页查询上都有优势。openid 是微信用户的唯一标识必须加唯一索引因为同一用户重复登录时不能产生两条用户记录。2.3 主键、外键和预约状态容易被答辩老师追问的细节物理外键在毕设项目里不建议建。原因很实际删除场地或用户时物理外键会强制约束导致很多操作要按特定顺序执行而常见做法是用逻辑外键也就是在子表里保留父表的 id 字段user_id、court_id、slot_id然后给这些字段建普通索引。ER 图里仍然能画出表间关系代码里却不用受数据库级约束牵制。预约状态这个小字段特别容易被忽视但它恰恰是答辩时老师最喜欢追问的点。建议用数字状态0 待审核、1 已确认、2 已取消、3 已完成。取消从来不是真的删除记录而是把状态改成 2这样论文里能写“预约状态流转”统计已取消数量也方便。代码层可以定义一个常量类或枚举把这些状态值映射成有意义的名称避免到处写魔法数字。为什么不直接存“pending”“confirmed”这类字符串字符串可读性确实好但存储空间更大而且写错一个字母就很难查出来。用 tinyint 存数字配合代码里的常量说明兼顾可读性和稳定性。数据库排序上数字也比字符串更直接。2.4 初始化 SQL 与演示数据第一次启动就能看到完整效果初始化脚本要一次跑通演示数据要覆盖两个场地、两个用户、未来几天的场次否则小程序首页打开就是空白后面所有调试都会卡在“没数据可看”这一步。下面这份 SQL 是我常用的初始版本CREATE DATABASE court_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE court_system; CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信 openid, nickname VARCHAR(32) DEFAULT , role TINYINT NOT NULL DEFAULT 0 COMMENT 0普通用户1管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB; CREATE TABLE court ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, location VARCHAR(100) DEFAULT , image_url VARCHAR(200) DEFAULT , description VARCHAR(255) DEFAULT , status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用0停用, PRIMARY KEY (id) ) ENGINEInnoDB; CREATE TABLE slot ( id BIGINT NOT NULL AUTO_INCREMENT, court_id BIGINT NOT NULL, date DATE NOT NULL, start_time VARCHAR(5) NOT NULL COMMENT 如 09:00, end_time VARCHAR(5) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0可约1已锁定, PRIMARY KEY (id), KEY idx_court_date (court_id, date) ) ENGINEInnoDB; CREATE TABLE booking ( id BIGINT NOT NULL AUTO_INCREMENT, booking_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, slot_id BIGINT NOT NULL, court_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审核1已确认2已取消3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_slot_user (slot_id, user_id) ) ENGINEInnoDB; -- 演示数据 INSERT INTO user (openid, nickname, role) VALUES (demo_openid_admin, 管理员, 1), (demo_openid_user, A同学, 0); INSERT INTO court (name, location, description) VALUES (羽毛球馆 1 号场, 综合楼 3 层, 标准羽毛球场地), (羽毛球场 2 号场, 综合楼 3 层, 靠近窗边光线较好); INSERT INTO slot (court_id, date, start_time, end_time) VALUES (1, DATE_ADD(CURDATE(), INTERVAL 1 DAY), 09:00, 10:00), (1, DATE_ADD(CURDATE(), INTERVAL 1 DAY), 10:00, 11:00), (2, DATE_ADD(CURDATE(), INTERVAL 1 DAY), 09:00, 10:00);这段脚本里有几个点值得说明。字符集用 utf8mb4不只是为了中文而是为了兼容四字节字符。日期不写死而是用 DATE_ADD(CURDATE(), INTERVAL 1 DAY) 生成明天这样无论哪一天初始化页面都能看到未来可约的场次演示时不用手动改数据。booking 表加了一个 UNIQUE KEY uk_slot_user是一道兜底防线同一个用户对同一个场次不能重复预约后面并发处理会再次提到它。3. 后端 SSM 怎么落地场地查询与预约提交的关键实现后端是整套系统的业务核心。这一章按“配置 → 查询 → 提交 → 权限”的顺序写全程围绕最小可运行的项目来组织代码不给多余的封装。3.1 SSM 分层结构和三个关键配置文件SSM 项目从上到下分三层Controller 接收请求并返回 JSONService 写业务规则Mapper 负责 SQL。答辩老师问“一个请求进来怎么走的”回答顺序就是 页面 → Controller → Service → Mapper → 数据库然后逆向返回。先看三个最关键的配置。spring-mvc.xml 里要开注解驱动、扫描 Controller、配置静态资源映射其中静态资源映射是为了让上传的场地图片能通过 URL 访问context:component-scan base-packagecom.court.controller/ mvc:annotation-driven/ mvc:resources mapping/upload/** locationfile:D:/court-upload//mybatis-config.xml 里最实用的是驼峰映射开关打开以后数据库的 create_time 能自动映射到 Java 属性 createTime省去一大把 resultMapconfiguration settings setting namemapUnderscoreToCamelCase valuetrue/ /settings /configuration数据库连接配置放在 jdbc.properties注意 MySQL 8 的参数一个都不能少jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/court_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456url 里的 serverTimezone 指定时区useSSL 关掉避免本地环境握手报错allowPublicKeyRetrieval 是为了兼容 MySQL 8 的认证方式。第一个出问题的往往是最后这个参数后面排坑章节会再讲。3.2 场地列表接口从 Controller 到 Mapper 的一次完整调用场地列表是首页最主要的接口目标是把启用的场地按分页返回给小程序。Controller 只做参数接收和结果包装RestController RequestMapping(/api/court) public class CourtController { Autowired private CourtService courtService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { return Result.ok(courtService.pageQuery(page, size)); } }Service 层调用分页插件并返回 PageInfo 给 ControllerService public class CourtServiceImpl implements CourtService { Autowired private CourtMapper courtMapper; Override public PageInfoCourtVO pageQuery(Integer page, Integer size) { PageHelper.startPage(page, size); ListCourtVO list courtMapper.selectCourtList(); return new PageInfo(list); } }Mapper 接口只有一行方法真正干活的是对应的 XMLpublic interface CourtMapper { ListCourtVO selectCourtList(); }select idselectCourtList resultTypecom.court.vo.CourtVO SELECT id, name, location, image_url, description FROM court WHERE status 1 ORDER BY id DESC /select这里有一个细节查询返回的是 CourtVO 而不是 Court 实体。VO 只带页面需要的字段不会把 status 这类内部字段直接暴露给前端后期如果要在列表里加“今日剩余场次数”直接在 VO 里扩字段就行。PageHelper 的 startPage 必须紧跟第一条查询语句中间不能有其他数据库操作否则分页会串到别的查询上。3.3 预约提交与冲突检测用一条 UPDATE 抢占场次预约提交是整个项目里最核心的业务。表面上是“插入一条预约记录”但背后要处理并发问题两个人同时提交同一个场次不能两个都成功。常见做法是在 Service 层加 Transactional 事务把检查场次、锁定场次、写入预约这三步包在同一个事务里Transactional(rollbackFor Exception.class) public BookingVO submitBooking(BookingRequest req) { // 1. 校验场次存在且可约 Slot slot slotMapper.selectById(req.getSlotId()); if (slot null || slot.getStatus() ! 0) { throw new BizException(该场次不可预约); } // 2. 抢占场次把 status 置为 1防止并发下重复预约 int rows slotMapper.lockSlot(req.getSlotId()); if (rows 0) { throw new BizException(手慢了场次已被抢走); } // 3. 写入预约记录初始状态为待审核 Booking booking new Booking(); booking.setUserId(req.getUserId()); booking.setSlotId(req.getSlotId()); booking.setCourtId(slot.getCourtId()); booking.setStatus(0); bookingMapper.insert(booking); return buildVO(booking); }对应的 lockSlot 是整段代码的灵魂update idlockSlot UPDATE slot SET status 1 WHERE id #{slotId} AND status 0 /update这条 UPDATE 利用数据库行锁把“检查是否可约”和“锁定场次”合并成了一步。两个请求同时进来数据库层面会串行执行第一个请求更新成功返回 1第二个请求再执行时 status 已经不是 0影响行数为 0事务回滚返回“手慢了”。这里不要用先 SELECT 再 INSERT 的方式因为两条语句之间有时间窗口并发下必然会出现重复预约。3.4 管理端接口用拦截器控制管理员权限管理端接口涉及审核、取消、统计这些接口不能只靠前端隐藏按钮来保护。常见做法是写一个拦截器拦截所有 /api/admin/ 开头的请求从请求头里取 token解析出用户 id再查数据库确认角色是管理员public class AdminAuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(token); Integer userId TokenUtil.parseUserId(token); if (userId null) { throw new BizException(登录已过期请重新登录); } User user userMapper.selectById(userId); if (user null || user.getRole() ! 1) { throw new BizException(没有管理员权限); } return true; } }然后在 spring-mvc.xml 里注册拦截规则mvc:interceptors mvc:interceptor mvc:mapping path/api/admin/**/ bean classcom.court.interceptor.AdminAuthInterceptor/ /mvc:interceptor /mvc:interceptors普通用户接口不在拦截范围内因为小程序首页、场地列表、提交预约这些操作只要登录就能访问。拦截器只保护管理操作token 在小程序启动时通过 wx.login 换取后端拿到 code 后向微信接口换 openid再生成自定义 token 返回给小程序。这个流程写成图放在论文里能直接当“系统登录时序图”用。4. 微信小程序端页面结构、角色切换和预约流程小程序端的目标是让用户能在手机上完成“看场地—选场次—提交预约—查看状态”的完整闭环同时让管理员有一个简单的审核入口。4.1 小程序目录结构与 tabBar页面怎么划分小程序页面按功能拆成四个首页展示场地列表预约页负责选场次和提交我的页面展示个人预约列表管理页做审核和场地维护。前两个页面放 tabBar后两个用普通页面跳转进入{ pages: [pages/index/index, pages/my/my], tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/my/my, text: 我的 } ] } }预约页不放到 tabBar因为它需要携带 courtId 参数进入不同场地跳转到同一个预约页再动态加载场次这是最省代码的做法。管理页同样做成普通页面从“我的”页面里根据角色判断后显示入口。4.2 预约提交页日期选择、场次选择、提交校验预约页的核心逻辑是进入页面先加载未来七天的日期列表默认选第一天日期变化后重新请求该日期的场次列表选中场次后点提交调用后端接口。日期和场次分开加载避免一次性拉回大量数据Page({ data: { courtId: null, dateList: [], slotList: [], selectedDate: , selectedSlotId: null }, onLoad(options) { this.setData({ courtId: options.courtId }) this.loadDateList() this.loadSlotList() }, onDateChange(e) { this.setData({ selectedDate: e.detail.value, selectedSlotId: null }) this.loadSlotList() }, loadSlotList() { wx.request({ url: ${app.globalData.baseUrl}/api/slot/list, data: { courtId: this.data.courtId, date: this.data.selectedDate }, success: (res) { this.setData({ slotList: res.data.data }) } }) }, submit() { if (!this.data.selectedSlotId) { wx.showToast({ title: 请先选择场次, icon: none }) return } wx.request({ url: ${app.globalData.baseUrl}/api/booking/submit, method: POST, header: { token: app.globalData.token }, data: { slotId: this.data.selectedSlotId }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 预约成功 }) wx.navigateBack() } else { wx.showToast({ title: res.data.msg, icon: none }) } } }) } })baseUrl 放在 app.js 的 globalData 里本地联调用 http://localhost:8080真机调试时改成电脑的局域网 IP这样环境切换只改一个文件。提交按钮前端只做“是否选中场次”的空值校验真正的权限、场次状态、并发冲突都交给后端处理前端校验只是体验层的提示。4.3 我的预约与取消状态展示与按钮逻辑“我的预约”列表按状态展示文案0 待审核、1 已确认、2 已取消、3 已完成。取消按钮要按状态控制已经取消或已经完成的预约不能再次操作button wx:if{{item.status 0 || item.status 1}} bindtapcancelBooking >view wx:if{{userInfo.role 1}} button bindtapgoAdmin管理入口/button /view管理页分成两个区块待审核预约列表和场地管理。审核操作就是调后端审核接口传 bookingId 和审核结果后端把状态改成 1 或 2。场地管理复用场地列表接口加上新增、停用、编辑三个操作。这里的关键是小程序端的管理入口只是 UI 层面的隐藏真正防止越权的是后端拦截器也就是 3.4 节做的那个拦截。5. 从环境到部署场地预约小程序避坑指南与最常见的 5 个坑这一章写实际开发里最容易翻车的 5 个点每一条都按“现象 → 原因 → 解决”来写很多坑不是你逻辑写错而是环境和配置的问题。5.1 启动秒退Tomcat 端口被占用现象Tomcat 启动后控制台直接报 BindException或 IDE 提示端口被占用。原因大多是上一次运行没有正常停止或者是本机其他程序占用了 8080。解决分两种先查占用进程再决定是杀进程还是换端口。netstat -ano | findstr 8080 taskkill /PID 12345 /F如果 8080 被系统服务占用不方便杀也可以直接改 Tomcat 的 server.xml把 Connector 的 port 改成 8081。注意改完以后小程序端的 baseUrl 也要同步改成 8081否则页面全部请求失败。这个坑的频率极高但定位起来也最快。5.2 MySQL 8 连接报“Public Key Retrieval is not allowed”现象项目启动时数据源初始化报错控制台出现 Public Key Retrieval is not allowed。原因MySQL 8 默认使用 caching_sha2_password 认证JDBC 驱动第一次连接时需要从服务器获取公钥。解决方式是在连接串上补参数jdbc.urljdbc:mysql://localhost:3306/court_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue这里 allowPublicKeyRetrievaltrue 是关键。注意这个报错只在部分 MySQL 8 版本中出现如果本地装的是 MySQL 5.7 或更早版本一般不会遇到。出现这个错先别急着改代码核对一下驱动版本和连接参数就够了。5.3 真机连不上本地后端小程序 request 一直 fail现象微信开发者工具里所有接口正常点“真机调试”后全部请求失败报错提示不是 url not in domain list 就是请求超时。原因开发者工具允许访问 localhost但手机上的小程序没有这个概念必须通过局域网 IP 访问电脑上的后端。解决方法是把 baseUrl 从 http://localhost:8080 改成 http://电脑局域网IP:8080/项目名并在开发者工具的“详情 → 本地设置”里勾选“不校验合法域名”。开发阶段这个配置是常规操作。真正上线前再按平台的 HTTPS 域名要求处理那是另一个阶段的事。5.4 并发抢同一场次数据库出现重复预约现象两个同学同时点同一个场次的“提交预约”数据库里出现两条相同 slot_id 的预约记录。原因先 SELECT 判断再 INSERT 的做法存在时间窗口两个请求可能同时通过检查。解决方式就是 3.3 写的 lockSlot 方案用一条 UPDATE 抢占场次。如果项目已经写成了先查后插不用推倒重来把这两步替换成 UPDATE 抢占再把预约插入放到事务里就行。booking 表上的唯一索引 uk_slot_user 可以作为兜底防线即使在极端情况下业务锁失效数据库这一层也会拦住重复数据。5.5 场地图片 404静态资源映射没配现象数据库里 image_url 存的是 /upload/xxx.jpg页面图片却显示不出来浏览器直接访问也是 404。原因Tomcat 不会把磁盘上的任意目录当成静态资源图片文件虽然上传到了本地磁盘但对外没有对应的 URL 映射。解决方式在 spring-mvc.xml 里加一行资源映射mvc:resources mapping/upload/** locationfile:D:/court-upload//location 对应你实际保存图片的目录Windows 下写法是 file:D:/court-upload/Linux 下是 file:/home/court/upload/结尾斜杠不能丢。改完配置重启 Tomcat图片立刻能访问。这个坑几乎每个人都会踩一次因为本地开发时文件明明存在容易忽略 URL 映射这一环。6. 从代码到毕业论文验证方法、演示顺序和值得深挖的扩展项目代码写完后还有开题报告、毕业论文和视频演示要交。这一章把验收阶段的三个关键事项一次说清楚。6.1 演示路线怎么排先用户端、再管理端、最后看数据库视频演示和答辩现场演示顺序都很重要。我最常推荐的路线是打开小程序首页展示场地列表 → 选一个未来场次提交预约 → 切到管理员账号审核通过 → 切回用户端看到状态变成已确认 → 最后打开数据库客户端执行 SELECT * FROM booking现场展示这条预约记录和状态字段。这一轮走完前端、后端、数据库三个层面都有画面老师想看的“数据是怎么流进去的”也有了着落。演示前记得用“未来日期”的场次提前准备一个已完成的场次放在列表末尾用来展示不同状态的界面差异。6.2 并发抢场地的验证方法两个账号同时提交并发是这套毕设最容易讲出亮点的地方验证方法也不复杂准备两个账号在两个窗口或两台手机同时点同一个场次的提交按钮。观察返回结果应该只有一个成功另一个提示“手慢了”。把这个过程录进视频演示再配一句解释“这里用 UPDATE 影响行数来保证同一场次只被抢到一次”直接解决了论文里“系统如何处理并发预约”这一节的素材问题。如果测试时发现两个都成功了说明用的还是先查后插的老写法按 5.4 的方法改掉。6.3 从毕设到可运营系统还差哪几块拼图如果论文写完之后还想往“可上线”的方向走一步需要补的东西集中在三块把场次规则做成可配置支持周末、节假日不同时段把数据库锁换成分布式锁支撑多实例部署加一个预约成功后的通知机制。但注意毕设的核心是“把主流程讲清、把状态流转讲闭合”扩展方向列在论文末尾的展望里就够了不要把大量篇幅花在还没实现的分布式方案上。我的习惯是开工前先在一页纸上把三张状态图画出来预约状态流转、场次状态流转、用户角色关系项目做完再回头对照基本不会跑偏。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?