体育馆场地预约系统实战:uni-app与Spring Boot前后端分离开发
从标题里的888hkm5j这个随机后缀就能看出来这是一套典型的毕业设计或课程设计级别的前后端分离项目uni-app编写跨端小程序前端Spring Boot提供后端服务业务场景是体育馆场地预约。但说实话真正做完一个能通过答辩、能演示、甚至能上线试运营的版本和网上那些从零搭建教程之间隔着大量业务细节。这篇文章把我自己做这个平台时踩过的坑、想清楚的问题、以及代码之外的决策过程完整写出来给准备做类似项目的同学一个真实参考。先说结论体育馆场地预约这个业务难点从来不在CRUD本身而在于场地资源的并发冲突处理、价格策略的灵活性以及小程序端对微信生态规范的适配。这三块如果不在设计阶段考虑清楚后面每改一处都牵一发动全身。1. 为什么是uni-app Spring Boot一次技术选型的复盘选型这件事很多教程会直接告诉你用uni-app配Spring Boot就对了但很少有人讲清楚为什么这个组合适合这类管理平台项目。我做完整个项目后回头复盘觉得有三个理由是这个组合真正打动我的地方。1.1 uni-app解决的是多端焦虑而非跨端炫技体育馆场地预约这类系统目标用户通常会通过两种方式触达微信小程序为主偶尔需要H5页面用于宣传页或临时入口。如果用原生微信小程序写未来想加一个H5版本几乎等于重写前端如果上Flutter或React Native微信小程序生态的登录、支付、订阅消息等能力接入又绕一大圈。uni-app的Vue语法对做过Vue的同学几乎没有学习成本更重要的是它把微信小程序的wx.login、wx.request、wx.navigateTo这些API都封装成了uni.login、uni.request、uni.navigateTo一套代码同时编译到微信小程序和H5。我在实际开发中90%的时间只写一套逻辑只有涉及微信特定能力时才写条件编译代码。这种开发体验比跨端框架四个字能描述的要实在得多。1.2 Spring Boot的生态成熟度让后端开发回归业务本身后端选Spring Boot几乎不需要犹豫。原因很简单这个项目涉及用户登录含微信OAuth、订单管理、场地管理、支付回调每一个模块在Spring Boot生态里都有极其成熟的接入方案。MyBatis-Plus让简单的CRUD省掉大量XML配置Spring Security或Sa-Token做鉴权都很顺手微信支付SDK直接引入即可。我用的是Spring Boot 2.7.x版本。为什么不追新用3.x因为3.x基于Jakarta EE规范部分老牌库比如某些支付SDK、数据库连接池的兼容版本迁移成本高国内大量中间件和教程仍基于2.x编写。做这类项目稳定性和可查资料数量远比版本号新旧重要。1.3 数据库选型MySQL是这个体量的最优解体育馆场地预约的数据量级撑死就是几千用户、几百个场地、每天几千条订单记录MySQL完全能扛而且Navicat可视化操作、SQL查询排错都非常方便。我用了MySQL 8.0主要看中它对JSON类型的支持——后面价格策略、场地扩展字段都用得上。给新手的建议不要在一开始就纠结分布式、高并发、缓存架构这些词。这类项目的核心是跑通业务闭环MySQL Redis可选已经是这个体量的奢侈配置了。2. 业务模型设计场地、时段、订单三张核心表怎么落地做完整个系统最大的体会是体育馆预约的数据库设计核心不是场地表和订单表而是场地与时段的关系模型。这个模型设计得好后面的查询、冲突检测、价格计算都会很顺畅设计得不好后面会在SQL里写出各种让人头疼的嵌套条件。2.1 场地表不要把价格和时段硬塞进去很多初级设计会把场地表设计成CREATE TABLE venue ( id bigint PRIMARY KEY, name varchar(50), price_per_hour decimal(10,2), open_time varchar(20), close_time varchar(20) );这个设计的典型问题是一个体育馆里羽毛球场地在工作日的早场和晚场价格可能完全不同周末和节假日价格也可能上浮。如果你把price_per_hour写死在场地表碰到周一至周五10点前30元/小时其余时段50元/小时这种需求时只能硬编码在业务逻辑里非常被动。我采用的方式是独立的场地-时段-价格关联表CREATE TABLE venue ( id bigint PRIMARY KEY AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 场地名称如1号羽毛球场地, venue_type varchar(20) NOT NULL COMMENT 场地类型badminton/basketball/table_tennis等, location varchar(100) COMMENT 位置描述, status tinyint DEFAULT 1 COMMENT 1可用 0停用, cover_url varchar(255) COMMENT 封面图, create_time datetime DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE price_rule ( id bigint PRIMARY KEY AUTO_INCREMENT, venue_id bigint NOT NULL COMMENT 关联场地, weekday varchar(20) NOT NULL COMMENT 适用星期1-7或holiday, start_time time NOT NULL COMMENT 时段开始, end_time time NOT NULL COMMENT 时段结束, price decimal(10,2) NOT NULL COMMENT 该时段每小时价格, max_book_hours int DEFAULT 2 COMMENT 该时段最多可订时长 );这种设计的好处是计费规则完全数据化前端展示可选时段时直接查这张表就行后端计算价格也变成一个简单的查询而不是一堆if-else。2.2 时段管理30分钟为最小预约单元我认为在做场地预约这类系统时时段的粒度设计要先想清楚不然一定会返工。最粗的粒度是按天预约比如订一整天场地这对篮球馆包场可以对羽毛球馆完全不可行——羽毛球馆通常是按小时订的。最细的粒度是按分钟但这对用户体验和系统复杂度都不友好也不符合实际场馆管理习惯。我做的是30分钟最小粒度。也就是说一个用户可选的时段是09:00-10:00、09:30-10:30这种整点或半点为边界的区间。这个粒度对羽毛球、乒乓球、篮球半场这样的场景都很合适。为了不让用户随意拼出超长时段占用场地我还限制了单笔订单最大时长规则写在price_rule.max_book_hours里。时段的计算我放在后端完成。前端只传startTime和endTime后端做时段切分与冲突校验避免前端传一个09:10到09:40这种没法落地的区间。2.3 订单表状态机的设计决定了后端的复杂度订单表是整个系统里最关键的表因为它在整个生命周期里有多个状态流转CREATE TABLE booking_order ( id bigint PRIMARY KEY AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint NOT NULL, venue_id bigint NOT NULL, booking_date date NOT NULL COMMENT 预订日期, start_time time NOT NULL, end_time time NOT NULL, total_amount decimal(10,2) NOT NULL, status tinyint NOT NULL COMMENT 0待支付 1已支付 2已取消 3已完成 4已退款, contact_name varchar(20), contact_phone varchar(20), create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime, KEY idx_user_id (user_id), KEY idx_venue_date (venue_id, booking_date) );status字段的值我通过代码常量来管理而不是散落在各处魔法数字里。这个状态机的流转规则是用户提交订单 -0待支付支付成功后 -1已支付用户主动取消或超时未付 -2已取消预约日期用完后 -3已完成管理员退款 -4已退款这里有个非常重要的细节取消订单和支付超时是两个完全不同的事件。用户主动取消是即时操作超时未付需要一个定时任务或延迟队列来扫描。我最初用Spring的Scheduled每分钟扫一次待支付订单超过15分钟未支付就自动置为取消并释放场地。这种方式在低并发下足够可靠实现成本也低。3. Spring Boot后端预约流程的接口设计与冲突处理后端接口设计是整个系统的主干我把它分成三类用户端的场地查询与预约接口、管理员端的场地与订单管理接口、微信相关的认证与支付接口。这里挑最核心的几个接口讲清楚设计思路特别是并发冲突处理——这部分是答辩时最容易被老师追问的。3.1 核心接口清单与数据流整个后端接口按业务模块划分如下模块接口功能说明认证模块POST /api/user/login微信登录或账号密码登录返回JWT场地模块GET /api/venue/list按类型、日期查询可用场地场地模块GET /api/venue/slots查询某个场地某天的可约时段预约模块POST /api/booking/create创建订单含并发冲突校验预约模块POST /api/booking/cancel取消订单预约模块GET /api/booking/my查询我的预约记录管理模块POST /api/admin/booking/confirm管理员确认/取消订单支付模块POST /api/pay/wx调用微信支付统一下单前端调用最频繁的接口是GET /api/venue/slots。它的逻辑是根据场地ID和日期查出该场地当天的所有price_rule时段再扣除已经被有效订单占用的时段最后把剩余可订时段加上价格返回给前端。这个查询涉及两次IO一次查价格规则一次查已占用时段。我开始时直接在Service层先查规则再查订单然后用Java代码做差集。后来发现数据量上来后这段代码不太优雅就改用SQL一次性查出来两个结果集在内存里比对反正一天最多也就20来个时段性能根本不是问题。3.2 并发预约冲突乐观锁是最合适的方案这是整个系统最值得讲的技术点。场景很简单上午10点整两个用户同时点击预约1号羽毛球场地10:00-11:00的时段。如果系统不做并发控制两个订单都会通过校验都显示预约成功场地就超售了。解决这个问题有三类方案悲观锁在事务里对场地记录加行锁SELECT ... FOR UPDATE直到事务结束才释放。问题是锁的范围不好控制而且如果同一时间段有多个场地锁的粒度容易出错。乐观锁在订单表加version字段或利用唯一索引更新时检查版本号更新失败则重试或提示用户。唯一约束在订单表上建立(venue_id, booking_date, start_time, end_time, status)的联合唯一索引利用数据库层约束保证不会重复插入。我最终使用的是乐观锁 数据库唯一索引双保险。核心是插入订单时先检查是否存在重叠订单Override Transactional(rollbackFor Exception.class) public BookingOrder createBooking(BookingCreateDTO dto) { // 1. 校验场地存在且可用 Venue venue venueMapper.selectById(dto.getVenueId()); if (venue null || venue.getStatus() ! 1) { throw new BizException(场地不存在或已停用); } // 2. 校验预约时间是否在当前日期之后 LocalDate today LocalDate.now(); if (dto.getBookingDate().isBefore(today)) { throw new BizException(不能预约过去的日期); } // 3. 核心并发控制查询当天该场地是否存在重叠预约 // 这一步在事务内执行配合数据库索引能防止超售 Integer overlapCount bookingMapper.selectOverlapCount( dto.getVenueId(), dto.getBookingDate(), dto.getStartTime(), dto.getEndTime(), Arrays.asList(0, 1) // 待支付和已支付都算占用 ); if (overlapCount 0) { throw new BizException(该时段已被预约请选择其他时段); } // 4. 计算价格 BigDecimal amount priceCalculator.calculate(dto.getVenueId(), dto.getBookingDate(), dto.getStartTime(), dto.getEndTime()); // 5. 生成订单 BookingOrder order new BookingOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(dto.getUserId()); order.setVenueId(dto.getVenueId()); order.setBookingDate(dto.getBookingDate()); order.setStartTime(dto.getStartTime()); order.setEndTime(dto.getEndTime()); order.setTotalAmount(amount); order.setStatus(0); order.setContactName(dto.getContactName()); order.setContactPhone(dto.getContactPhone()); bookingMapper.insert(order); // 6. 释放超时未支付订单的定时清理逻辑会在后面说明 return order; }对应的SQL里的selectOverlapCount是防超售的最后一道防线SELECT COUNT(*) FROM booking_order WHERE venue_id #{venueId} AND booking_date #{bookingDate} AND status IN (0, 1) AND ( (start_time #{endTime} AND end_time #{startTime}) )这个SQL用时间区间重叠判断来代替简单的时间等值匹配能覆盖用户从09:30订到11:00但已经有10:00-11:00的订单这种部分重叠的边界情况。实际压测中两个并发请求同时到达时虽然有可能都通过了Java层的检查但由于订单表上idx_venue_date索引的存在数据库层面会串行化这里的插入操作。再配合Transactional的事务隔离后一个事务能看到前一个事务插入的结果从而触发冲突异常。这也是为什么我强烈建议保留Transactional——面试官追问并发问题时这能体现出你对事务隔离级别的理解。3.3 价格计算的规则引擎设计价格计算看起来简单实际需求千奇百怪工作日的白天和晚上价格不同周末全天价格统一上浮节假日在周末价格基础上再上浮双十一全场八折……如果把这些规则四处铺开写if-else后期维护会非常痛苦。我的做法是把价格规则完全数据化public BigDecimal calculate(Long venueId, LocalDate date, LocalTime start, LocalTime end) { // 查找命中的价格规则按日期类型工作日/周末/节假日和时段区间 ListPriceRule rules priceRuleMapper.findMatchRules(venueId, date, start, end); if (rules.isEmpty()) { throw new BizException(该时段暂未开放预订); } BigDecimal total BigDecimal.ZERO; // 按小时计算不足一小时的按分钟比例折算 Duration duration Duration.between(start, end); BigDecimal minutes BigDecimal.valueOf(duration.toMinutes()); for (PriceRule rule : rules) { BigDecimal spanMinutes BigDecimal.valueOf( Duration.between(rule.getStartTime(), rule.getEndTime()).toMinutes() ); BigDecimal pricePerHour rule.getPrice(); total total.add(pricePerHour.multiply( minutes.min(spanMinutes).divide(BigDecimal.valueOf(60), 2, RoundingMode.HALF_UP) )); } return total.setScale(2, RoundingMode.HALF_UP); }这段代码思路很直白但要注意一个细节预算时长是跨时段的比如用户订的是21:30到22:30这个区间横跨了晚间时段21:00-22:00和深夜时段22:00-23:00两个规则那就要按每个规则的实际覆盖分钟数分别计价。这就是为什么不能用查一条规则算总价必须用循环把命中的规则都遍历一遍。经验之谈凡是涉及价格、时间、状态这类字段不要在前端做计算后端必须作为唯一事实来源。前端传过来的价格一律不信任后端用数据库里的规则重新计算。这样以后调整价格策略时不用强迫用户升级小程序版本。4. uni-app前端实现页面结构、状态管理与微信适配前端部分我用uni-app Vue 3 Pinia的组合。页面结构上一个体育馆预约小程序的典型页面是五个首页场地展示与类型筛选、场地详情页、订单确认页、订单列表页和个人中心页。每个页面都有值得讲的细节。4.1 首页与场地列表骨架屏提升首屏体验首页是整个小程序的脸面它要完成三件事展示场地类型分类羽毛球、篮球、乒乓球等、展示推荐场地卡片、提供日期选择器让用户去查看某天的可用情况。这里的核心交互是日期选择。用户选择明天或后天页面下方的场地列表要对应刷新可用状态。我用的方案是在首页顶部放一个横向滑动的日期栏默认选中今天实际上默认选明天因为今天通常已经订不到了每次切换日期就重新请求/api/venue/list?datexxx。关于首屏加载体验我提一个很多人忽略的点在小程序里onLoad阶段请求数据时页面已经有了一整年的骨架屏动画。我用简单的v-if控制数据没返回前渲染灰色占位块返回后渲染真实内容视觉上会舒服很多比加一个loading转圈容易实现且效果更好。template view classvenue-list view v-ifloading classskeleton-card v-fori in 4 :keyi view classskeleton-block skeleton-cover/view view classskeleton-block skeleton-line/view /view view v-else classvenue-card v-foritem in venueList :keyitem.id image :srcitem.coverUrl modeaspectFill classvenue-cover / text classvenue-name{{ item.name }}/text text classvenue-price v-ifitem.minPrice¥{{ item.minPrice }}起/小时/text /view /view /template这里有个性能细节场地列表接口返回的minPrice是后端算好的而不是前端遍历所有时段价格规则后自己取最小值。前端做这种聚合计算在小数据量时看不出问题数据结构一变就会很难维护。4.2 预约流程的时间选择交互从场地详情页到订单确认页中间最关键的是选择可预约时间。我的做法是进入场地详情页时默认带上今天或用户首页选择的日期请求/api/venue/slots接口获取该场地当天的剩余时段。后端返回的数据格式是[ { startTime: 09:00, endTime: 10:00, price: 30.00, available: true }, { startTime: 09:30, endTime: 10:30, price: 30.00, available: true }, { startTime: 10:00, endTime: 11:00, price: 50.00, available: false } ]前端按照已占用/未占用渲染为灰色或可选样式。用户点击某个时间段组件自动记录如果这个场地支持连续订多段时间比如羽毛球可从10:00订到12:00我做成再点一个时间段则与已选项合并的交互取消已选则再次点击取消。这块的交互逻辑看似简单但有一个坑用户选了一个起始时段又选了一个不连续时段到底算两笔订单还是一笔订单我的做法是只允许连续时段合并系统计算合并后的总金额和总时长并在订单确认页标识清楚。如果用户选了10:00-11:00和14:00-15:00两个不连续时段则在前端直接提示当前仅支持连续时段合并下单。4.3 Pinia状态管理与登录态控制我用了Pinia做全局状态管理。全局只放两类东西用户信息和登录态。场地列表和订单数据不进全局store因为它们是页面级数据跨页面共享的需求不强但用户信息几乎每个页面都要用判断有没有登录、显示头像昵称、下单时填充联系人所以放store里合适。关于微信登录这里有一个必须讲清楚的点wx.login获取的code本身不是用户身份它只是换取openid的临时凭证而且这个code五分钟内有效且只能用一次。正确的流程是小程序端调用uni.login()拿到code后端拿着code调用微信接口jscode2session换取openid和session_key后端把openid作为用户唯一标识在user表里查找或新建用户后端生成JWT返回给前端前端存到Storage里后续请求Header带Authorization: Bearer token这里有个多数初级开发者容易犯的错直接把openid返回给前端前端拿它当user_id用。这样做有安全隐患——openid一旦泄露别人就能冒充这个用户。正确做法是openid只留在后端JWT里只存一个内部生成的userId。4.4 uni-app打包微信小程序的适配经验大部分代码一套写完后微信小程序打包还有一堆适配问题。我这里列三个我真实遇到过的坑坑一Vue 3的ref响应式在微信小程序里的表现问题。在H5端ref({name: xxx})用起来很自然但在微信小程序端某些嵌套层级较深的对象在跨页面传递时会丢失响应式。我的经验是跨页面传递数据尽量用简单类型或JSON字符串不要传复杂响应式对象。详情页跳转订单确认页我用URL参数传venueId和startTime订单确认页再根据这些参数重新请求详情接口而不是把整个场地对象塞进store或事件通道。坑二小程序包体积限制。微信小程序主包限制2MB超过就得用分包。这个项目里页面数量不算多主包勉强能塞下。我的做法是把用户协议、关于我们这类低频页面放到subpackage分包里这样主包体积能控制住。坑三微信开发者工具和真机的差异。很多API在开发者工具里表现正常一到真机上就出问题。最典型的是网络请求开发者工具默认不校验合法域名真机上必须在小程序后台配置服务器域名且必须是HTTPS。我一开始在本地用http://localhost:8080调接口一切正常一上真机全部请求失败才想起来需要在内网穿透工具或已备案HTTPS域名下调试。提醒如果你没有现成的HTTPS域名开发阶段可以用微信开发者工具 - 详情 - 本地设置 - 不校验合法域名这个开关。但上架前必须配置正式的HTTPS接口域名。5. 那些文档里不会写的坑时间解析、状态流转与管理后台这一章我想专门写做这类项目时最容易翻车的几个隐蔽问题每个都是我实际花过时间的。5.1 时间分区与跨天预约陷阱体育馆场地预约基本不会出现跨天预约不会有人从晚上10点订到凌晨2点但即使如此时间处理也有陷阱。第一个坑是Java后端接收前端时间参数的格式问题。前端传的startTime可能是10:00也可能是10:00:00如果后端DTO用LocalDate和LocalTime类型接收格式不一致直接报400。我统一在前端字段上做了格式化后端配置了JsonFormat(pattern HH:mm:ss)同时兼容HH:mm格式。第二个坑是数据库时区不一致导致的日期偏移。MySQL的datetime类型默认不带时区如果连接串里没写serverTimezoneAsia/Shanghai而数据库服务器是UTC时区查询出来的时间可能会比实际少8小时。我踩过一次用户订了明天10点的场地查看订单列表时显示的是今天凌晨2点。排查半天发现是连接串少配了serverTimezone。5.2 待支付订单的自动回收定时任务要注意边界订单创建后用户没付款场地一直被占着必须释放。我用的是Spring自带的Scheduled固定频率扫描。Component public class OrderTimeoutTask { Scheduled(fixedDelay 60 * 1000) // 每分钟执行一次 public void cancelTimeoutOrders() { LocalDateTime deadline LocalDateTime.now().minusMinutes(15); ListBookingOrder timeoutOrders bookingMapper.selectByStatusAndCreateTime(0, deadline); for (BookingOrder order : timeoutOrders) { order.setStatus(2); // 已取消 order.setCancelReason(超时未支付系统自动取消); bookingMapper.updateById(order); } } }这里要提醒的是分布式环境下多个实例同时跑定时任务会重复处理同一批订单。单机部署没问题但如果将来把后端拆成多个实例就得引入分布式锁或使用xxl-job这类任务调度平台。答辩时可以提一嘴这个扩展点能加分。还有一个小细节取消订单后如果用户已经支付过状态从1变为4需要走退款流程。微信支付原生退款要调用退款接口且需要证书。我在这个项目里做的是管理员可见待处理退款列表被取消的已支付订单由管理员人工确认后在微信支付商户平台手动退款同时把订单状态更新为已退款。这种方式对毕业设计完全够用但真实商业环境建议接入自动退款接口。5.3 管理后台用Spring Boot Vue还是纯后端接口由于这是小程序项目我默认还有一个管理后台给体育馆管理员用。我提供两个方案供选择方案A另做一个Vue管理后台。使用Vue Element Plus搭一个PC端页面典型的管理端功能场地管理、价格规则管理、订单列表、退款审核。这种方案结构清晰前后端接口完全复用小程序后端推荐时间充裕的同学做。方案B小程序内嵌管理员入口。在个人中心页里如果当前用户角色是ADMIN就多显示一个管理入口页面里实现简单的场地状态修改和订单管理。优点是省一个PC端项目缺点是移动端管理体验较差并且答辩展示时还是PC端可视化更直观。我实际选择的是方案B因为毕设或课设的时间通常有限博客里这个项目的重点在小程序端和后端核心逻辑。但如果你时间允许方案A会更好。5.4 微信支付的接入逻辑不一定要真跑通现实情况是个人开发者的小程序几乎无法开通微信支付需要企业主体所以很多同学做到支付这一步就卡住了。我的处理方式分两层代码层面完整实现模拟支付流程。创建订单后状态为待支付前端调到收银台页面用户点击确认支付前端请求POST /api/pay/mock后端直接把这个订单状态从待支付改为已支付同时记录支付时间和支付方式。这样业务流程完全走通答辩演示很顺畅。替换为真实微信支付只需要在接口内换成调用WxPayService的统一下单接口拿到payParams后前端用uni.requestPayment发起支付。代码结构完全不变只是把mock接口的实现换掉。这个小技巧很实用。很多同学被困在没有企业主体无法开通微信支付这一步导致整个项目流程走不完。先用模拟支付把业务闭环跑通后续再替换成真实支付是成本最低的做法。6. 数据查询的最佳实践与权限控制细节这个章节把我做项目时积累的几个数据查询和权限控制经验写下来可能比前面的接口设计更实用。6.1 场地可用时段查询的一次性SQL最开始我写可用时段查询时是分三次查询的先查价格规则、再查已占用订单、然后Java内存里做差集。代码能跑但总觉得不够优雅。后来优化成一条SQLSELECT pr.id, pr.start_time, pr.end_time, pr.price, pr.venue_id FROM price_rule pr WHERE pr.venue_id #{venueId} AND pr.weekday #{weekday} ORDER BY pr.start_time;然后已占用时段用另一条简单SQL查出所有status IN (0,1)的订单时间区间在内存中用两个指针做时间段合并和差集。因为一天内场地时段数量本身不超过30个这种内存计算效率远高于复杂的SQL区间运算代码也更可读。有时候不追求极端性能追求可维护性才是中型项目的正确选择。6.2 基于JWT的登录鉴权和角色权限用户登录后拿到JWT这个Token里我放了userId和role两个关键claim。后端用一个拦截器统一处理Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); Claims claims JwtUtil.parseToken(token); if (claims ! null) { request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } } response.setStatus(401); return false; } }Admin接口再套一层角色校验比如RequireRole(ADMIN)注解。这样实现了登录用户可访问个人接口管理接口仅限管理员的权限模型。Sa-Token或Spring Security可以做得更细但对这个项目体量自定义拦截器已经完全足够而且代码透明、可控、好维护。6.3 多条件动态查询用MyBatis-Plus还是XML场地列表支持按类型筛选、按日期查剩余场地、按关键字搜索场地名称。动态SQL条件不少我选了MyBatis-Plus的LambdaQueryWrapper方式因为代码里写条件链比XML清晰不少LambdaQueryWrapperVenue wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(type), Venue::getVenueType, type) .like(StringUtils.hasText(keyword), Venue::getName, keyword) .eq(Venue::getStatus, 1) .orderByDesc(Venue::getCreateTime);如果是特别复杂、需要联表的分页统计SQL那就老老实实用XML写。两种方式并存是这个项目的真实状态。7. 联调、部署与答辩演示的全流程经验做完了代码真正让这个项目能跑起来给别人看的过程其实比写代码更考验耐心。这一章聊聊我这个项目从本地到演示的整体流程。7.1 本地联调环境怎么搭前端uni-app跑在微信开发者工具里后端Spring Boot跑在本地8080端口。两者之间通信最大的麻烦是域名和端口问题。我在开发阶段的一个做法是微信开发者工具里勾选不校验合法域名Web请求地址直接写http://localhost:8080。但真机预览时手机的localhost指向的是手机自己不是电脑所以必须用局域网IP访问。这时需要保证电脑和手机在同一个WiFi下同时后端启动时加上--server.address0.0.0.0让服务监听所有网卡。注意微信开发者工具可以关掉域名校验但真机调试时这个选项不生效。最稳妥的方式是本地跑一个内网穿透工具把电脑的8080端口映射成一个HTTPS公网域名然后把小程序后台的request域名临时配置成这个映射域名。我用这种方式在真机上测试登录、支付流程都很顺畅。7.2 部署到服务器的两条路演示时有两种部署方式本地演示和服务器部署。本地演示是最省事的插着电源电脑上同时跑起微信开发者工具和后端IDE所有操作可视化。缺点是如果网络不稳定或者评委老师要求现场用手机扫码体验本地环境会很尴尬。服务器部署相对稳定但需要一台云服务器。流程是后端Maven打包成jar用nohup java -jar xxx.jar启动前端uni-app通过npm run build:mp-weixin构建出小程序包然后在微信开发者工具里上传体验版用手机扫码体验。数据库用Navicat连接云服务器管理。我推荐时间充裕的同学至少试一次服务器部署。原因很简单本地运行和服务器运行之间隔着一条真正的生产环境鸿沟很多问题比如端口开放、MySQL远程连接权限、防火墙设置不亲自踩一遍始终只停留在IDE里。7.3 演示脚本顺序很重要答辩或项目演示时我建议按这个顺序来既快又稳首页展示展示体育馆场地分类和场地卡片列表说明前端用的uni-app、后端用的Spring Boot。用户登录用微信扫码登录展示用户信息和JWT交互过程。查询可用时段选择一个羽毛球场地、选择明天的日期展示接口返回的剩余时段和价格。下单支付选择一个时段模拟支付完成订单状态从待支付变为已支付。订单列表展示我的预约页面说明订单状态流转。冲突校验演示用一个已占用时段再走一次下单流程展示该时段已被预约的错误提示。这一步能体现出并发控制逻辑是加分项。管理员操作如果做了管理入口停用一个场地或查看全部订单。演示过程中最怕的就是网络请求慢或者接口报错所以演示前务必把微信开发者工具的缓存清掉、后端日志打开、数据库连接确保可用。不要现场改代码这是我踩过最痛的坑演示时想改一行CSS结果触发了热更新整个页面卡死最后只能重启项目白白浪费了几分钟。写在最后的经验总结做完这个体育馆场地预约平台我最大的体会是技术框架只是工具真正考验人的是对业务场景的理解和边界情况的处理。有时间冲突就想到并发控制有超时未付就想到状态回收有价格浮动就想到规则数据化——这些思考过程比背会几个框架API重要得多。如果你正在做类似的项目我还想额外分享三个具体的建议第一数据库表结构设计时多花两小时后面coding能省两天。特别是场地预约这种强业务约束的系统把price_rule独立出来、把订单状态机画清楚远比急着写Controller更值得。第二前端不要过度设计。uni-app的优势是开发快你就用它快速把交互跑起来不要把时间花在抽象组件、封装request层这些锦上添花的事上先把核心流程打通。第三日志和异常处理不要省略。这个项目里我习惯了在Service层抛出带有明确中文描述的BizException全局用RestControllerAdvice统一捕获并返回统一错误结构。这样做的好处是前端提示用户该时段已被预约很简单后端排查问题时日志里也有清晰的业务上下文而不是满屏的NullPointerException。这套系统做完之后我后来又在这个基础上扩展过会员积分、场地月卡、多场馆支持等方案技术上都没有本质变化核心逻辑全部绕不开时间-场地-订单这个三角关系。只要把这个三角关系处理得干净透彻这套代码未来不管是换前端框架还是增加预约类型迁移成本都非常低。