ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微信小程序场地预约系统:状态机建模与时间冲突检测实战

2026/9/12 22:02:18 拓冰建站 浏览量
微信小程序场地预约系统:状态机建模与时间冲突检测实战 简介一份面向微信小程序毕业设计/课程设计场景的场地预约系统完整开发项目前端由微信小程序承担后端基于Java与MySQL覆盖管理员与用户双角色管理员可进行场地类型、场地、用户、预约、取消申请、退回押金等管理操作用户可查询场地、预约支付、使用场地、退回押金及取消预约并包含场地公告功能体系贴近真实业务。资源包共1234个文件约102.7MB主要包含Java后端业务代码、Vue管理端页面、小程序WXML/WXSS页面与JS逻辑、PNG界面素材、SQL数据库脚本以及JSON、XML配置等另附bat批处理脚本便于一键安装、构建与运行目录结构规整便于定位前后端各模块。内置演示视频与说明文档可快速掌握从数据库设计到小程序交互的完整链路适合正在筹备毕业设计、课程设计或想学习全栈案例的开发者参考。目前已有114人学习浏览具备一定的实践参考价值。1. 微信小程序场地预约系统先把完整业务闭环跑明白「微信小程序 场地预约系统」这类题目在毕业设计里出现频率很高但多数实现只停留在“查场地 下单”两步后台能干预的很少。这个项目把流程做满了用户端走完查询场地、预约场地、支付费用、确认使用、退回押金、取消预约六个动作管理员端覆盖场地类型、场地信息、用户、预约记录、取消申请、退押金六类管理还带场地公告。技术栈是微信小程序开发工具 Java 后端 MySQL资源包里含源码、演示视频和说明文档。做毕业设计或课程设计的可以直接拿这套改想理解“预约类业务怎么建模、状态怎么流转”的开发者也能从源码里看到完整答案。2. 技术栈拆解小程序端、Vue 管理后台与 Java 后端的分工2.1 用户端、管理端与后端“一项目三端”的结构由来从源码文件看这不止是一个小程序项目而是三端协同用户端是微信小程序管理员端是一套 Vue 写的 Web 后台源码里大量 .vue 文件后端是 Eclipse 管理的 Java 工程.classpath 文件是 Eclipse 工程标记。三个端共用同一个 MySQL 库职责划分很清楚——小程序只做 C 端查询与操作管理后台处理审核类功能Java 后端暴露 REST 接口。选这套组合的原因很实际。微信小程序开发工具官方文档全原生组件对场地列表、日期选择这类交互够用Java 后端生态成熟SSM 或 Spring Boot 都跑得起来答辩时讲三层架构也很好讲MySQL 5.7 对预约这类中小并发场景完全够。不需要引入微服务、消息队列、Redis 缓存这些重组件否则光环境搭建就能消耗掉大量时间反而把核心业务淹没了。2.2 .classpath 和 .vue.bak 透露的项目组织方式.bak 后缀文件很能说明问题。main.css.bak、update-password.vue.bak、IndexMain.vue.bak、IndexAsideStatic.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak这些都是改之前留的备份。开发者的习惯是先复制一份再动手改挂了直接用原备份覆盖回去。这个习惯在改管理后台样式时尤其好用改到自己都不记得动了哪几行时diff 一下就能定位。.classpath 是 Eclipse Java 工程的核心配置文件记录源码目录、输出目录和依赖库路径。拿到项目后直接用 Eclipse 导入即可省去手动配 build path 的步骤。如果习惯用 IDEA也可以对照 .classpath 里的依赖项手动建 Maven 工程依赖坐标清晰的话一小时内就能迁移完成。2.3 三个批处理脚本install / run / build 分别是做什么的源码包里三个 .bat 文件把项目的自动化流程串起来了。1-install.bat 负责装依赖2-run.bat 负责启动3-build.bat 负责构建打包。下面是一个与常见毕业设计项目结构匹配的 install 脚本写法echo off setlocal echo [1/3] install backend dependencies... call mvn clean install -DskipTests if errorlevel 1 goto :fail echo [2/3] install admin web dependencies... cd /d %~dp0..\admin-web call npm install if errorlevel 1 goto :fail echo install all done pause exit /b 0 :fail echo install failed pause exit /b 1脚本逻辑不复杂但有几点值得学习。errorlevel 1判断前一条命令是否返回非零值任何一步失败就直接退出避免在残缺环境下继续执行。%~dp0取当前脚本所在目录保证脚本在任意路径双击运行都能找到相对路径。mvn clean install -DskipTests把 Java 后端依赖装进本地仓库并跳过测试打包npm install装 Vue 管理后台依赖。对应地2-run.bat 一般负责启动 MySQL 服务和后端进程3-build.bat 负责把管理后台构建产物npm run build 出来的 dist同步到 Tomcat 的 webapps 目录下。3. 数据库表与预约状态机先建模再写业务代码3.1 六张表把业务拆干净核心建表语句按业务流拆这个项目大约有六张核心表场地类型表 venue_type、场地表 venue、用户表 user、预约表 reservation、取消申请表 cancel_apply、押金退回表 deposit_refund另加一张公告表 notice。场地类型和场地拆开的原因很简单一个类型比如羽毛球下有多块场地1 号场、2 号场分表后场地类型改名只动一条记录不涉及场地数据批量更新。CREATE TABLE venue_type ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 羽毛球、篮球等类型名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 场地类型表; CREATE TABLE venue ( id INT PRIMARY KEY AUTO_INCREMENT, type_id INT NOT NULL COMMENT 关联 venue_type.id, name VARCHAR(100) NOT NULL COMMENT 场地名称/编号, location VARCHAR(255) COMMENT 楼层或区域描述, price DECIMAL(10,2) NOT NULL COMMENT 场地使用费, deposit DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 押金金额, status TINYINT NOT NULL DEFAULT 1 COMMENT 1开放预约 0停用, description VARCHAR(500) ) COMMENT 场地表; CREATE TABLE reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, venue_id INT NOT NULL, book_date DATE NOT NULL COMMENT 预约日期, start_time VARCHAR(5) NOT NULL COMMENT 开始时段如 14:00, end_time VARCHAR(5) NOT NULL COMMENT 结束时段如 15:00, total_fee DECIMAL(10,2) NOT NULL COMMENT 场地费用, deposit_amt DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 押金, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付, use_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未使用 1已确认使用, refund_status TINYINT NOT NULL DEFAULT 0 COMMENT 0无需退 1可退 2退款中 3已退, cancel_status TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1取消申请中 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 预约记录表;几点说明金额一律用 DECIMAL不推荐 float因为浮点数对精度敏感涉及退款时一分钱差异都难查时间用 DATE 加 VARCHAR(5) 组合比用 DATETIME 存完整时间更直观字符串比较也能判断先后但格式必须统一成两位小时和分钟reservation 表把支付、使用、退款、取消四个状态拆成独立字段和单字段状态机相比好处是每个维度可以独立统计比如“已支付但未使用”的订单能直接 query 出来做催场提醒。3.2 预约状态机状态分拆比一个 status 字段走得远很多预约系统喜欢用一个 status 字段从 0 走到 9刚开始写起来快后面加需求就乱。这个项目把状态按四个维度拆开每个维度各自流转互不干扰。这样做的好处是查询粒度细比如统计“待退押金但已使用”的单子一条 where 就完事。阶段触发动作状态字段变化操作角色待支付用户提交预约pay_status0用户已支付待使用用户支付成功pay_status1用户已使用用户到场核销use_status1用户/管理员退款中用户申请退押金refund_status2用户已完成管理员同意退押金refund_status3管理员取消申请中用户提交取消cancel_status1用户已取消管理员审核通过cancel_status2管理员注意“待支付”和“取消申请中”是两个不同维度的状态一个订单可以同时存在。比如用户支付前想取消直接关闭订单即可不需要提交取消申请但支付后想取消就必须进入取消申请流程由管理员判断是否退还费用。编码时建议为每个状态维度建枚举类把魔法数字挡在业务代码之外。3.3 取消申请与退押金为什么必须走审核取消预约放在独立表而不是直接改 reservation是为了留审计记录。cancel_apply 表存申请原因、申请时间、审核时间、审核结果管理员看到的是申请上下文而不是一条干巴巴的状态变更。退押金的边界条件是 use_status1 且 refund_status 处于可退状态没使用过就退款使用过且无损坏则退全款有损坏记录时管理员在退押金管理功能里修改退款金额并备注原因。Transactional(rollbackFor Exception.class) public boolean applyDepositRefund(Long reservationId, Long userId) { Reservation r reservationMapper.selectById(reservationId); if (r null || !r.getUserId().equals(userId)) { return false; } // 只有已使用且未退款、未申请退款的订单才能发起退押金 if (r.getUseStatus() ! USE_STATUS_USED || r.getRefundStatus() ! REFUND_STATUS_NONE) { return false; } r.setRefundStatus(REFUND_STATUS_PENDING); reservationMapper.updateById(r); depositRefundMapper.insert(new DepositRefund(r.getId(), userId, r.getDepositAmt())); return true; }这段逻辑保护了两个边界没有使用过场地不能退押金已经提交过退款申请的不能重复提交。事务注解保证状态更新和退款记录插入要么同时成功要么同时回滚。4. 核心接口与代码落地从时间冲突检测到请求封装4.1 时间冲突检测SQL 里最容易写错的重叠判断创建预约第一步不是落库是查冲突。用户选了 14:00-15:00 的 2 号羽毛球场地后端要判断同一日期该场地是否有预约与之重叠。判断逻辑用NOT (end_time #{startTime} OR start_time #{endTime})这组条件。换成大白话就是已有预约的结束时间不早于新预约开始时间且已有预约的开始时间不晚于新预约结束时间两者同时成立才算时间撞上。看似简单翻车率却很高很容易写成end_time startTime OR start_time endTime这种恒真的表达式。SELECT COUNT(*) FROM reservation WHERE venue_id #{venueId} AND book_date #{bookDate} AND cancel_status 0 AND pay_status 1 AND NOT (end_time lt; #{startTime} OR start_time gt; #{endTime})MyBatis 的 XML 里写需要转义成lt;不少人在这里报语法错误。cancel_status 0排除已取消的预约pay_status 1只统计已支付订单未支付订单默认 15 分钟过期由定时任务把超时单取消。后端 Service 在事务里先查后插把同一场地的创建请求串行化处理Transactional(rollbackFor Exception.class) public Long createReservation(ReservationCreateDTO dto) { // 查询场地并校验开放状态 Venue venue venueMapper.selectById(dto.getVenueId()); if (venue null || venue.getStatus() ! 1) { throw new BizException(场地不存在或已停用); } // 时间冲突校验conflict 大于 0 说明重叠 int conflict reservationMapper.countConflict( dto.getVenueId(), dto.getBookDate(), dto.getStartTime(), dto.getEndTime()); if (conflict 0) { throw new BizException(该时段已被预约); } Reservation r new Reservation(); r.setUserId(dto.getUserId()); r.setVenueId(dto.getVenueId()); r.setBookDate(dto.getBookDate()); r.setStartTime(dto.getStartTime()); r.setEndTime(dto.getEndTime()); r.setTotalFee(venue.getPrice()); r.setDeposit(venue.getDeposit()); r.setPayStatus(0); reservationMapper.insert(r); return r.getId(); }setPayStatus(0)是让订单先落到待支付支付完成后由支付回调把状态翻成已支付。这样设计的好处是未支付订单不会占用场地资源太久超时释放逻辑只需要扫 pay_status0 且 create_time 早于 15 分钟前的记录。4.2 小程序端 wx.request 封装统一处理登录失效与错误码小程序端所有接口调用都走 wx.request如果不做封装每个页面都要重复写 header、重复处理 401、重复弹 toast。常见的做法是抽一个 request.js把公共逻辑收敛起来// utils/request.js const BASE_URL http://localhost:8080/api; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: Bearer wx.getStorageSync(token) }, success(res) { if (res.statusCode 401) { // 登录失效跳回登录页 wx.navigateTo({ url: /pages/login/index }); reject(res); } else if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request, BASE_URL };封装之后业务页面只需要关心数据不需要关心错误弹窗和登录态跳转。后端返回结构统一成{ code, msg, data }code 为 0 表示成功非 0 或 HTTP 状态码 4xx 都按失败处理。页面里查询场地列表的代码就变得很薄const { request } require(../../utils/request); Page({ data: { venueList: [], typeId: 0 }, onLoad() { this.loadVenues(); }, loadVenues() { request(/venue/list?typeId this.data.typeId) .then(list this.setData({ venueList: list })); } });4.3 管理后台接口的路由设计角色业务接口路径说明用户查询场地列表GET /api/venue/list支持按类型筛选用户创建预约POST /api/reservation/create带时间冲突校验用户模拟支付POST /api/reservation/pay把 pay_status 置 1用户确认使用POST /api/reservation/useuse_status 置 1用户申请退押金POST /api/reservation/deposit-refund生成退款记录用户取消申请POST /api/reservation/cancel-apply生成取消申请管理员取消申请审核POST /api/admin/cancel-audit通过或拒绝管理员退押金管理POST /api/admin/refund-audit修改金额并退款后端用 Controller → Service → Mapper 三层小项目不用过度设计两层或三层都行。关键是把返回结构统一小程序端封装直接依赖这个格式。管理员接口独立在 /api/admin 前缀下配合拦截器做角色鉴权用户接口只允许用户角色访问这个设计在答辩时很加分。5. 上线前要处理的几个细节并发、域名校验与备份还原5.1 并发重复预约业务校验之外再加一把锁两个用户同时提交同一时段预约请求同时进入后端业务校验可能都通过数据库就出现重复记录。上一章代码在单机演示时够用但要在并发场景下立住需要在事务里对场地行加锁。SELECT * FROM venue WHERE id #{venueId} FOR UPDATE锁住场地记录后同一场地的创建预约请求会排队执行前一个事务提交后后一个才能执行冲突检测。注意锁的是 venue 行而不是 reservation 行因为同一场地的创建都在同一条 venue 行上排队才能真正互斥。MyBatis 中给查询加forUpdate()方法即可Spring 事务下锁会在提交后自动释放。5.2 开发调试阶段的域名校验问题微信开发者工具里跑通真机预览前有一个极易踩坑的点wx.request 的 url 不是合法域名时会直接 fail。调试阶段在「详情 - 本地设置」勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」就能让开发工具访问 http://localhost:8080 或局域网 IP。真机预览时手机和电脑要在同一局域网后端服务监听地址改成 0.0.0.0小程序请求里写电脑的局域网 IP。演示视频里大概率就是这种局域网联调方式如果换网段后手机连不上优先检查 Windows 防火墙是否放行了后端端口。5.3 用 diff 确认改了什么用 cp 快速还原源码包里的 .bak 文件还有一个用处对比改动。比如 update-password.vue.bak 和 update-password.vue 只差几行用 diff 命令能精确看出改了什么排查问题时比盲猜高效很多。diff update-password.vue.bak update-password.vue确认改动有误时把 .bak 覆盖回去就能还原cp update-password.vue.bak update-password.vue这个技巧对管理后台的样式调整尤其有用。改完发现布局乱了先 diff 看差异再决定手动回滚还是直接 cp 覆盖。本文还有配套的精品资源点击获取