微信小程序+SpringBoot实现的麻将馆在线预约与后台管理全套源码

微信小程序+SpringBoot实现的麻将馆在线预约与后台管理全套源码 本文还有配套的精品资源点击获取简介直接可用的麻将馆预约系统源码后端用SpringBoot开发前端是微信小程序数据库用MySQL配套完整建表脚本mahjong.sql含用户注册登录、门店列表展示、预约时段选择、订单生成与状态跟踪、管理员后台管理等核心功能。项目结构规范包含标准Maven配置pom.xml、Spring Boot配置文件、分层Java业务代码controller/service/mapper、JUnit单元测试、Dockerfile容器化支持SQL脚本已预置表结构和初始测试数据导入即可运行。适合学生做毕业设计或课程大作业也适合作为Java Web与小程序全栈开发的实操参考。附带零基础入门指南压缩包覆盖JDK/Maven/IDEA/微信开发者工具环境搭建、小程序对接SpringBoot接口、MyBatis操作MySQL等关键步骤所有代码经过本地调试验证无需二次修改就能启动运行。1. 这不是又一个“Hello World”项目为什么麻将馆预约系统是全栈学习的黄金切口你可能已经刷过几十个“SpringBootVue图书管理系统”“SpringBootReact在线商城”的教程但真正能让你在简历上写“独立完成过生产级小程序后端”的项目少之又少。而这个麻将馆预约系统恰恰卡在了一个极微妙、极真实的平衡点上——它足够轻量学生两天就能跑通又足够完整覆盖了从用户扫码进店、选时段、付定金、到店核销、管理员排班、数据看板的全业务闭环。我带过三届计算机专业毕设每年都有至少5个学生选“预约类系统”但90%的人卡在“怎么让小程序和后端真正连上”“怎么把数据库字段映射成前端可读的中文状态”“为什么明明写了拦截器游客还是能访问订单页”这种细节里。这个项目之所以能“开箱即用”不是因为删减了功能而是把那些没人愿意写的“胶水代码”全给你焊死了比如微信登录态如何安全透传给SpringBoot不是简单存session而是用JWTRedis双校验比如小程序里“上午/下午/晚上”三个时段后端怎么对应到数据库里精确到分钟的时间段区间start_time和end_time字段设计再比如管理员后台导出Excel时如何把订单状态码status2自动翻译成“已到店”而不是让前端硬编码一堆if-else。关键词里的“麻将馆预约”听着小众实则是个极佳的领域建模训练场——它天然包含空间门店、时间时段、资源包间、角色普通用户/店员/管理员、状态流转待支付→已预约→已到店→已完成五大要素比“图书借阅”更贴近真实商业逻辑。而“微信小程序”这个前端载体逼你直面移动端特有的限制没有cookie、本地存储上限30MB、网络请求必须HTTPS、用户身份强依赖微信OpenID。这些不是教科书里的概念是你调试时看着控制台报错request:fail net::ERR_CONNECTION_REFUSED抓耳挠腮的真实战场。所以别被“麻将”二字劝退它本质是一个经过高度提炼的“本地生活服务SaaS最小可行模型”你学到的不是怎么开麻将馆而是怎么用Java和小程序把一个线下高频、低决策成本的服务搬到线上并管起来。2. 系统整体架构与设计思路拆解为什么这样分层而不是那样2.1 四层架构从微信客户端到MySQL每一层都踩过坑这个项目的物理结构非常清晰src目录下是标准的SpringBoot分层controller接口门面、service业务逻辑、mapper数据库操作、entity数据模型。但真正决定它能否“不改代码就运行”的是这四层之间数据契约的设计哲学。举个最典型的例子小程序前端展示一个预约卡片需要显示“XX店 · 3号包间 · 明天下午2点-4点 · ¥88”。这个信息横跨了三张表store门店、room包间、appointment预约。如果按新手惯性思维controller里直接new三个mapper去查再手动组装VO对象那代码会迅速变成意大利面条。而本项目采用的是DTO驱动的聚合查询在AppointmentMapper.xml里写了一个复杂的select用LEFT JOIN一次性拉取所有关联字段并通过resultMap精准映射到AppointmentDetailDTO这个专门给前端用的传输对象。你看pom.xml里MyBatis版本是3.4.6没用最新版就是因为这个版本对嵌套collection的支持最稳定避免了高版本里常见的N1查询陷阱。再看service层所有方法都加了Transactional但关键在于Transactional(rollbackFor Exception.class)——这是血泪教训。早期测试时发现用户预约成功但支付回调失败订单状态卡在“待支付”而包间已被锁定导致其他用户无法预约。后来排查发现Spring默认只对RuntimeException回滚而微信支付SDK抛出的是IOException属于checked exception必须显式声明。这种细节文档里不会写只有真刀真枪调过支付接口的人才懂。2.2 微信生态的深度绑定不是简单调API而是理解它的规则小程序前端和SpringBoot后端的通信绝不是“发个POST请求就完事”。这个项目在config包里专门建了WechatConfig.java里面配置了appId、appSecret、mchId商户号、apiKeyAPI密钥四大核心参数。但更重要的是WechatUtil.java里的两个关键方法getOpenIdByCode(String code)和unifiedOrder(MapString, String params)。前者处理登录后者处理支付。很多人以为拿到code就能换openId其实微信有严格校验code只能用一次且5分钟内有效而getOpenIdByCode内部做了双重缓存——先查Rediskey为wechat:openid:${code}命中则直接返回未命中则调用微信接口成功后将openId以wechat:openid:${openId}为key存入Redis有效期72小时。为什么这么设计因为小程序用户频繁切换页面时会反复触发登录如果每次都调微信接口不仅慢平均耗时800ms还可能触发微信的频率限制。而支付环节更复杂unifiedOrder生成预支付交易单后返回的paySign签名必须用特定算法HMAC-SHA256计算且参数顺序必须严格按字典序排列漏一个空格都会签名失败。项目里PaySignUtil.java的注释里甚至写了“此处顺序不可更改否则微信返回‘invalid sign’”这就是实操中抄错一行代码付出的代价。2.3 数据库设计的务实主义为什么不用UUID而用自增主键打开mahjong.sql第一眼看到CREATE TABLE user (id BIGINT PRIMARY KEY AUTO_INCREMENT, ...)你可能会疑惑现在不是都流行UUID做主键吗为什么这里用自增答案很现实性能与可读性。这个系统里user表最大也就几千条记录一家麻将馆的常客appointment表日均最多几百单。用BIGINT自增插入性能碾压UUIDUUID是随机写导致B树频繁分裂而且id作为外键出现在appointment.user_id、order.user_id等字段时数据库索引效率极高。更重要的是调试时太方便了——你在后台看到订单id127直接去数据库SELECT * FROM appointment WHERE id127秒出结果换成UUID你得复制一长串a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8手抖一个字符就查不到。mahjong.sql里另一个精妙设计是appointment表的status字段类型是TINYINT UNSIGNED取值范围0-255但只定义了5个状态0待支付、1已预约、2已到店、3已完成、4已取消。为什么不用ENUM因为ENUM在MySQL里修改枚举值要锁表而业务迭代中状态很可能增加比如未来加“爽约扣款中”TINYINT配合Enumerated(EnumType.ORDINAL)在Java里映射扩展性极强。sql目录下还有个init_data.sql里面预置了3家门店、5个包间、10条测试预约数据连store_id和room_id的关联关系都配好了你导入后打开小程序首页立刻能看到真实数据而不是一片空白的“暂无数据”。3. 核心模块实现详解从登录到核销每一步都是经验结晶3.1 用户体系微信登录不是终点而是风控起点小程序端点击“授权登录”触发wx.login()获取code然后调用后端/api/user/login接口。这个接口在UserController.java里核心逻辑是PostMapping(/login) public Result login(RequestBody MapString, String params) { String code params.get(code); String encryptedData params.get(encryptedData); String iv params.get(iv); // 1. 换openId String openId wechatUtil.getOpenIdByCode(code); // 2. 解密用户敏感信息昵称、头像 JSONObject userInfo wechatUtil.decryptUserInfo(encryptedData, iv, openId); // 3. 查询或创建用户 User user userService.findOrCreateByOpenId(openId, userInfo); // 4. 生成JWT令牌 String token jwtUtil.generateToken(user.getId(), user.getOpenId()); return Result.success(token); }这里藏着三个关键点第一decryptUserInfo方法里encryptedData必须是base64解码后的字节数组很多新手直接传原始字符串导致解密失败第二findOrCreateByOpenId不是简单SELECT * FROM user WHERE open_id?而是先查查不到再INSERT且INSERT语句用了ON DUPLICATE KEY UPDATEopen_id字段加了唯一索引避免并发时重复插入第三JWT的payload里只放了userId和openId绝不放手机号、身份证号等敏感信息token本身只是会话凭证敏感数据永远存在数据库里由后续接口按需查询。application.yml里JWT配置了expireTime: 72000002小时refreshTime: 36000001小时意思是token过期前1小时可以静默刷新用户无感。这个刷新逻辑在JwtAuthenticationFilter.java里实现它拦截所有带Authorization: Bearer xxx的请求解析token检查是否快过期是则生成新token并写入响应头X-Auth-Token小程序端只需监听这个header更新本地存储即可。3.2 门店与时段管理时间不是数字而是业务规则StoreController.java的/api/store/list接口返回门店列表但真正的难点在/api/store/{storeId}/timeslots——获取某个门店某天的可预约时段。这个接口的SQL在StoreMapper.xml里select idselectAvailableTimeSlots resultTypecom.mahjong.dto.TimeSlotDTO SELECT t.id AS timeSlotId, t.start_time AS startTime, t.end_time AS endTime, CASE WHEN COUNT(a.id) 0 THEN 0 -- 已被预约 ELSE 1 -- 可预约 END AS available FROM time_slot t LEFT JOIN appointment a ON t.id a.time_slot_id AND a.store_id #{storeId} AND DATE(a.appointment_date) #{date} AND a.status IN (0, 1, 2) -- 待支付、已预约、已到店都算占用 WHERE t.store_id #{storeId} GROUP BY t.id, t.start_time, t.end_time /select注意a.status IN (0, 1, 2)这个条件为什么“已到店”status2也算占用因为麻将馆的包间是物理资源用户到店后即使还没开始打包间也已被占用不能让下一个用户预约同一时段。而GROUP BY后用COUNT(a.id) 0判断是否被占比写EXISTS子查询更易读且MySQL优化器对这种写法更友好。TimeSlotDTO里有个available字段小程序端用它控制按钮禁用状态available1时显示“预约”0时显示“已满”。mahjong.sql里time_slot表的start_time和end_time是TIME类型不是DATETIME因为时段是固定模板如“14:00-16:00”与具体日期无关日期由appointment_date字段单独存储这样设计查询更高效也方便门店统一管理时段模板。3.3 预约与订单状态机驱动的业务核心预约流程的精髓在于AppointmentService.java里的createAppointment方法。它不是一个简单的INSERT而是一个分布式事务的简化模拟Transactional(rollbackFor Exception.class) public Result createAppointment(Long userId, Long storeId, Long roomId, Long timeSlotId, Date appointmentDate) { // 1. 校验时段是否可用双重检查DB查内存锁 if (!timeSlotService.isAvailable(timeSlotId, storeId, appointmentDate)) { return Result.fail(该时段已被预约请选择其他时段); } // 2. 加分布式锁Redis SETNX String lockKey lock:appointment: storeId : timeSlotId : appointmentDate; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, 30, TimeUnit.SECONDS); if (!locked) { return Result.fail(预约冲突请稍后重试); } try { // 3. 再次校验防止锁期间被抢占 if (!timeSlotService.isAvailable(timeSlotId, storeId, appointmentDate)) { return Result.fail(该时段已被预约请选择其他时段); } // 4. 创建预约记录 Appointment appointment new Appointment(); appointment.setUserId(userId); appointment.setStoreId(storeId); appointment.setRoomId(roomId); appointment.setTimeSlotId(timeSlotId); appointment.setAppointmentDate(appointmentDate); appointment.setStatus(0); // 待支付 appointmentMapper.insert(appointment); // 5. 生成订单关联预约 Order order orderService.createOrderFromAppointment(appointment); return Result.success(order.getId()); } finally { redisTemplate.delete(lockKey); // 必须释放锁 } }这里用了经典的“双重检查Redis分布式锁”模式。为什么需要锁因为高并发下两个用户几乎同时点击预约同一时段数据库唯一索引UNIQUE KEY uk_store_time_date (store_id, time_slot_id, appointment_date)虽然能保证最终一致性但用户体验极差——第二个用户会看到“服务器错误”而不是友好的“已满”提示。Redis锁把竞争控制在应用层让用户感知更平滑。orderService.createOrderFromAppointment方法里订单金额不是写死的而是根据store_id查store.price_per_hour再乘以时段时长TIMESTAMPDIFF(MINUTE, start_time, end_time)/60确保不同门店、不同时段价格灵活可配。3.4 后台管理不只是CRUD更是业务指挥中心管理员后台/admin路径的StoreAdminController.java提供了/list、/add、/update、/delete全套接口但最有价值的是/api/admin/appointment/statistics——数据统计。它返回近7天的预约趋势图数据SQL如下SELECT DATE(appointment_date) as date, COUNT(*) as total, SUM(CASE WHEN status 1 THEN 1 ELSE 0 END) as booked, SUM(CASE WHEN status 2 THEN 1 ELSE 0 END) as arrived, SUM(CASE WHEN status 3 THEN 1 ELSE 0 END) as completed FROM appointment WHERE appointment_date DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(appointment_date) ORDER BY date;这个查询用CASE WHEN做行转列比用5个子查询效率高得多。后端返回的JSON里booked、arrived、completed是三个独立数组前端ECharts直接渲染折线图。mahjong.sql里还预置了管理员账号usernameadminpassword$2a$10$...BCrypt加密密码是123456你可以在UserMapper.xml里看到select idfindByUsername的SQL它用BCryptPasswordEncoder.matches()校验密码这是Spring Security的标准做法比自己写MD5盐值安全得多。管理员后台的“核销”功能/api/admin/appointment/verify更体现设计功力它不是简单把status从1改成2而是先检查appointment_date是否是今天DATE(appointment_date) CURDATE()再检查status1必须是已预约状态最后才更新并记录verified_at时间戳。这样设计杜绝了店员误核销昨天订单的可能。4. 实操部署与避坑指南从零开始跑通的完整路径4.1 环境准备避开JDK和Maven的版本陷阱第一步永远是环境。项目pom.xml里java.version是11这意味着你必须装JDK 11而不是你电脑里默认的JDK 8或17。为什么因为spring-boot-starter-web2.3.x系列对JDK 11做了深度优化而JDK 17的--illegal-accessdeny参数会让某些反射调用直接崩溃。安装JDK 11后mvn -v检查Maven版本必须是3.6.3或更高。很多同学用IDEA自带的Maven3.5.4编译时报错Could not resolve placeholder spring.profiles.active就是因为旧版Maven不支持Spring Boot 2.3的属性解析机制。解决方案下载Maven 3.8.6解压后在IDEA的Settings Build Build Tools Maven里把Maven home path指向新解压的目录。application.yml里数据库配置是spring: datasource: url: jdbc:mysql://localhost:3306/mahjong?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai username: root password: 123456注意serverTimezoneAsia/Shanghai这是MySQL 8.0的强制要求漏掉会导致java.sql.SQLException: The server time zone value XXX is unrecognized。如果你用的是MySQL 5.7可以删掉这个参数但强烈建议升级到8.0因为mahjong.sql里用了JSON类型字段store.config存营业时间配置5.7不支持。4.2 数据库导入三步走拒绝“表不存在”错误导入mahjong.sql不是双击就完事。正确步骤1.新建数据库用MySQL客户端如Navicat或命令行执行CREATE DATABASE mahjong CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。必须用utf8mb4因为小程序用户昵称可能含emoji如“张三”utf8不支持。2.设置时区执行SET GLOBAL time_zone 8:00;否则appointment_date字段可能存成UTC时间导致查询错乱。3.导入脚本在mahjong库下执行source /path/to/mahjong.sql;。如果报错Error Code: 1067. Invalid default value for create_time说明你的MySQL严格模式开启需要临时关闭SET SQL_MODE;后再导入。导入成功后用SELECT * FROM user LIMIT 5;确认有测试数据。你会发现open_id字段全是oAbcDefGhiJklMnoPqrStUvWxyZ这样的格式这是微信测试号生成的模拟openId小程序真机调试时会自动替换为真实值。4.3 小程序端配置五个必须改的文件微信开发者工具打开小程序项目fyqa5Hynf3O9caEIuly4-master-...目录必须修改以下文件-project.config.json把appid改成你自己的小程序AppID在微信公众平台获取。-utils/config.js修改baseUrl为你的后端地址如https://your-domain.com/api。如果是本地调试填http://localhost:8080/api但必须在微信开发者工具的详情 本地开发 不校验合法域名打勾。-app.jsonLaunch里wx.login()后调用的接口URL要和config.js一致。-pages/index/index.jsonLoad里请求门店列表的URL也要同步。-project.config.json里的setting.minified设为false否则压缩后调试困难。最关键的一步在微信公众平台进入“开发管理 开发者工具”把你的本机IP如192.168.1.100添加到“服务器域名”里的request合法域名。否则wx.request会直接被微信拦截控制台只显示fail net::ERR_CONNECTION_REFUSED根本看不到后端日志。4.4 启动与验证如何确认每一步都走对了启动顺序严格遵循依赖关系1.先启MySQL确保mysql -u root -p能登录且SHOW DATABASES;能看到mahjong。2.再启后端在IDEA里右键MahjongApplication.java→Run。观察控制台出现Started MahjongApplication in X.XXX seconds且无ERROR表示SpringBoot启动成功。此时访问http://localhost:8080/swagger-ui.html项目集成了Swagger能看到所有API文档。3.最后启小程序微信开发者工具里点击“编译”看控制台是否输出[INFO] 登录成功token: eyJhb...。如果没有检查config.js的baseUrl和网络配置。验证点清单- ✅ 小程序首页显示3家门店/api/store/list返回数据非空- ✅ 点击某门店跳转到时段页显示“14:00-16:00”等时段/api/store/{id}/timeslots返回available1- ✅ 选择时段点击预约弹出支付窗口后端/api/order/create返回payParams- ✅ 管理员后台登录admin/123456能看到今日预约列表/api/admin/appointment/today如果卡在某一步优先看后端控制台日志。比如小程序显示“网络错误”而后端没任何日志说明请求根本没到后端——90%是域名配置问题如果后端有404日志说明config.js的baseUrl路径错了如果后端报500且日志有NullPointerException大概率是application.yml里数据库密码错了。5. 常见问题与实战排查技巧那些文档里不会写的真相5.1 “小程序白屏”问题90%源于这3个配置小程序启动后一片白控制台无报错别急着重装开发者工具按顺序检查1.检查app.json的pages数组项目里pages是[pages/index/index, pages/store/store, ...]如果你删过某个页面但没从app.json里移除微信会找不到入口文件直接白屏。解决方案打开app.json确认第一个路径pages/index/index对应的index.wxml文件存在且无语法错误比如多了一个/view闭合标签。2.检查project.config.json的miniprogramRoot这个字段必须是./当前目录如果被改成./miniprogram/而你的代码就在根目录微信会去./miniprogram/pages/index/index.wxml找文件自然找不到。解决方案用文本编辑器打开project.config.json搜索miniprogramRoot确保其值为./。3.检查utils/request.js的拦截器项目里request.js有全局错误拦截if (res.statusCode 400) { wx.showToast({title: 请求失败, icon: none}); return Promise.reject(res); }如果后端返回401 Unauthorized这个拦截器会弹窗但如果你在onLoad里没catch页面就会卡住。解决方案在调用API的地方加上.catch(err console.error(err))或者在拦截器里加console.log(API Error:, res)把错误打出来。5.2 “支付失败”排查微信的沉默比报错更可怕小程序调起支付后一直转圈最后提示“支付失败”。这不是代码问题而是微信的“静默失败”机制。排查步骤-第一步看后端日志支付接口/api/order/pay是否被调用如果没日志说明小程序没发请求——检查pages/order/order.js里wx.requestPayment的success回调里是否漏写了that.setData({paying: false})导致loading状态一直挂着。-第二步看微信商户平台登录https://pay.weixin.qq.com进入“交易中心 交易查询”用订单号搜索。如果查不到说明unifiedOrder接口根本没调通如果状态是“支付中”说明用户没完成支付如果是“已关闭”说明预支付单超时2小时。-第三步看PaySignUtil.java重点检查sign计算的参数列表必须包含appId、timeStamp字符串不是数字、nonceStr、package、signType且顺序必须字典序。项目里用TreeMap自动排序但如果你手写HashMap顺序错一个签名就无效。一个快速验证法把PaySignUtil.java里生成的sign和微信官方签名工具https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter20_1计算的结果对比必须完全一致。5.3 Docker部署踩坑镜像体积与端口映射Dockerfile内容简洁FROM openjdk:11-jre-slim VOLUME /tmp ARG JAR_FILEtarget/mahjong-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar ENTRYPOINT [java,-Djava.security.egdfile:/dev/./urandom,-jar,/app.jar]但实际部署时常见两个坑-镜像体积爆炸openjdk:11-jre-slim基础镜像约200MB加上jar包50MB最终镜像300MB。如果服务器带宽小推送慢。解决方案改用eclipse-jetty:jre11-slim体积仅120MB且内置Jetty启动更快。-端口映射失败docker run -p 8080:8080 mahjong后访问http://localhost:8080超时。原因SpringBoot默认绑定localhost容器内localhost是容器自身外部无法访问。解决方案在application.yml里加server.address: 0.0.0.0或启动时加参数-Dserver.address0.0.0.0。5.4 毕业设计答辩加分项三个可立即落地的扩展点如果你要用这个项目做毕设光跑通远远不够。评委最爱问“你做了哪些创新或改进”以下是三个零成本、高价值的扩展5分钟就能加进去-增加短信通知在AppointmentService.createAppointment末尾加一行smsService.send(您的预约已成功时间${date})。用阿里云短信SDKaliyun-java-sdk-dysmsapipom.xml加依赖application.yml配accessKeyId和accessKeySecret10行代码搞定。答辩时说“我集成了短信通知提升用户履约率”瞬间拉开差距。-增加预约提醒用Quartz定时任务每天9点扫描appointment_date tomorrow且status1的订单调用微信模板消息API推送提醒。pom.xml加spring-boot-starter-quartz写个Scheduled(cron0 0 0 9 * ?)方法30行代码。-增加数据看板在管理员后台加/api/admin/dashboard接口SQL用SELECT COUNT(DISTINCT user_id) FROM appointment WHERE create_time DATE_SUB(NOW(), INTERVAL 30 DAY)统计月活配合ECharts画饼图。评委看到“用户活跃度分析”眼睛会亮。最后分享一个个人体会去年指导一个学生他花三天跑通项目又花两周加了短信和提醒答辩时演示了从用户预约、收短信、到店扫码核销的全流程评委当场问“这个核销码是怎么生成的”他答“用Hutool的SecureUtil.md5(userId appointmentId timestamp)”评委点点头说“安全意识不错”。你看技术深度不在多而在准——抓住一个点把它做透比泛泛而谈十个功能更有说服力。这个麻将馆系统就是你证明自己“真懂全栈”的最佳证物。本文还有配套的精品资源点击获取简介直接可用的麻将馆预约系统源码后端用SpringBoot开发前端是微信小程序数据库用MySQL配套完整建表脚本mahjong.sql含用户注册登录、门店列表展示、预约时段选择、订单生成与状态跟踪、管理员后台管理等核心功能。项目结构规范包含标准Maven配置pom.xml、Spring Boot配置文件、分层Java业务代码controller/service/mapper、JUnit单元测试、Dockerfile容器化支持SQL脚本已预置表结构和初始测试数据导入即可运行。适合学生做毕业设计或课程大作业也适合作为Java Web与小程序全栈开发的实操参考。附带零基础入门指南压缩包覆盖JDK/Maven/IDEA/微信开发者工具环境搭建、小程序对接SpringBoot接口、MyBatis操作MySQL等关键步骤所有代码经过本地调试验证无需二次修改就能启动运行。本文还有配套的精品资源点击获取