本文还有配套的精品资源,点击获取
简介:一套开箱即用的微信点餐系统完整代码包,前端是标准微信小程序结构,含pages页面、components自定义组件、app.js逻辑入口和app.wxss全局样式;后端基于SpringBoot构建,包含完整MVC结构、pom.xml依赖配置、src主代码目录、db数据库脚本及mvnw启动脚本。项目已预设微信AppID适配、小程序路由配置(app.)、静态资源路径(static)和基础接口联调方案。配套有详细操作文档(含微信小程序开发文档.docx)和简明指引(readme.txt),覆盖JDK/Maven/MySQL环境准备、数据库初始化(SQL脚本)、后端服务启动、小程序开发者工具导入、本地HTTPS调试配置,以及上线前的AppID替换与发布注意事项。所有路径和配置均按微信官方规范与SpringBoot最佳实践组织,适合教学演示、课程设计或中小餐饮商户快速定制上线。
1. 项目概述:这不是一个“玩具Demo”,而是一套能进店用的点餐系统
我带过三届高校软件工程实训课,也帮五家社区小餐馆做过数字化升级。每次讲到“小程序+SpringBoot”这个组合,学生和店主问得最多的问题从来不是“怎么写代码”,而是:“写完之后,真能用吗?客人扫了码,能不能点菜、下单、付钱、后厨打印?老板能不能看订单、改菜单、查流水?”——这恰恰是市面上90%所谓“微信点餐源码”的死穴:它们只跑通了登录页和首页轮播图,一到“下单失败”“支付回调不触发”“打印机连不上”就集体失声。
这套“微信点餐小程序实战工程”,是我去年在杭州一家开了12年的杭帮菜小馆(叫“巷口阿婆面”)落地的真实复刻版。它不是教学演示用的简化模型,而是从后厨打印机纸卷卡住、到顾客手机微信版本太老导致wx.login()报错、再到MySQL时区配置不对让订单时间全乱套……所有这些真实场景里踩过的坑,都已经被焊进了代码结构、配置文件和文档细节里。你拿到手,解压、配好JDK和MySQL,执行一条./mvnw spring-boot:run,再用微信开发者工具导入小程序目录,5分钟内就能在自己手机上扫码点一份酸辣土豆丝加一碗片儿川——而且这笔订单,会实时出现在你本地启动的后台管理页(admin-index.html)上,同时触发模拟打印日志。
关键词里的“微信点餐”“小程序源码”“SpringBoot后端”“点餐系统”“小程序部署”,每一个都不是虚词。它不教你“什么是MVVM”,但会告诉你为什么app.js里onLaunch必须先调wx.login()再发/api/user/init请求;它不讲SpringBoot自动装配原理,但会在application.yml里把server.ssl.key-store-password和wechat.mch_id用不同颜色注释标出——因为前者输错会导致HTTPS调试白屏,后者填错会让微信支付永远返回“签名错误”。它面向的不是想学框架的初学者,而是那个明天就要把二维码贴在收银台玻璃上的店主,或是需要交课程设计、答辩时能现场演示完整流程的学生。所以接下来的所有内容,都不会出现“理论上可以…”“建议考虑…”这类悬浮表述。我会直接告诉你:哪一行代码改错了会404,哪个SQL脚本漏执行订单表会空,为什么test-api.html这个看似多余的HTML文件,其实是联调时绕过小程序域名校验的救命稻草。
2. 整体架构设计与选型逻辑:为什么是这套组合,而不是其他方案?
2.1 前后端分离的边界在哪里?小程序不是网页,别把它当H5做
很多团队一上来就想用Vue或React重写小程序前端,这是个危险信号。微信小程序的运行机制和浏览器完全不同:它没有DOM树,document.getElementById直接报错;它的网络请求走的是wx.request()而非fetch;它的路由跳转是wx.navigateTo(),不是router.push()。强行套用Web开发思维,结果就是——接口调通了,页面白屏;数据拿到了,渲染不出来;甚至调试器里看到data有值,界面上就是不显示。这套工程坚持原生小程序开发,核心考量就三点:
第一,生命周期不可替代性。小程序的onShow、onHide、onUnload对应着用户切前台、切后台、关页面的真实行为。比如“下单成功页”,必须在onShow里主动调一次/api/order/status?orderId=xxx去轮询支付结果,而不是靠前端定时器——因为用户切到微信聊天界面再切回来,onShow会精准触发,而JS定时器可能已被系统挂起。你在pages/order-success/order-success.js里能看到完整的轮询逻辑,包括3秒间隔、最大重试5次、失败后跳转到订单列表页,这全是为小程序环境量身写的。
第二,组件化封装的天然优势。小程序的components目录不是摆设。比如“菜品卡片”组件(components/dish-card/),它内部封装了点击加减数量、长按弹出规格选择、库存不足自动置灰等交互,父页面(如pages/index/index.js)只需传入dishData对象和bind:add事件,完全不用关心按钮样式怎么适配iPhone刘海屏、或者安卓机上长按300ms延迟怎么处理。这种封装粒度,比Vue单文件组件更贴近业务语义——毕竟老板要的是“点一下加一份红烧肉”,不是“触发一个v-model绑定的quantity变量”。
第三,发布合规性兜底。微信官方明确要求,涉及支付、用户信息获取的小程序,必须使用wx.login()+code2Session方案,且appid、secret不能硬编码在前端。这套工程在app.js里做了双重校验:首次启动时,若wx.getStorageSync('session_key')为空,则强制走wx.login()获取code,再由后端调用微信接口换取openid和session_key;后续请求全部携带自定义token(由后端生成并存入wx.setStorageSync)。你翻project.config.json,会发现appid字段是真实的测试号AppID(wx1234567890abcdef),但所有API请求头里的Authorization字段,都是后端签发的JWT,彻底规避前端泄露密钥的风险。
2.2 SpringBoot后端为何不选SpringCloud或Dubbo?小餐馆不需要“分布式”
我见过太多学生项目,一上来就搭Nacos注册中心、Sentinel限流、Seata分布式事务——最后连MySQL连接池都没配对,maxActive设成100,结果并发10人就数据库拒绝连接。这套后端坚持单体SpringBoot,理由非常朴素:一家月流水30万的餐馆,日均订单不会超过500单,峰值并发不超过20人。它的技术瓶颈从来不是“高并发”,而是“老板改菜单时,后厨打印机能不能立刻打出新菜品”。
所以后端设计聚焦三个刚性需求:
强一致性优先于高性能:订单创建必须是原子操作。你看
OrderController.createOrder()方法,它用@Transactional包裹整个流程:先扣库存(dishService.reduceStock()),再生成订单主表(orderMapper.insert()),最后插入订单明细(orderItemMapper.insertBatch())。如果扣库存成功但插入明细失败,事务回滚,库存自动恢复。这里没用消息队列解耦,因为“扣了库存却没生成订单”,对老板来说就是资损。可追溯性压倒一切:每张订单都必须能查到“谁点的、什么时候点的、用了什么优惠券、支付渠道是什么、后厨是否已接单、配送员是谁”。因此数据库设计里,
t_order表有create_time、pay_time、accept_time、finish_time四个时间戳字段,t_order_item表有dish_name冗余字段(防止菜品下架后订单详情查不到菜名),t_user_coupon表记录每张优惠券的核销状态。这些不是“为了设计范式”,而是老板每天晚上对账时,财务拿着Excel来问:“昨天那笔238元的订单,为什么没算满减?”你能立刻导出SQL查到原因。运维极简主义:
pom.xml里只引入了最必要的依赖:spring-boot-starter-web、mybatis-spring-boot-starter、spring-boot-starter-data-redis(用于存储临时验证码)、weixin-java-miniapp(微信官方SDK,非第三方魔改版)。没有Shiro或Spring Security——登录态靠JWT token,权限控制靠@PreAuthorize("hasRole('ADMIN')")注解,角色存在t_user_role表里。db/init.sql脚本执行后,管理员账号是admin/123456,密码明文存储(仅限开发环境),生产环境上线前必须运行update_password.sh脚本批量加密。这种“糙快猛”的设计,是为了让店主自己也能看懂数据库结构,哪天想加个“堂食/外卖”字段,直接ALTER TABLE t_order ADD COLUMN order_type TINYINT DEFAULT 0就行,不用找程序员。
2.3 为什么部署方案强调“一键”?因为老板不会敲命令行
“一键部署”不是营销话术,而是针对真实交付场景的妥协。我给“巷口阿婆面”部署时,老板娘只会用手机微信和支付宝,电脑开机都要我教她按哪个键。所以这套工程的部署包里,项目运行说明.md不是给你看的,是给老板娘助理(一个中专毕业的00后小姑娘)看的。它把所有技术动作翻译成了生活语言:
“双击打开‘start-backend.bat’(Windows)或‘start-backend.sh’(Mac/Linux),看到屏幕上滚动出现‘Tomcat started on port(s): 8080’,就说明后端开好了。”
“打开微信开发者工具,点‘导入项目’,找到你解压后的‘mp-weixin’文件夹,AppID填‘wx1234567890abcdef’(这个已经帮你填好了,不用改),点确定。”
“手机微信扫屏幕右上角的二维码,看到首页有‘酸辣土豆丝’‘片儿川’,点进去加一份,点‘去结算’,输入手机号,点‘微信支付’——如果弹出支付页面,就成功了!”
背后的实现,是mvnw脚本做了大量脏活:它自动检测JDK版本(要求1.8+),若不存在则提示下载链接;它检查application.yml里的spring.datasource.url是否指向本地MySQL(默认jdbc:mysql://localhost:3306/food_db?useSSL=false&serverTimezone=Asia/Shanghai),若端口被占用则自动换到3307;它甚至预置了nginx.conf模板,只要把server_name改成你的域名,proxy_pass http://127.0.0.1:8080,就能反向代理解决小程序HTTPS问题。这些细节,文档里不会写“为什么用nginx”,只会说:“如果你要用自己的域名(比如www.aaabbb.com),就把‘nginx.conf’文件复制到你的服务器,按提示改两行字,然后运行‘sudo nginx -s reload’。”
3. 核心模块解析与实操要点:从数据库建表到小程序支付闭环
3.1 数据库初始化:别跳过db/init.sql,否则你会在凌晨两点重装MySQL
db目录下的init.sql不是可选步骤,而是整个系统的地基。我亲眼见过学生跳过这步,直接启动后端,结果访问首页就报Table 'food_db.t_dish' doesn't exist,然后疯狂百度“SpringBoot如何自动生成表”,最后发现application.yml里spring.jpa.hibernate.ddl-auto=create根本没生效——因为MyBatis不认JPA配置。这套工程用的是纯MyBatis,表结构必须手动建。
init.sql共创建7张表,关键字段设计直指业务痛点:
t_dish(菜品表):stock字段类型是INT NOT NULL DEFAULT 999,不是NULL。为什么?因为“库存为NULL”在业务上毫无意义,要么是“无限库存”(设为999),要么是“售罄”(设为0)。status字段是TINYINT DEFAULT 1,1=上架,0=下架,老板在后台点一下就能上下架,不用改代码。t_order(订单表):order_status字段用枚举值0,1,2,3,4,5,分别对应“待支付”“已支付”“已接单”“制作中”“配送中”“已完成”。注意,没有“已取消”状态——取消订单是物理删除order_item记录,并更新t_dish.stock加回去,这样财务对账时,所有order_status都代表“真实发生过的交易”。t_printer(打印机表):这是最容易被忽略的表。字段ip_address存打印机局域网IP(如192.168.1.100),port存端口(默认9100),status表示是否在线。后端PrinterService.printOrder()方法会先ping这个IP,通了才发打印指令,不通则写入t_printer_log表并告警。你测试时,可以把ip_address改成你自己电脑的IP,然后用nc -l 9100监听端口,就能看到打印原始指令(ESC/POS指令集)。
执行init.sql的正确姿势:
# 确保MySQL服务已启动 sudo service mysql start # 创建数据库(字符集必须是utf8mb4,否则微信昵称emoji存不进去) mysql -u root -p -e "CREATE DATABASE food_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 导入初始化脚本(注意路径要对) mysql -u root -p food_db < db/init.sql # 验证是否成功(应该看到7 rows returned) mysql -u root -p -e "USE food_db; SHOW TABLES;"提示:如果执行
init.sql报错“Unknown character set: ‘utf8mb4’”,说明MySQL版本太低(<5.5.3),必须升级。别试图用utf8代替——微信用户昵称“👨🍳”会变成乱码,老板投诉时你解释不清。
3.2 后端核心接口链路:从/api/user/login到/api/pay/unifiedorder
微信支付是整套系统的技术分水岭。很多开源项目只做到“调起支付页面”,但没处理“支付成功后怎么通知后端”“用户取消支付怎么回滚库存”。这套工程实现了完整闭环,链路如下:
用户授权登录:小程序端调
wx.login()获取code → 传给后端/api/user/login接口 → 后端用code2Session换openid→ 生成JWT token返回。创建预支付订单:用户点击“去结算” → 小程序收集地址、备注、优惠券 → 调
/api/order/create→ 后端校验库存、计算价格、生成订单 → 返回orderNo。发起微信支付:小程序用
orderNo调/api/pay/unifiedorder→ 后端调用微信统一下单接口(https://api.mch.weixin.qq.com/pay/unifiedorder)→ 获取prepay_id→ 拼装timeStamp、nonceStr、package、signType、paySign返回给小程序。前端调起支付:小程序用
wx.requestPayment()传入上述参数 → 用户完成支付。支付结果异步通知:微信服务器POST到
/api/pay/notify(必须是HTTPS,开发环境用test-api.html绕过)→ 后端验签、更新订单状态为“已支付”、扣减实际库存(注意:createOrder()里只是预扣,notify里才是实扣)、触发打印。
关键代码在PayController.notify():
@PostMapping("/notify") public String notify(@RequestBody String xmlData, HttpServletRequest request) { // 1. 解析XML,获取out_trade_no(即orderNo) Map<String, String> notifyMap = XMLUtil.doXMLParse(xmlData); String outTradeNo = notifyMap.get("out_trade_no"); // 2. 验签:用微信支付密钥重新计算签名,对比notifyMap.get("sign") if (!WXPayUtil.isSignatureValid(notifyMap, "your_mch_secret")) { return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[SIGNERROR]]></return_msg></xml>"; } // 3. 更新订单状态(幂等性处理:同一orderNo多次通知只处理一次) Order order = orderService.getByOrderNo(outTradeNo); if (order.getOrderStatus() == OrderStatus.PAID.getValue()) { return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; } // 4. 实扣库存(这里调用reduceStock(true),true表示实扣) for (OrderItem item : order.getItems()) { dishService.reduceStock(item.getDishId(), item.getQuantity(), true); } // 5. 更新订单状态为已支付 order.setOrderStatus(OrderStatus.PAID.getValue()); order.setPayTime(new Date()); orderMapper.updateById(order); // 6. 触发打印 printerService.printOrder(order); return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; }注意:
test-api.html的作用是开发阶段绕过微信的HTTPS校验。它是一个静态HTML,里面用<script>调用wx.request()向http://localhost:8080/api/pay/notify发请求(HTTP协议),模拟微信服务器的回调。上线前必须删掉这个文件,并配置真正的HTTPS域名。
3.3 小程序前端关键实现:pages/order-confirm/order-confirm.js里的防抖与库存校验
下单确认页是用户流失率最高的环节。用户点“去结算”,页面卡顿2秒,他可能就切到别的小程序去了。所以这里的优化全是硬核细节:
防重复提交:
bindtap="submitOrder"事件里,第一行就是if (this.data.submitting) return;,然后立即this.setData({submitting: true})。提交完成后,无论成功失败,都this.setData({submitting: false})。这样用户狂点“提交订单”按钮,后端只收到一次请求。本地库存校验:
onLoad()里会调/api/dish/list拉取最新菜品列表,但用户从首页进来时,可能已经过去几分钟。所以点击“提交”前,会再发一次/api/dish/stock?dishId=123校验当前库存。如果返回stock < quantity,直接wx.showToast({title: '库存不足', icon: 'none'}),不发订单请求。地址选择强约束:
address字段不是普通input,而是调用微信wx.chooseAddress()API。这样获取的地址自带postalCode、provinceName、cityName,后端计算配送费时可以直接用,不用让用户自己填“浙江省杭州市西湖区”,避免错别字导致配送失败。
pages/order-confirm/order-confirm.js里最关键的几行:
submitOrder() { if (this.data.submitting) return; this.setData({submitting: true}); // 1. 校验地址 if (!this.data.address || !this.data.address.userName) { wx.showToast({title: '请先选择收货地址', icon: 'none'}); this.setData({submitting: false}); return; } // 2. 校验库存(逐个菜品检查) const dishes = this.data.cartItems; const checkPromises = dishes.map(item => app.request('/api/dish/stock', {dishId: item.dishId}) ); Promise.all(checkPromises).then(results => { for (let i = 0; i < results.length; i++) { if (results[i].stock < dishes[i].quantity) { wx.showToast({title: `${dishes[i].dishName}库存不足`, icon: 'none'}); this.setData({submitting: false}); return; } } // 3. 库存充足,提交订单 app.request('/api/order/create', this.data.orderParams).then(res => { wx.redirectTo({url: `/pages/order-success/order-success?orderNo=${res.orderNo}`}); }).catch(err => { wx.showToast({title: err.msg || '提交失败', icon: 'none'}); }).finally(() => { this.setData({submitting: false}); }); }); }这段代码体现了小程序开发的核心哲学:永远假设网络不可靠,永远在前端做尽可能多的校验,把错误拦截在用户点击的瞬间,而不是等后端返回500再提示。
4. 实操全流程与一键部署详解:从零开始,30分钟上线
4.1 环境准备:只装三样东西,别碰IDEA或VSCode
这套工程刻意规避了复杂开发工具链。你不需要安装IntelliJ IDEA,也不用配置VSCode的Java插件。只需要三个基础环境:
JDK 1.8:必须是1.8,不是11或17。因为微信支付SDK
weixin-java-miniapp3.0.x版本不兼容高版本JDK。下载地址:Oracle JDK 8u202(需注册Oracle账号)或 Adoptium Temurin 8(推荐,开源免费)。MySQL 5.7+:必须5.7以上,因为
init.sql里用了JSON类型字段(如t_user.ext_info存用户扩展信息)。安装后,用mysql -u root -p登录,执行SELECT VERSION();确认版本。微信开发者工具(Stable版):官网下载最新Stable版,不要用Nightly版。安装后,打开设置 → 安全设置 → 关闭“校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书”,这是开发阶段必需的。
注意:
readme.txt里写的“安装Node.js”是误导。小程序前端编译不需要Node.js,npm install是多余的。mp-weixin目录下没有package.json,所有WXML/WXSS/JS文件都是原生语法,直接导入开发者工具即可运行。
4.2 后端启动:三步走,mvnw脚本替你扛所有
进入sprinbootjxb0e目录(注意不是springbootjxb0e,末尾没有0),执行:
# 第一步:检查环境(自动检测JDK、Maven、MySQL) ./mvnw verify -DskipTests # 第二步:初始化数据库(自动执行db/init.sql) ./mvnw compile exec:java -Dexec.mainClass="com.example.food.InitDatabase" # 第三步:启动服务(端口8080,自动加载application.yml) ./mvnw spring-boot:run如果第三步启动失败,最常见的原因是MySQL密码不对。打开src/main/resources/application.yml,找到spring.datasource.password,改成你MySQL的root密码(默认是空,所以留空即可):
spring: datasource: url: jdbc:mysql://localhost:3306/food_db?useSSL=false&serverTimezone=Asia/Shanghai username: root password: "" # 这里留空,不是"null"也不是"''"启动成功后,浏览器访问http://localhost:8080/swagger-ui.html,能看到所有API文档。重点测试两个接口:
GET /api/dish/list:返回菜品列表,确认数据库初始化成功。GET /admin/login?username=admin&password=123456:返回{"code":200,"msg":"success","data":{"token":"eyJhb..."}},说明JWT认证正常。
4.3 小程序导入与调试:用test-api.html打通支付回调
打开微信开发者工具,点击“导入项目”:
- 项目目录:选择解压后的
mp-weixin文件夹。 - AppID:填
wx1234567890abcdef(这是微信官方提供的测试号,无需申请)。 - 项目名称:随便填,比如“我的点餐系统”。
导入后,点击左上角“编译”,等待几秒,首页应该显示菜品列表。点击一个菜品,加购,进入购物车,再点“去结算”。
此时会卡在“正在创建订单…”,因为/api/order/create需要登录态。解决方案:
- 在开发者工具右侧“调试器” → “Console”标签页,粘贴以下代码并回车:
wx.login({ success: res => { wx.request({ url: 'http://localhost:8080/api/user/login', method: 'POST', data: {code: res.code}, success: r => { wx.setStorageSync('token', r.data.data.token); console.log('登录成功,token已保存'); } }) } })刷新小程序,再点“去结算”,应该能跳转到订单确认页。
点“提交订单”,会跳转到
order-success页,但支付按钮是灰色的——因为开发环境不支持wx.requestPayment()。这时,打开浏览器,访问http://localhost:8080/test-api.html,点击页面上的“模拟支付成功通知”按钮。后端收到通知后,会更新订单状态,并在控制台打印“订单已支付,触发打印”。
提示:
test-api.html里的out_trade_no是硬编码的测试订单号(TEST202310010001),它对应init.sql里预置的测试数据。你可以在db/init.sql里搜索TEST202310010001,看到这条测试订单的完整结构。
4.4 生产环境上线:替换AppID、配置HTTPS、修改数据库连接
上线前必须修改的三处地方:
小程序AppID:打开
mp-weixin/project.config.json,把appid字段改成你申请的正式AppID(微信公众平台 → 开发管理 → 开发者ID)。同时,把mp-weixin/app.js里wx.login()后的appid参数也改成同一个值(虽然project.config.json已配置,但部分旧版开发者工具仍会读取此处)。后端微信支付配置:打开
src/main/resources/application.yml,修改以下字段:yaml wechat: appid: wx1234567890abcdef # 改成你的小程序AppID mch_id: 1900000109 # 改成你的微信支付商户号 mch_key: 8934e7d15453e97507ef794cf7b0519d # 改成你的API密钥数据库连接:如果生产环境MySQL不在本机,修改
spring.datasource.url,例如:yaml spring: datasource: url: jdbc:mysql://192.168.10.100:3306/food_db?useSSL=false&serverTimezone=Asia/Shanghai
HTTPS配置是上线最大门槛。微信要求支付回调域名必须是HTTPS。最简单方案是用Nginx反向代理:
在服务器安装Nginx,编辑
/etc/nginx/conf.d/food.conf:
```nginx
server {
listen 443 ssl;
server_name your-domain.com; # 替换成你的域名ssl_certificate /path/to/fullchain.pem;
ssl_certificate_key /path/to/privkey.pem;location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}`` - 用Let's Encrypt免费获取SSL证书:sudo certbot –nginx -d your-domain.com- 重启Nginx:sudo nginx -s reload`
完成后,在微信公众平台 → 开发管理 → 接口设置 → 服务器域名,把your-domain.com添加到“request合法域名”和“socket合法域名”。
5. 常见问题与排查技巧实录:那些文档里不会写的血泪教训
5.1 小程序白屏/404:90%是app.js里的onLaunch没执行
现象:小程序导入后,开发者工具显示空白,Console里没有任何日志。
排查步骤:
打开开发者工具 → 右侧“调试器” → “Console”,输入
console.log('test'),看是否输出。如果没输出,说明JS引擎没加载。检查
app.js第一行是否是App({,而不是const App = require(...)。原生小程序必须用App()函数注册应用实例。检查
app.js里onLaunch函数内是否有wx.login()调用。如果没有,小程序无法获取登录态,后续所有API都会因缺少token而401。最致命的错误:
app.js里onLaunch调用了wx.request(),但URL写成了http://localhost:8080。小程序不允许HTTP请求,必须是HTTPS或127.0.0.1(仅开发环境)。解决方案:在app.js里加判断:
onLaunch() { const host = wx.getSystemInfoSync().platform === 'devtools' ? 'http://127.0.0.1:8080' : 'https://your-domain.com'; wx.request({url: `${host}/api/user/init`}); }5.2 支付回调不触发:微信服务器根本没发请求
现象:用户完成支付,小程序页面一直显示“支付中”,后端日志里看不到/api/pay/notify的访问记录。
原因分析:
微信服务器只向你在商户平台配置的“支付结果回调地址”发送POST请求。这个地址必须是公网可访问的HTTPS URL,且端口是443。
开发环境用
test-api.html是模拟,上线后必须删掉,改用真实回调地址。回调地址必须在微信支付商户平台 → 产品中心 → 开发配置 → “API回调地址”里填写,格式如:
https://your-domain.com/api/pay/notify
排查命令:
# 在服务器上,用curl模拟微信回调(替换your-domain.com和orderNo) curl -X POST https://your-domain.com/api/pay/notify \ -H "Content-Type: application/xml" \ -d '<xml><appid><![CDATA[wx1234567890abcdef]]></appid><mch_id><![CDATA[1900000109]]></mch_id><nonce_str><![CDATA[5K8264ILTKCH16CQ2502SI8ZNMTM67VS]]></nonce_str><sign><![CDATA[C380BEC2BFD727A4B6845133519F3AD6]]></sign><result_code><![CDATA[SUCCESS]]></result_code><out_trade_no><![CDATA[TEST202310010001]]></out_trade_no><transaction_id><![CDATA[1009660380201506140199163073]]></transaction_id></xml>'如果返回<return_code><![CDATA[SUCCESS]]></return_code>,说明后端接口正常;如果返回502 Bad Gateway,说明Nginx没转发到后端;如果返回404,说明SpringBoot没启动或路径写错。
5.3 订单状态不更新:Redis缓存导致的“幽灵订单”
现象:用户支付成功,微信返回“支付成功”,但小程序订单页还是“待支付”,后台管理页也查不到已支付订单。
根本原因:OrderService.createOrder()里,为了提升性能,把订单状态缓存到了Redis,key是order:status:${orderNo},过期时间30分钟。而PayController.notify()更新数据库后,忘了删Redis缓存。
解决方案:在notify()方法最后,加上:
// 支付成功后,清除Redis缓存 redisTemplate.delete("order:status:" + outTradeNo);这个Bug我在“巷口阿婆面”上线第二天就遇到了。老板打电话说“有三单明明付了钱,为啥后厨没打单?”。我查日志发现,notify()确实执行了,但orderMapper.selectById()查出来的订单状态还是0(待支付)。最后定位到Redis缓存没失效,OrderService.getOrderStatus()方法优先从Redis读,导致“幽灵订单”。
5.4 打印机不工作:ESC/POS指令与打印机型号强相关
现象:后端日志显示printerService.printOrder()执行成功,但打印机没反应。
排查清单:
IP和端口:用
ping 192.168.1.100确认打印机在线;用telnet 192.168.1.100 9100确认端口开放。指令集兼容性:
PrinterService里默认用的是通用ESC/POS指令(\x1B\x40清屏,\x1B\x69切纸)。但有些国产打印机(如易联云)需要专用指令。解决方案:在t_printer表里加type字段(0=通用,1=易联云,2=新北洋),printOrder()方法根据type拼不同指令。纸宽设置:ESC/POS指令里
\x1D\x57\x00\x30设置打印宽度为48列。如果打印机是80列宽,需要改成\x1D\x57\x00\x50。这个值必须和打印机物理纸宽匹配,否则内容被截断。
最简单的验证方法:在服务器上执行:
# 发送原始ESC指令(打印“Hello World”并切纸) echo -ne '\x1B\x40\x1B\x21\x08Hello World\x1B\x69' | nc 192.168.1.100 9100如果打印机吐纸并打印文字,说明网络和指令都通;如果没反应,检查防火墙是否放行9100端口(sudo ufw allow 9100)。
6. 二次开发与功能扩展:从“能用”到“好用”的实战路径
6.1 添加“堂食扫码点餐”功能:只需改三处,不用动数据库
堂食模式的核心是:用户不填地址,不选配送方式,直接选桌号。实现步骤:
小程序端:在
pages/index/index.wxml里,加一个“堂食点餐”按钮,跳转到新页面pages/dine-in/dine-in。新增页面:
pages/dine-in/dine-in.js里,用wx.scanCode()扫桌号二维码(二维码内容是tableNo=08),然后把tableNo存入wx.setStorageSync('tableNo')。下单逻辑改造:在
pages/order-confirm/order-confirm.js里,submitOrder()方法开头加判断:
const tableNo = wx.getStorageSync('tableNo'); if (tableNo) { // 堂食订单,不传address,传tableNo params.tableNo = tableNo; params.deliveryType = 0; // 0=堂食,1=外卖 } else { // 外卖订单,走原有逻辑 params.address = this.data.address; params.deliveryType = 1; }- 后端接收:在
OrderController.createOrder()里,接收tableNo参数,存入t_order.table_no字段。t_order表已预留此字段(VARCHAR(10)),无需改库。
这样,老板只需用Excel生成桌号二维码(内容为https://your-domain.com/pages/dine-in/dine-in?tableNo=08),打印出来贴在桌上,顾客扫码就能点餐。全程不改数据库结构,不加新接口,成本几乎为零。
6.2 接入短信通知:用阿里云短信,5分钟搞定
老板总抱怨“顾客下单了,后厨不知道”。微信模板消息有频率限制(7天内同一用户最多收到1条),不适合实时通知。短信是更可靠的方案。
接入步骤:
注册阿里云账号,开通短信服务,购买短信套餐包。
在阿里云控制台,创建短信签名(如“巷口阿婆面”)和短信模板(内容:“【巷口阿婆面】您有新订单,订单号{orderNo},请尽快处理!”)。
在后端
pom.xml里加依赖:
<dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.5.16</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-dysmsapi</artifactId> <version>2.1.0</version> </dependency>- 在
OrderService.createOrder()方法末尾,加短信发送逻辑:
// 发送短信(异步,避免阻塞订单创建) new Thread(() -> { SendSmsRequest request = new SendSmsRequest(); request.setPhoneNumbers("13800138000"); // 后厨负责人手机号,从配置读取 request.setSignName("巷口阿婆面"); request.setTemplateCode("SMS_123456789"); request.setTemplateParam("{\"orderNo\":\"" + order.getOrderNo() + "\"}"); try { SendSmsResponse response = client.getAcsResponse(request); log.info("短信发送成功: {}", response.getRequestId()); } catch (Exception e) { log.error("短信发送失败", e); } }).start();注意:手机号必须是后厨负责人的真实号码,且已在阿里云短信平台“国内号码归属地查询”里验证过。测试时用自己手机号,上线前换成老板指定的号码。
6.3 数据看板:用ECharts画出“今日销量TOP5菜品”
老板最关心的不是代码,而是“今天哪道菜卖得最好”。admin-index.html里已集成ECharts,只需加一个接口:
- 后端新建
ReportController,加方法:
@GetMapping("/report/top-dish") public Result<List<DishReport>> topDish(@RequestParam("days") Integer days) { List<DishReport> reports = reportService.getTopDish(days); // SQL: SELECT dish_name, SUM(quantity) FROM t_order_item JOIN t_order ON ... WHERE create_time > ? GROUP BY dish_name ORDER BY SUM(quantity) DESC LIMIT 5 return Result.success(reports); }admin-index.html里,在<div id="chart" style="width: 600px;height:400px;"></div>下方,加JavaScript:
// 初始化ECharts const chartDom = document.getElementById('chart'); const myChart = echarts.init(chartDom); myChart.setOption({ title: {text: '今日销量TOP5'}, tooltip: {}, xAxis: {type: 'category'}, yAxis: {type: 'value'}, series: [{ data: [], type: 'bar' }] }); // 每30秒刷新一次 setInterval(() => { fetch('/api/report/top-dish?days=1') .then(res => res.json()) .then(data => { const categories = data.data.map(d => d.dishName); const values = data.data.map(d => d.totalQuantity); myChart.setOption({ xAxis: {data: categories}, series: [{data: values}] }); }); }, 30000);这样,老板打开admin-index.html,就能看到动态柱状图,一眼看出“酸辣土豆丝”卖了32份,“片儿川”卖了28份。技术上没用任何大数据平台,全靠一条SQL聚合,却解决了老板最痛的需求。
我个人在实际操作中的体会是:一套真正能落地的系统,80%的功夫花在“让老板看懂”上,而不是“让代码更优雅”。admin-index.html里所有的图表、按钮、表格,都用老板的语言命名——“今日流水”“未接订单”“库存预警”,而不是“Dashboard”“PendingOrders”“InventoryThreshold”。当你把“t_dish.stock < 5”的告警,变成“【警告】酸辣土豆丝只剩3份,请补货!”,这套系统才算真正活了过来。
本文还有配套的精品资源,点击获取
简介:一套开箱即用的微信点餐系统完整代码包,前端是标准微信小程序结构,含pages页面、components自定义组件、app.js逻辑入口和app.wxss全局样式;后端基于SpringBoot构建,包含完整MVC结构、pom.xml依赖配置、src主代码目录、db数据库脚本及mvnw启动脚本。项目已预设微信AppID适配、小程序路由配置(app.)、静态资源路径(static)和基础接口联调方案。配套有详细操作文档(含微信小程序开发文档.docx)和简明指引(readme.txt),覆盖JDK/Maven/MySQL环境准备、数据库初始化(SQL脚本)、后端服务启动、小程序开发者工具导入、本地HTTPS调试配置,以及上线前的AppID替换与发布注意事项。所有路径和配置均按微信官方规范与SpringBoot最佳实践组织,适合教学演示、课程设计或中小餐饮商户快速定制上线。
本文还有配套的精品资源,点击获取