
校园食堂点餐配送听起来是个老掉牙的选题但真正动手做过的朋友都知道这里面坑一点都不少。尤其是用上SpringBoot3打底、微信小程序做前端这套组合既要处理校园场景特有的高并发午高峰又要对接微信支付v3那一堆证书和回调还要让商家端、骑手端、用户端三方的订单状态保持一致。这篇文章我就以自己实际落地的经历把整个系统的设计与实现过程拆开来讲从数据库设计到小程序界面适配从支付对接踩坑到配送状态流转把那些文档里不会明说的细节一并交代清楚。如果你正在做类似的毕业设计或者想给学校食堂做一套可用的点餐配送系统这篇内容可以直接帮你少走一半弯路。1. 整体设计与思路拆解1.1 校园场景的特殊性决定了系统的核心诉求先聊需求。校园食堂点餐配送表面上看和普通外卖系统差不多用户选菜、下单、支付、配送但实际上校园场景有三个典型特征直接决定了系统的设计方向。第一是午高峰流量集中。中午十一点半到十二点半这一个小时内的订单量可能比全天其他时间的总和还高。第二是配送范围高度固定基本限定在宿舍区和教学区不需要复杂的LBS匹配和路径规划。第三是用户群体极其统一都是在校学生和老师使用习惯和账号体系相对单一甚至可以不用做复杂的会员体系直接绑定校园身份就行。这三个特征带来了什么影响首先后端在秒杀式流量进来的时候要能扛住数据库表设计、缓存策略、支付超时兜底这些都要提前考虑到其次配送模型不需要做成美团那样的大盘调度只需要简单的接单—取餐—送达三段式流转即可这大大降低了实现复杂度最后登录鉴权可以做得非常轻微信小程序本身就是天然的登录入口配合后端JWT签发令牌整个安全体系几分钟就能搭完。1.2 技术选型为什么是SpringBoot3加微信小程序后端我选的是SpringBoot3不是没纠结过。早期很多人还在用SpringBoot23.0推出之后最大变化是强制基于Jakarta EE 9规范很多老依赖的坐标从javax.*换成了jakarta.*这意味着网上大量的旧教程代码直接报红。但SpringBoot3带来的好处也很明显原生支持GraalVM启动速度更快内置的Tomcat和Undertow性能有提升而且Spring Security 6的配置方式更清晰对于新项目来说没有历史包袱直接上手反而更顺畅。前端选择微信小程序更是顺理成章的事情。校园场景下用户不太可能为了打个饭特意下载App小程序即扫即用而且微信支付在小程序内有天然的使用路径。整个开发过程中小程序端我用的是原生框架而非uniapp原因很简单原生框架调试更直接遇到问题搜索解决方案时命中率也更高。当然如果以后要打包成支付宝小程序或者抖音小程序那转uniapp是另一回事了但就校园食堂这个单平台场景原生开发完全够用不用为了框架而框架。1.3 功能模块全景四个端口的职责划分系统拆成四个角色用户端小程序、商家端小程序/Web、骑手端小程序、管理后台Web。核心功能模块包括用户端菜品浏览、分类检索、在线点餐、购物车管理、订单创建、在线支付、订单状态跟踪、配送地址维护、历史订单回看商家端菜品上下架管理、库存设置、订单接单/拒单、出餐状态更新、营业额统计骑手端待接订单大厅、抢单或系统派单、取餐确认、送达确认、配送统计管理后台用户管理、商家入驻审核、评论管理、数据大屏、系统配置整个系统最核心的就是订单状态机。这一块如果设计不清楚后面联调阶段会不停出bug。我把订单状态定义成如下链路待支付 → 待接单 → 待取餐/制作中 → 配送中 → 已完成中间穿插着已取消、已退款、异常单等分支状态。这个状态机的定义是所有业务逻辑的心脏后面章节我会专门展开讲。2. 后端核心架构与关键实现2.1 SpringBoot3项目骨架搭建与依赖版本锁定先说项目骨架。我使用IDEA的Spring Initializr生成项目Java版本选的17核心依赖包括Spring Web、Spring Validation、MyBatis-Plus、MySQL Driver、Lombok、Spring Data Redis用于缓存和Session管理、Spring AOP用于切面记录日志和权限验证。SpringBoot3版本的坑从一开始就存在比如传统连接池Druid的老版本在SpringBoot3下会报兼容错误需要指定druid-spring-boot-3-starter这个专门适配的版本很多新手在这一步就卡住了。最终的pom.xml里我用了这些关键坐标parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.0/version relativePath/ /parent dependencies !-- SpringBoot3 Web场景 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus注意SpringBoot3需要3.5.3版本 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency !-- MySQL驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Redis -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- knife4j 接口文档就是这个热词里提到的springboot3使用knife4j -- dependency groupIdcom.github.xiaoymin/groupId artifactIdknife4j-openapi3-jakarta-spring-boot-starter/artifactId version4.4.0/version /dependency /dependencies这里每一个人都要注意SpringBoot3下Knife4j、MyBatis-Plus、Druid这类组件一定要选带jakarta标识或者明确声明支持SpringBoot3的版本否则就会出现启动直接报ClassNotFoundException: javax.servlet.Filter之类的问题。接口文档用的是Knife4j 4.x版本。说实话以前用SpringFox那一套配SpringBoot3特别痛苦因为SpringFox已经停止维护了。Knife4j基于OpenAPI3规范在SpringBoot3上是开箱即用的只需在配置类里加OpenAPIDefinition和SecurityScheme注解就能让所有接口自动生成文档页面访问/doc.html能看到所有Controller的接口描述。这对我前后端联调的效率提升很大后面第4章会说具体用法。2.2 数据库设计八张核心表的关系与字段把控数据库是整个系统的地基。我强烈建议在写第一行业务代码前把表结构定清楚不然后面改表结构的成本极高。这个系统我设计了八张核心表表名核心字段说明userid, openid, nickname, avatar, phone, role, campus_address用户表role区分用户/商家/骑手merchantid, name, logo, description, business_status, rating商家信息表categoryid, merchant_id, name, sort菜品分类表dishid, merchant_id, category_id, name, price, image, stock, status菜品表cartid, user_id, dish_id, quantity, selected购物车表ordersid, order_no, user_id, merchant_id, rider_id, total_amount, status, remark订单主表order_itemid, order_id, dish_id, dish_name, dish_image, price, quantity订单明细表delivery_addressid, user_id, dorm_building, dorm_room, contact_name, contact_phone配送地址表订单主表是整个系统的核心关于状态字段我建议用Integer类型的枚举值存储而不要用字符串比如0待支付、1待接单、2待取餐、3配送中、4已完成、-1已取消。这样做的理由是数据库层面可以做索引比如统计某个商家的历史订单量时直接WHERE status 4的效率会好很多。还有一点经验之谈订单号一定要自己生成不要用数据库自增ID暴露给前端。我用的生成规则是yyyyMMddHHmmss 商家ID后三位 用户ID后四位 随机四位数字比如20250121123001001200012345这样既保证了唯一性也能从订单号直接反推出下单时间和商家排查问题非常方便。随机后四位我用的是RedisTemplate的increment操作生成自增序号再接一段随机数直接把相同时间、相同用户、相同商家的订单区分开。2.3 登录鉴权设计openid交换JWT令牌微信小程序登录流程很多刚接触的开发者容易误解以为需要用户输入账号密码。实际上小程序的登录逻辑非常固定小程序端调用wx.login()拿到临时code把code传给后端后端用code去微信官方接口换取openid和session_key然后后端生成自己的JWT令牌返回给小程序端后续所有请求都带上这个令牌就行。后端对应的核心代码大概是这样的PostMapping(/login) public Result login(RequestBody LoginRequest request) { // 1. 获取临时登录凭证 code String code request.getCode(); // 2. 请求微信接口获取 openid String url https://api.weixin.qq.com/sns/jscode2session?appid appId secret appSecret js_code code grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(response); String openid json.getString(openid); // 3. 判断用户是否已存在不存在则自动注册 User user userMapper.selectByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setRole(1); // 普通用户默认角色 user.setCreateTime(LocalDateTime.now()); userMapper.insert(user); } // 4. 生成JWT令牌 String token Jwts.builder() .setSubject(user.getId().toString()) .claim(role, user.getRole()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 3600 * 1000)) .signWith(secretKey) .compact(); return Result.success(token); }JWT令牌建议的有效期是7天因为学生手机端的微信每天都会打开过期后重新走一次静默登录完全没感知。注意当你后续要开发管理后台时不要复用这套JWT签名密钥或者至少要在token里加入role和audience字段区分来源防止普通用户的token直接调管理接口。2.4 订单状态机让每个状态流转都有迹可循订单状态是系统的主动脉。我之前见过很多人做这套系统时一个Controller里十几个if-else散落各处一旦订单处于某个中间状态代码根本猜不到下一步可以做什么。所以我自己专门写了一个OrderStatusHandler类把状态之间的合法迁移集中管理起来。我定义的合法迁移规则当前状态可执行操作目标状态待支付(0)用户支付待接单(1)待支付(0)用户取消/超时未付已取消(-1)待接单(1)商家接单待取餐(2)待接单(1)商家拒单已取消(-1)待取餐(2)骑手取餐配送中(3)配送中(3)骑手送达已完成(4)每个状态流转的方法里都要加状态校验防止前端重复点击导致状态错乱。比如用户支付成功后前端收到支付成功的回调同时还会触发轮询接口两次请求如果都到了后端第二次操作时订单已经是待接单状态了此时如果没检查就直接执行待支付→待接单是没问题的因为状态一致。但如果用户是在待接单状态下点了取消商家端同时也在点接单那么有一个请求会因为状态不匹配被拦截并抛出异常必须让前端捕获后刷新订单详情。这个状态机类核心就是一个ConcurrentHashMap存映射关系方法签名类似OrderStateTransitionResult tryTransition(oldStatus, targetStatus, operator)public boolean canTransition(Integer currentStatus, Integer targetStatus) { MapInteger, ListInteger map new HashMap(); map.put(0, Arrays.asList(1, -1)); map.put(1, Arrays.asList(2, -1)); map.put(2, Arrays.asList(3)); map.put(3, Arrays.asList(4)); return map.getOrDefault(currentStatus, Collections.emptyList()) .contains(targetStatus); }有了这个状态机之后微信支付回调的处理就简单了支付回调里验证完签名和金额后直接调用tryTransition(0, 1, system)如果返回失败说明订单状态已经不是待支付那就把回调标记为重复回调记录下来。不需要在每个业务方法里反复写状态判断。3. 微信小程序端开发实操3.1 项目初始化与目录结构设计小程序端我用的原生框架项目结构按页面维度划分。新建项目时首选使用微信开发者工具直接创建模板AppID建议申请一个测试号校园项目如果急着上线正式AppID的申请需要认证主体周期可能一周左右。目录结构这样设计最清晰├── app.js // 全局逻辑包含登录逻辑和全局用户信息 ├── app.json // 页面注册、窗口配置 ├── app.wxss // 全局样式 ├── utils/ │ ├── request.js // 封装 wx.request统一携带 token │ ├── config.js // 环境配置开发环境/生产环境地址切换 │ └── format.wxs // 时间格式化工具 ├── images/ ├── components/ │ ├── dish-card/ // 菜品卡片组件 │ ├── cart-bar/ // 购物车结算栏组件 │ └── order-card/ // 订单卡片组件 └── pages/ ├── index/ // 首页菜品分类与列表 ├── cart/ // 购物车 ├── order/ // 订单列表与详情 ├── profile/ // 个人中心 ├── address/ // 配送地址管理 ├── merchant/ // 商家管理 └── rider/ // 骑手接单大厅有一个非常容易被忽视的问题小程序顶部导航栏的适配。用默认导航栏时右上角胶囊按钮会挤压自定义导航栏的标题空间。我在做自定义导航栏时踩过坑后来直接使用系统导航栏只在首页做沉浸式轮播图时用了提升布局的navigationStyle: custom方案。自定义导航时需要用wx.getWindowInfo()获取状态栏高度和胶囊按钮位置动态计算导航栏高度这不仅仅是iOS和安卓的差异问题不同机型的刘海屏高度都不同建议封装一个navHeight的计算函数。3.2 用户登录链路从wx.login到token存储小程序的登录逻辑官网文档写得很简洁但实际操作有几个细节。第一wx.login()在每台设备每个会话的code只能用一次有效期为5分钟。如果你在app.js里面调用了两次wx.login()第二次获取的code再拿去后端换openid就会失败返回40029错误码。所以最好把登录封装成一个返回Promise的全局方法并且用一个isLogining的布尔变量防止并发触发重复请求。我最终的做法是维护一个登录Promise的单例模式// utils/login.js let loginPromise null; function login() { if (loginPromise) return loginPromise; loginPromise new Promise((resolve, reject) { wx.login({ success: (res) { request({ url: /api/user/login, method: POST, data: { code: res.code }, }).then((data) { const token data.token; wx.setStorageSync(token, token); resolve(token); }).catch(reject); }, fail: reject, }); }).finally(() { loginPromise null; }); return loginPromise; }这样无论页面同时多少个请求进来都只触发一次真实的登录请求。后续每次调用wx.request前都从storage里面读取token并追加到Header的Authorization字段。3.3 点餐与购物车交互数据怎么在小程序端流转点餐页是整个小程序端最核心的页面。左边是菜品分类栏右边是菜品列表底部悬浮购物车结算栏。这里有三个实现要点。第一个是分类切换的交互反馈。分类本质上是一个数组通过scroll-view来实现左右联动。右侧列表滚动时会触发onPageScroll或scroll-view的bindscroll事件根据当前滚动位置判断应该高亮哪个分类。这个联动逻辑写起来不难但要注意节流否则频繁触发setData会导致页面卡顿。第二个是购物车数据的本地缓存。学生在选择菜品时并没有真正下单购物车数据只存在于小程序端。我把购物车存在全局变量globalData.cartList里面同时同步到wx.setStorageSync(cartList, ...)做持久化防止用户手滑关闭小程序又打开后购物车丢失。购物车的数据结构是一个以dishId为key的Map包含dishId、name、price、quantity、image、stock等字段。第三个是购物车的价格计算。每次数量和选择状态变化时都要重新计算总价。这里有个坑浮点数精度问题。如果单价是3.5元数量是3份JavaScript里面3.5 * 3 10.499999999999998。所以我在工具函数里统一做了分转元的处理前端展示时后端传给小程序的price字段是整数分比如350表示3.5元计算逻辑全部用整数相加只在最终展示时除以100并保留两位小数。后端存储Price也用DECIMAL(10,2)或者直接用Integer单位分彻底规避精度问题。3.4 微信支付v3对接证书、回调与签名避坑记微信支付v3的对接绝对是我在开发过程中掉坑最多的地方。因为小程序的wx.requestPayment需要后端先调用微信支付API生成预支付订单拿到paySign等参数返回给前端然后前端拉起收银台。很多热词里提到 “小程序微信支付v3对接” 和 “无可用的平台证书”我就在这里卡住了很久。整个支付流程是这样的后端创建订单后调用微信支付v3接口下单参数包括appid、mchid、description、out_trade_no、amount.total、payer.openid最终返回prepay_id后端用prepay_id生成二次签名参数timeStamp、nonceStr、packageprepay_idxxx、signTypeRSA用商户私钥做SHA256WithRSA签名后返回给小程序端小程序端把这些参数传给wx.requestPayment微信客户端弹出支付确认框支付完成后微信服务器异步通知后端回调地址/api/pay/notify后端必须正确解密通知内容然后修改订单状态坑点一平台证书序列号。v3的报错信息里常出现 “请使用支付平台公钥验签” 或 “无可用的平台证书” 这类提示。这是因为微信支付v3的证书体系分为商户API证书用于请求签名和平台证书用于验证微信的响应与回调。老版本的sdk需要预先下载平台证书文件而新版本接口已经改为通过WechatPayHttpClientBuilder自动获取平台证书并缓存在本地只需要配置商户私钥、商户证书序列号和APIv3密钥即可。不要在商户平台手忙脚乱地“申请”什么那是个老误区。以wechatpay-java官方sdk为例配置类如下Bean public CloseableHttpClient wechatPayClient() { // 加载商户私钥 PrivateKey merchantPrivateKey PemUtil.loadPrivateKey( new FileInputStream(/path/apiclient_key.pem)); // 自动更新微信支付平台证书 AutoCertificateService.updateCertificate( wechatPayConfig.getMchId(), wechatPayConfig.getMerchantSerialNo(), merchantPrivateKey, wechatPayConfig.getApiV3Key(), wechatPayConfig.getPrivateKeyPath() ); return WechatPayHttpClientBuilder.create() .withMerchant(wechatPayConfig.getMerchantId(), wechatPayConfig.getMerchantSerialNo(), merchantPrivateKey) .withValidator(new AutoCertificateService.getValidator()) .build(); }坑点二回调验签。支付回调的请求体是加密的不是明文JSON。需要用平台证书或者APIv3密钥解密成明文的JSON然后取出out_trade_no和transaction_id再查库更新订单状态。我当时犯的错误是先验签再解密顺序搞反导致一直报验签失败。坑点三金额校验。回调里的amount.total是用户实际支付的金额单位为分。后端拿到金额后必须和订单表里的total_amount比对如果不一致绝对不能更新订单状态。这个不是为了防什么重大黑客而是防止支付和业务数据不一致导致的资损风险。哪怕只差一分钱都要记录日志并标记为异常单。4. 前后端联调与配送流转打通4.1 本地开发环境配置让小程序连上本机后端开发环境下小程序有一个很恼人的限制真机预览请求本机后端地址是不通的因为手机访问不了电脑的localhost。解决方案有两个。第一开发时使用微信开发者工具的“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”选项然后让小程序请求后端地址时填入电脑在局域网内的IP比如http://192.168.1.105:8080。注意同时要在IDEA中配置application.yml的server.address为0.0.0.0或者不管默认就是监听所有网卡。第二个方案是使用内网穿透工具把本机端口映射到一个公网域名上这样手机上直接用公网域名访问。但是注意这里必须加上安全组和权限控制否则开发期的接口就直接暴露了。我后来是在内网穿透服务上加了一层Token校验让只有携带小程序token的请求能通过。联调阶段强烈推荐在项目里接入Knife4j的/doc.html接口文档页面。前端同学看着Swagger文档调接口比直接翻后端代码高效得多。而且Knife4j支持在线调试不需要再用Postman单独维护一套请求地址了。启动后端后浏览器访问http://localhost:8080/doc.html在右上角把全局参数里的Authorization填上登录返回的token就能一键测试所有接口。联调中还经常用到的工具是抓包工具比如热词里提到的 Burp Suite 抓取 PC 端微信小程序。我说明一下开发期如果想看小程序的网络请求最直接的方法是用微信开发者工具自带的Network面板能看到每个请求的Headers、Payload和Response。如果你想分析别人线上小程序尤其是反编译这种操作我劝你打消这个念头这不仅涉及违规而且无助于你现在这个项目的开发。调试自己的接口用开发者工具就够了。4.2 配送模块实现从商家出餐到骑手送达的完整链路配送模块是这个系统里最容易做模糊的部分。我把它拆成两种模式商家自行配送和骑手抢单。校园场景下小食堂没有专职骑手更多的其实是众包式的谁有空谁抢单。数据库里orders表增加了rider_id和take_time、deliver_time字段。订单状态从待取餐流转到配送中这个动作必须是骑手端的“确认取餐”按钮触发。而骑手端看到哪些订单可以抢我的实现是当商家把订单状态从待接单改为待取餐即接单成功后立即出餐系统把这个订单自动拉入骑手端的待抢订单池。骑手端通过轮询接口拉取status 2 且 rider_id is null的订单列表点击抢单时后端要加乐观锁用SQL条件更新来防止两个骑手同时抢同一单public boolean grabOrder(Long orderId, Long riderId) { String sql UPDATE orders SET rider_id ?, status 3, take_time NOW() WHERE id ? AND (rider_id IS NULL OR rider_id 0) AND status 2; int rows orderMapper.updateRider(sql, riderId, orderId); return rows 1; }这个地方如果我当初不是用条件更新而是先查询再更新并发场景下就会有两个人同时抢到同一单那就尴尬了。4.3 消息通知机制订单状态变化怎么触达用户订单支付成功后用户希望第一时间知道商家已接单、骑手已出发。实现方案有几种小程序订阅消息、WebSocket长连接、短信通知。校园项目里短信通知成本高不划算我最终采用的是小程序订阅消息页面轮询双保险。小程序订阅消息的第一步是引导用户订阅。必须在用户点击“提交订单”按钮时调用wx.requestSubscribeMessage将tmplIds数组传给微信用户点允许后后端才能在下单成功时通过订阅消息模板给用户推送“支付成功通知”。这里有个反直觉的设计订阅是一次性的用户每次允许订阅只对应下一次推送所以小程序端要在需要通知的关键节点前引导用户再次订阅。页面轮询相对简单用户进入订单详情页后每3秒调用一次查询订单状态接口直到订单状态变为已完成或者用户离开页面就停止。这里注意不要写死在onLoad里忘记在onUnload清理定时器否则会导致离开页面后还在频繁请求接口浪费带宽和服务器资源。WebSocket这套方案我这里没有用但如果你订单量大、轮询压力大可以考虑服务端做WebSocket通道小程序端用wx.connectSocket。不过实际上校园订单量撑不住需要上WebSocket的量级轮询完全可以应付这也是很多人容易过度设计的地方。5. 常见问题与排查技巧实录开发这个系统的过程中我整理了十个高频问题很多都是网上搜不到明确答案但一旦踩到就很痛苦的这里集中列出来。问题现象原因解决方案小程序登录返回40029错误code重复使用或过期登录逻辑单例化确保一次会话内只发起一次code换openid支付报错无可用平台证书平台证书未正确加载或缓存使用官方sdk的AutoCertificateService配置APIv3密钥自动更新证书SpringBoot3启动报ClassNotFoundException: javax.servlet.*依赖版本不兼容SpringBoot3改用jakarta坐标版本Druid用druid-spring-boot-3-starter自定义导航栏在全面屏手机上顶部留白未适配状态栏高度使用wx.getWindowInfo().statusBarHeight动态计算导航栏高度购物车金额计算出现小数误差JavaScript浮点数精度问题金额统一用整数分存储和计算展示时再转元两个骑手同时抢到同一单先查询再更新导致并发冲突使用数据库条件更新UPDATE ... WHERE rider_id IS NULL商品库存超卖扣库存操作未加锁使用UPDATE语句带stock0条件扣减或用乐观锁支付回调收到多次微信网络重试机制回调幂等处理处理过的订单直接返回成功蓝牙搜索不到设备涉及多端联动调试时未申请蓝牙权限或未授权在app.json配置permission字段检查Android 12的附近设备权限5.1 微信环境兼容性从模拟器到真机的不一致问题开发过程中最让人头疼的问题之一就是模拟器一切正常真机上一跑各种幺蛾子。我用真机测试时遇到最典型的几个第一个是wx.request的域名问题。开发者工具勾选了“不校验合法域名”后模拟器能请求但真机上这个选项不生效必须把后端接口域名配置到小程序后台的 request 合法域名里并且要求域名有HTTPS证书。开发模式下很多人用内网穿透工具生成的临时域名但那个域名没有备案或证书真机往往无法请求。后来我的做法是后端绑定一个泛域名的HTTPS证书并让request的合法域名填主域名加端口然后在utils/config.js里用环境变量区分开发环境和生产环境请求前缀。第二个是安卓手机上wx.requestPayment拉起收银台时报403。排查下来是后台支付目录配置问题微信支付商户平台需要配置支付授权目录格式类似https://你的域名/pay/。目录配置错一点都不行必须精确到目录级别。这个坑网上讨论的人不多但遇到就会卡很久。第三个是iPhone的Safari下小程序的Date对象解析yyyy-MM-dd HH:mm:ss格式的时间字符串时返回Invalid Date。iOS不支持这种带横杠的时间格式必须替换成yyyy/MM/dd HH:mm:ss才能被正确解析。我一开始在订单详情页显示时间时iOS真机上一片空白排查了很久才发现是这个原因。后来统一封装了一个formatTime工具函数所有时间展示前都先转换。5.2 SpringBoot3实战经验热部署、日志与异常处理后端在SpringBoot3上的开发体验有几个细节值得单独聊一下。第一个是热部署。SpringBoot3搭配spring-boot-devtools实现代码热更新非常爽改完Java文件自动重启不用手动按CtrlF5。但注意devtools和IDEA的自动编译有联动需要开启Build project automatically选项。而且热部署时候Redis、MyBatis-Plus的缓存数据会保留如果改了实体类字段容易在运行时报字段映射错误这种情况直接手动重启一次解决就行。第二个是全局异常处理。我整个项目用一个RestControllerAdvice加ExceptionHandler统一处理异常。对于业务异常定义了一个BizException类包含错误码和错误信息前端根据错误码做不同提示。比如库存不足错误码是20001前端收到后自动清理购物车中对应的菜品并弹Toast提示“菜品已经卖完了”。第三个是接口日志切面。我写了一个AOP切面对所有Controller的请求进行日志记录包括请求路径、请求参数、响应结果、耗时。这个对排查线上问题特别有用比如某一单支付回调丢了翻日志可以看到回调到底有没有到达后端。日志格式尽量结构化用JSON格式输出后续如果接ELK或者Loki解析起来会方便很多。5.3 上线前必做的五件事最后聊上线。很多人的项目在本地跑得飞起一部署到服务器就崩了原因往往集中在环境差异、资源不足、配置硬编码这三类问题上。我列出上线前必做的五件事切换生产环境配置。application.yml里的数据库连接、Redis地址、微信支付商户号和密钥、AppSecret全部从环境变量或配置中心读取禁止硬编码写在代码里。配置HTTPS证书。小程序的request、uploadFile接口对域名有HTTPS强制要求用Nginx做反向代理在Nginx层配置SSL证书SSL证书可以申请免费的DV证书只是过期时间短记得设置自动续期或到期提醒。数据库自动备份。MySQL每天凌晨自动全量备份保留最近7天。同时开启binlog以备误操作导致的数据丢失可以按时间点恢复。这个校园系统虽然数据量不大但万一哪天数据库挂了用户点不了餐比自己吃不上饭还着急。页面加载性能优化。小程序首屏加载时不要一次性请求全部菜品按分类懒加载图片要做压缩。后端给前端返回的菜品列表接口加上Redis缓存缓存的失效时间设置为菜品上下架时手动刷新。实测下来首屏白屏时间能减少30%以上。准备降级预案。如果支付接口挂了用户在收银台无法完成支付系统要有提示并允许用户取消订单。如果订单量突然爆了后端接口响应变慢前端要有超时重试机制避免请求堆积。关于这套系统我最后再啰嗦几句我自己做完这套校园食堂点餐配送系统最大的感受是技术栈本身并没有多难真正的难点在于把校园场景下的业务流程理清楚把订单状态的流转、并发的库存扣减、支付回调的幂等性这些细节想明白。SpringBoot3加微信小程序这个组合非常成熟社区资料也多遇到问题基本都能搜到答案。如果你也在做类似的系统我建议先花一整周时间把数据库设计和状态机定义敲定不要急着写代码这个地基打好了后面开发会流畅得多。还有一个小技巧开发过程中任何一次修改数据库表结构都顺手更新一份字段说明文档放在项目根目录哪怕是个简单的Excel表格。因为两周后你回头看自己写的SQL可能已经记不清某个字段到底是取值为1表示正常还是0表示正常了。团队协作时这份文档更加重要它能避免无数个“这个字段是什么意思”的来回询问。