ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

三端酒店管理系统实战:权限、微信支付与并发避坑

三端酒店管理系统实战:权限、微信支付与并发避坑 简介一套面向毕业设计及酒店管理开发学习的一体化酒店管理系统涵盖后台管理端、门户网站和微信小程序三端支持在线预订、订单管理、房态实时推送、点餐绑定房间、微信支付与退款等核心业务。资源共1477个文件以515个js、193个wxss、186个wxml、191个json为主分别对应后端逻辑、小程序样式、页面结构与配置数据另有hotel.sql示例脚本和项目说明文档压缩包约52.77MB目录结构清晰便于定位和阅读。目前已有87人浏览学习。该项目后端采用Egg.js集成Socket.io、JWT与Redis网站前端基于ReactAnt Design小程序使用Vant Weapp数据库通过Sequelize管理并配有VSCode配置、测试用例及迁移种子文件适合需要快速启动酒店管理项目或参考完整前后端分离方案的学习者可直接导入数据库并对照文档梳理业务模块。1. 一体化酒店管理系统三端协同到底在解决什么问题“一体化酒店管理系统”这六个字拆开看其实是三件事给酒店前台和老板用的管理后台、给客人看房下单的官网以及用户随手打开就能订房点餐的微信小程序。三端共享同一个订单与房态数据源再挂上微信支付就构成了一套从客人下单、前台排房、餐厅点单到资金到账都在一个后台闭环的迷你酒店 ERP。它最适合两类人一类是中小酒店和民宿老板受够了多套系统各管各的数据孤岛另一类是做外包或全栈自学的开发者想找一个后台管理系统、微信小程序和微信支付接口一次串齐的工程做地基。下面按这套源码最常见的落地路径从架构拆到数据模型再讲支付与部署最后把最容易翻车的坑逐个点名。2. 先把三端架构立住后台/网站/小程序的分工与技术选型2.1 管理后台为什么要按角色和权限拆而不是一个页面打通首先理解后台管理系统在这个系统里的真实作业场景。前台接待的日常工作是在办入住、办退房、换房、续住、收押金财务要核对微信支付流水和做日结餐厅那边只需要看到当前房台的点餐内容老板要的是今天出租率、营收和客单价。如果后台所有菜单对所有账号开放操作效率低还是小事退款、改价这类敏感操作被人乱点才是大问题。所以成熟的酒店后台都会做角色-权限-操作三层角色role管理员、前台、财务、餐厅每类角色对应一组菜单和按钮权限permission后端按「权限码」控制接口前端按「按钮权限」控制菜单项操作日志log改价、退款、换房、取消订单这些高风险动作必须有记录可追溯以常见后端接口写法为例# perm.py —— 权限校验装饰器 def require_perm(perm_code): def decorator(func): wraps(func) def wrapper(*args, **kwargs): current_user get_current_user() if not current_user or not current_user.has_perm(perm_code): return jsonify({code: 403, msg: 无权限操作}) return func(*args, **kwargs) return wrapper return decorator # 退款接口只有拥有 order:refund 权限的角色能调用 app.route(/api/admin/order/refund, methods[POST]) require_perm(order:refund) def refund_order(): data request.get_json() order_no data.get(order_no) refund_reason data.get(reason) # 业务逻辑调用微信支付退款接口再改订单状态 ...这段代码把权限校验和业务本身分开装饰器负责「这个账号能不能做这件事」接口函数只关心「这件事怎么做」。后续要加新的高风险操作比如「批量改房价」只需要在角色管理里新增一个权限码再挂到对应角色上前端菜单用同一个权限码控制显示后端用同一个权限码拦截请求两端就不会对不上。权限码的命名有一个约定我用的是「模块:动作」格式比如 order:refund、room:assign、price:update。这样做的好处是一个权限码对应一个按钮级的操作尤其在前后端分离的项目里前端可以通过 permissionCodes 字段拿到自己拥有的按钮集合再用 v-if 控制按钮显隐。不要为了省事只做菜单权限菜单挡住了入口但别人抓包拿到接口地址照样能调用后端接口级的权限码才是真正兜底的那道闸。2.2 官网和微信小程序两个前端入口共用一个业务接口网站和小程序在业务上很容易被做成两套「看起来一样」的页面结果订单表里还要区分两个来源维护成本直接翻倍。常见做法是官网负责品牌展示、房型展示、在线预订小程序负责高频场景的快速预订、订单查询、远程入住和到店点餐。但两者的下单动作都打到同一个订单创建接口用 channel 字段区分来源这样前台在处理订单时只面对一套流程。# channel: 0官网 1小程序 2前台手工建档 app.route(/api/order/create, methods[POST]) def create_order(): data request.get_json() channel data.get(channel, 0) room_ids data.get(room_ids, []) check_in data.get(check_in_date) check_out data.get(check_out_date) # 锁房一次锁住所有目标房间避免部分成功 locked lock_rooms(room_ids, check_in, check_out) if not locked: return jsonify({code: 409, msg: 所选房间已被预订请重新选择}) order build_order(channel, check_in, check_out, locked) return jsonify({code: 0, msg: ok, data: {order_no: order[order_no]}})这里最核心的是 lock_rooms 这一步它不是去 select 一遍看看 status 是不是 0而是直接对房间状态做条件更新。后面第三章会专门讲这个并发处理的写法先记住一个原则查询到的空闲状态在提交瞬间可能已经失效只有更新时受影响行数为 1才算真正锁住了房间。页面框架方面官网如果只是为了预订落地渲染层直接用 Vue/Vite 或服务端模板都行小程序端如果这套源码自带原生工程就用微信开发者工具打开如果还想同时发到支付宝、抖音等平台则需要评估迁移到 uni-app。具体怎么选看下一节。2.3 技术栈选型Vue3 后台、uni-app 小程序还是原生工程后台管理系统这块Vue3 Element Plus 基本是近几年最稳的组合热门的“vue3 后台管理系统”模板已经把登录、动态路由、按钮权限、菜单管理都铺好了路把酒店的业务页面订单列表、房态图、点餐台填进去就行。注意看一下模板里的动态路由是不是按后端返回菜单渲染的这套源码如果带权限系统前端路由通常是静态路由 动态路由拼接而不是把所有页面写死在 router 表里。小程序端的选择要分两种情况。如果手头的工程本身就是用微信原生语法写的那就直接用微信开发者工具打开别强行迁移到跨端框架除非你有明确的多端发布需求。如果是从零开始或者打算同时上多个小程序平台我一般会用 uni-app 重写页面因为它的页面语法接近 Vue2/Vue3打包的时候通过“uniapp 微信小程序打包”流程可以把同一套代码编译成不同平台的小程序但代价是原生插件和三方 SDK 的兼容性要额外验证尤其是微信支付、定位这类依赖原生能力的功能。后端我只说最常见的两类Spring Boot 和 ThinkPHP。团队 Java 背景深、追求接口和事务的强一致性选 Spring Boot个人开发者或小团队、服务器资源紧张ThinkPHP 这类 PHP 工程布署简单指到 web 目录就能跑。数据库统一用 MySQL 8字符集用 utf8mb4因为小程序端的用户昵称、备注里有 emojiutf8 会存不进去。2.4 三端共用的接口约定统一返回格式和错误码三个前端后台、官网、小程序消费同一组接口最怕每个接口返回结构都不一样。比如后台接口返回 {success:true}小程序接口返回 {code:0}前端每个请求都要单独适配调试成本会直线上升。所以第一件事是统一成{ code: 0, msg: ok, data: {} }code 为 0 表示成功非 0 表示业务失败HTTP 状态码永远返回 200除非是网关或服务不可用。为什么故意不用 HTTP 404/500 表达业务失败因为小程序 wx.request 对非 2xx 状态会直接走进 fail 回调拿不到后端真正的错误信息统一 200 业务 code前端只需要在 success 回调里判断 code 即可。常见业务码可以约定1001 参数错误、1002 未登录、2001 房间被抢、2002 点餐台不存在、5001 支付未完成。登录态方面小程序端用的是 wx.login 换 openid再在服务端生成自己的 session token官网用的是普通账号密码后台管理系统一般用账号密码 验证码。三端各自登录后统一在请求头里带 Authorization: Bearer 后端过滤器统一解析不区分来源。这样订单接口、点餐接口、房态接口都不需要关心自己是被哪个端调用的。到这里架构基本定住一个后端工程、三套前端壳子、一套权限体系、一套接口协议。接下来进入最容易被忽略的数据库层。3. 把订单、房态、点餐、支付串成一条线核心表结构与业务流程3.1 房间表与房态状态机四个状态还是五个状态先看房间模型。几乎所有酒店系统的起点都是这张房间表我用的是五个状态而不是简单的「有房/没房」0 空闲、1 在住、2 清洁中、3 维修、4 预留。多出来的「清洁中」和「维修」是运营里绕不开的字段——客人退房后房间要先查房、打扫这时候房间还不能售卖如果直接把它标成「空闲」前台很容易把还没打扫的房间派给下一单客人客人推门看到床铺是乱的自然是要投诉的。CREATE TABLE room ( room_id int NOT NULL AUTO_INCREMENT, room_no varchar(10) NOT NULL, room_type varchar(20) NOT NULL COMMENT 大床房/双床房/套房, floor smallint DEFAULT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0空闲 1在住 2清洁中 3维修 4预留, default_price int DEFAULT NULL COMMENT 标准单价单位分, PRIMARY KEY (room_id), UNIQUE KEY uk_room_no (room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意这里有两个容易漏的细节一是房号的唯一索引同一酒店里房间编号必须唯一不然选房时会出现两个一样的房间二是金额列用 int 存分而不是 decimal 存元这样对接微信支付时接口参数 total 直接传字段值不用再做小数乘法浮点误差彻底不存在第四章会展开讲。需要展示成「¥399.00」时由前端在渲染层除以 100后端永远只认分。房间状态流转看起来简单但真正的魔鬼在并发。前台在电脑上看到房间空闲同时小程序里有个客人在手机上提交了同一间房两边都去 update rooms set status4 where room_id7 and status0数据库行锁会保证只有一个 update 成功、影响行数为 1另一个影响行数为 0业务上就把后者当作「房间已被抢走」处理并让他换房。这就是防超卖的最小实现比在应用层加分布式锁简单得多也足够应对一家中小酒店的量。3.2 订单主表和订单房间明细为什么必须拆开一个订单可能同时订两间房、住三个晚上如果每间房、每晚的价格都塞在订单表一行里后面要改其中一间房的价格、要单独取消其中一晚会非常痛苦。所以订单表拆成主表和明细表CREATE TABLE hotel_order ( order_id int NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 对外单号如 H202506180001, member_id int DEFAULT NULL, guest_name varchar(50) DEFAULT NULL, guest_phone varchar(20) DEFAULT NULL, channel tinyint NOT NULL DEFAULT 0 COMMENT 0官网 1小程序 2前台, check_in_date date NOT NULL, check_out_date date NOT NULL, night_count tinyint NOT NULL DEFAULT 1, total_amount int NOT NULL DEFAULT 0 COMMENT 应收总额单位分, paid_amount int NOT NULL DEFAULT 0 COMMENT 已收金额单位分, pay_status tinyint NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付 2部分支付 3已退款, order_status tinyint NOT NULL DEFAULT 0 COMMENT 0待确认 1已确认 2已入住 3已离店 4已取消, PRIMARY KEY (order_id), UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;明细表主要记录「每个房间、每晚的价格」这样同一个订单如果跨假期可以做到第一晚普通价、后两晚假期价前台改价也能精确定位到某一行CREATE TABLE order_room ( id int NOT NULL AUTO_INCREMENT, order_id int NOT NULL, room_id int NOT NULL, room_no varchar(10) DEFAULT NULL, night_date date NOT NULL COMMENT 具体入住晚, price int NOT NULL COMMENT 当晚房价单位分, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;一段常见的下单事务逻辑是先插入 hotel_order 主表拿到 order_id再把选中的每个房间 × 每个晚上逐行插入 order_room最后 update room 把房间状态改成 4 预留。三个动作必须放在同一个数据库事务里任何一个失败全部回滚。如果分三步写而不加事务就会出现订单主表生成了、明细插入失败房间却已经被占的情况排查起来非常头疼。这里额外提醒一个订单号生成的注意点不要用自增主键对外展示。给客人的回执单号、给微信支付的 out_trade_no都必须用独立的业务单号比如 H 日期 随机序列。原因是自增 id 一旦被猜出来竞争对手或者恶意用户可以通过遍历 id 爬取他人订单而且在支付回调里用自增 id 做索引也不安全。生成方式有雪花 ID、Redis 自增、日期 随机数任选一种只需要保证唯一即可。3.3 点餐模块独立支付还是挂房账点餐在酒店系统里比在纯餐饮系统里多一层选择这顿饭是客人直接付款还是先记到房费里、离店时统一结算。两种模式对应不同的表设计和支付流程。独立支付模式适合酒店的餐厅对外开放的场合客人点完餐直接用微信支付这时候点餐单就是一张独立的订单走和房费一模一样的支付通道简单直接。挂房账模式则适合客人报房号点餐、最后退房一起结账的场合这种模式下点餐记录里必须关联一个房费订单或房号并且离店结账时要把点餐金额和房费汇总到一张账单里。我一般推荐这套源码先用挂房账因为它的业务复杂度更高、也更符合「一体化」的定位CREATE TABLE dining_order ( dining_id int NOT NULL AUTO_INCREMENT, dining_no varchar(32) NOT NULL, order_id int DEFAULT NULL COMMENT 关联房费订单挂房账时不为空, room_no varchar(10) DEFAULT NULL, guest_name varchar(50) DEFAULT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待制作 1制作中 2已上菜 3已结账 4已取消, total_amount int NOT NULL DEFAULT 0 COMMENT 点餐金额单位分, PRIMARY KEY (dining_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;点餐行表记录具体菜品和数量每一个菜品在提交点餐、加菜、退菜时都要操作这张行表。点餐台和后厨可以共用同一个数据源前台在后台点一点「下单」后厨的大屏通过轮询或者 WebSocket 看到新单上菜后再点「上菜」状态回到餐桌或房号的账上。这一步不要做成两个独立系统否则后台改菜、后厨看不到又变成数据孤岛。如果点餐单独走微信支付支付回调更新的是 dining_order 这张表如果挂房账点餐只记账不支付等退房时由订单结账接口统一处理。后者更贴近酒店真实场景客人也不会因为在房间里点餐还要一单一单地扫码付款而感到别扭。3.4 支付回调和订单状态联动状态更新必须在一个事务里支付回调成功之后不能只把订单表 pay_status 改了就算完。想一想房态客人付了钱房间应该从「4 预留」变成「1 在住」或者至少变成已锁定不可取消的状态如果只更新订单忘记更新房间第二天前台会看到订单显示已支付但房态图里这间房还是预留导致第二晚被重复售卖。正确做法是在回调处理中把订单支付状态、房间占用状态、如果有押金还要加押金记录全部放到同一个数据库事务里def handle_pay_success(order_no, paid_fee): try: db.begin() order get_order_by_no(order_no) if order.pay_status 1: # 幂等判断微信会把回调投递多次不能重复入账 db.commit() return # 更新订单支付状态与已收金额 update_pay_status(order_no, paid_fee) # 把该订单锁定的房间从预留变为在住 lock_to_stay(order_no) db.commit() except: db.rollback() raise这里幂等判断尤其重要。微信支付的回调通知是「会重试投递」的服务端处理成功后必须在返回里给微信一个成功标志并且当同一笔通知再次到达时通过订单的支付状态直接跳过重复处理。如果不做幂等第一次回调网络抖动、第二次重试你的代码就会执行两遍入账、两遍把房间置为在住账实不符就是这么来的。到这里订单、房态、点餐、支付四个模块就通过订单号和数据表关联成了一个闭环。4. 微信支付对接从商户号到小程序拉起支付的最小可用配置4.1 对接前先确认四样东西AppID、商户号、APIv3 密钥和证书微信支付分为 v2 和 v3现在新接入的基本都在用 APIv3。这套源码只要写的是微信支付接口大概率走的就是 v3 的 JSAPI 支付。开始写代码之前先在微信支付商户平台确认这四个参数都存在并且权限已经开通AppID小程序后台「开发管理-开发设置」里能看到后续 JSAPI 下单要用商户号 mchid微信支付商户平台的商户编号和 AppID 需要做关联绑定APIv3 密钥商户平台上自己设置的 32 位字符串用于回调内容解密丢失后只能重置商户证书从商户平台下载的证书压缩包里面有 apiclient_key.pem、apiclient_cert.pem前者是私钥务必只存放在服务器不能打进前端代码或放在仓库另外要在商户平台配置「支付回调通知」。支付回调通知就是后端的 notify_url必须是可以被公网访问的 https 地址小程序支付主要做域名校验小程序后台的 request 合法域名必须包含你的接口域名。一个小坑私钥文件的权限和路径经常被忽略服务重启后找不到 pem 文件就直接报错。稳妥做法是把私钥放在 /etc/wxpay/ 目录下chmod 600在工程配置里引用绝对路径而不是把 pem 塞到 resources 目录一并进入发布包。4.2 小程序支付全流程登录换 openid、预下单、拉起支付、回调确认先看完整链路再做分段实现。小程序端用户点击「去支付」后事情按照这样的顺序发生小程序 wx.login 拿 code请求后端登录接口后端用 code 调微信的 code2Session 接口换取 openid小程序拿到支付参数后把订单号传给后端下单接口后端调用微信支付 v3 的 JSAPI 下单接口拿到 prepay_id后端用 prepay_id 和其他参数按官方规则签名把 timeStamp、nonceStr、package、signType、paySign 返回给小程序小程序调用 wx.requestPayment弹出微信支付收银台用户完成支付微信服务器向 notify_url 发送支付结果通知后端回调处理函数更新订单状态并返回成功标志这七步里任何一步断掉订单都会卡死在「未支付」或「已支付但后台不知道」。最稳妥的兜底方案是后端主动查单小程序端在 wx.requestPayment 的 success 回调之后除了收通知还可以隔几秒请求一次后端查单接口后端调用微信查单接口确认状态后再更新订单。这样即使回调因为网络问题丢了用户看到的订单状态仍然是正确的。// 小程序端登录并支付的核心动作 wx.login({ success(loginRes) { wx.request({ url: https://api.example.com/auth/login, data: { code: loginRes.code }, success(loginResp) { // data.token 为后端返回的会话标识同时后端已把 openid 与用户关联 wx.request({ url: https://api.example.com/pay/create, method: POST, header: { Authorization: Bearer token }, data: { orderNo: orderNo, payType: room }, success(createResp) { const pay createResp.data.data wx.requestPayment({ timeStamp: pay.timeStamp, nonceStr: pay.nonceStr, package: pay.package, signType: RSA, paySign: pay.paySign, success() { checkOrderStatus(orderNo) }, fail(err) { console.error(用户取消或失败, err) } }) } }) } }) } })注意 wx.requestPayment 的 package 字段长这样prepay_idwx1234567890signType 必须和签名算法对应v3 签名用的是 RSA也就是 SHA256 签名这一点写过 v2 的人最容易带错。4.3 后端预下单和签名金额、回调、签名的参数细节预下单接口地址是 https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi请求体里核心字段如下payload { appid: APP_ID, mchid: MCH_ID, description: 酒店订单- order_no, out_trade_no: order_no, notify_url: https://api.example.com/pay/notify, amount: {total: amount_fen, currency: CNY}, payer: {openid: openid} }三个参数最容易翻车。第一个是 amount.total单位必须为分一个订单总额 399.50 元这个字段就是 39950。第二个是 notify_url微信服务器会向这个地址 POST 一个加密的 JSON你的回调接口必须返回 HTTP 200 且响应体是 {code:SUCCESS,message:成功}如果返回其他状态码微信会按 30 分钟、2 小时、24 小时等间隔重试投递。第三个是 out_trade_no作为商户侧的唯一标识前面说过直接用业务单号不允许重复。对于下单请求本身的处理要点在于签名。微信支付 v3 接口要求对每个 HTTP 请求计算 Authorization 头需要用商户私钥对待签名串做 SHA256 签名格式为Authorization: WECHATPAY2-SHA256-RSA2048 mchid...,nonce_str...,signature...,timestamp...,serial_no...第一次联调时签名不过是最常见的故障点。排查顺序固定三步看私钥是否对应商户平台下载的证书看 serial_no 填的是证书序列号而不是商户号看签名串拼接顺序中换行符是不是 \n。这三步没问题签名基本不会再犯。def build_pay_params(prepay_id): params {} params[timeStamp] str(int(time.time())) params[nonceStr] random_hex(16) params[package] fprepay_id{prepay_id} params[signType] RSA message \n.join([ APP_ID, params[timeStamp], params[nonceStr], params[package] ]) # 用 apiclient_key.pem 做 SHA256withRSA 签名 params[paySign] sign_sha256_rsa(message) return params这里的时间戳是秒级字符串不是毫秒nonceStr 每次支付必须不同连续的两次支付如果随机串一样微信可能直接拒绝。4.4 回调解密与验签AES-256-GCM 的顺序不能错v3 支付结果通知使用 AES-256-GCM 算法加密 resource 里的数据所以回调处理分三步验签、解密、更新订单。验签的细节容易让人头大我一般建议封装一个 notify 处理函数把 body、header、证书序列号都传进去先验证 header 里的 Wechatpay-Signature再解密。回调解密的几个参数APIv3 密钥32 字节作为 AES 密钥、resource 里的 nonce 作为初始向量、resource 里 ciphertext 是密文认证标签包含在 ciphertext 末尾用 AESGCM 解密后得到一个 JSON里面有 out_trade_no、trade_state、amount.total 等字段。解密前确认 resource.algorithm 是 AEAD_AES_256_GCM而不是 v2 的 MD5 验签一半的坑都出现在把这套逻辑当 v2 处理上面。from cryptography.hazmat.primitives.ciphers.aead import AESGCM def decrypt_resource(ciphertext_b64, nonce, api_v3_key): key api_v3_key.encode(utf-8) # 32 字节 aesgcm AESGCM(key) ciphertext base64.b64decode(ciphertext_b64) # nonce 作为 IV密文尾部是认证标签 plaintext aesgcm.decrypt(nonce.encode(utf-8), ciphertext, None) return json.loads(plaintext)解密成功后再判断 trade_state 是否为 SUCCESS然后走上一章那个包含幂等判断的订单更新事务。注意这里的 APIv3 密钥是自己在商户平台设置的不要和服务端应用的数据库密码混在一起泄露任何一个都要去商户平台重置并重新部署。4.5 本地调试四板斧模拟支付、抓包、查单、日志真机预览不方便每次都真实支付微信开发者工具提供了「模拟支付」能力勾选后点击支付会直接走到支付成功回调适合验证整条链路是否通畅。但这只能模拟小程序端请求成功验证不了微信服务器真的回调了 notify_url所以线上联调时还需要第二板斧——查单接口微信支付 v3 有 /v3/pay/transactions/out-trade-no/{out_trade_no}服务端可以主动查这笔单的状态回调没到也能把订单状态拉回来。第三板斧抓包。小程序端的请求默认可以在开发者工具的 Network 面板里看但真机场景更常用的做法是 Charles 这类抓包工具配合 HTTPS 配置注意在微信开发者工具里设置好抓包端口并且把请求目标域名放到抓包白名单。抓包用于看三样东西请求有没有带 Authorization 头、后端返回的支付参数里 paySign 是不是完整、回调接口有没有 200 响应。凡是支付报「支付验证签名失败」十有八九是 paySign 拼接时把参数顺序弄错了。第四板斧是后端的支付日志。我会在预下单、回调收包、解密成功、更新订单这四个位置各打印一行日志记录关联单号线上出问题不要靠猜直接看日志就能定位是微信没回调还是回调解密失败或者是订单更新事务回滚了。5. 部署结构与避坑排查nginx 配置和五处生产翻车现场5.1 部署结构与 nginx 反向代理配置这套系统在本地跑通之后部署上线核心是一个 nginx 加三个应用进程官网前端、后台管理系统前端、后端 API 服务。小程序不像网页需要服务端渲染它只需要能够访问 API 服务。三个端通过不同路径与不同端口分流nginx 是这个架构里最薄的一层但也是最先暴露问题的地方。server { listen 80; server_name demo.example.com; client_max_body_size 20m; location / { proxy_pass http://127.0.0.1:8081; # 官网页面 proxy_set_header Host $host; } location /admin/ { proxy_pass http://127.0.0.1:8082/; # 管理后台 } location /api/ { proxy_pass http://127.0.0.1:8080; # 后端 API } }注意 location /admin/ 配了尾斜杠代理到后端时会把路径后缀也带上如果官网和后台构建后是纯静态文件用 root 指向 dist 目录即可API 服务单独代理如果前端还要做服务端渲染则全部走 proxy_pass。把复杂的地方尽量留给后端前端只做静态托管排错半径会小很多。5.2 翻车现场一微信支付回调不触发订单一直显示未支付现象用户在小程序端明显支付成功了微信支付商户平台也显示交易成功但酒店后台的订单状态始终是「未支付」。原因需要一个个排除。最常见的是 notify_url 配成了一个内网地址或 localhost微信服务器根本访问不到其次是回调接口代码写逻辑忘了返回 200导致微信一再重试直到超过有效期还有一类是回调接口做了登录鉴权微信服务器的请求带不上你的 token被挡在业务逻辑外面。解决分三处检查先在商户平台确认「支付回调通知」里的地址是 https 公网地址再确认这个接口在公网访问时能返回协议要求的 JSON最后看后端日志里有没有收到微信的 POST 请求。如果日志没有是网络没通如果日志有且处理报错是业务代码问题。一多半的「回调不触发」其实是回调已经收到了、代码抛了异常没返回 200微信显示投递失败而已。5.3 翻车现场二小程序请求接口报 -10002 或 request:fail现象在微信开发者工具里请求正常换到真机预览后所有请求全部失败控制台打印 -10002 或 request:fail。原因很直接小程序真机环境强制校验「request 合法域名」开发工具里勾选的「不校验合法域名、TLS 版本以及 HTTPS 证书」只在工具里生效真机上必须配置正确。还有一类场景是接口域名里的证书链不完整导致微信客户端 TLS 校验失败报错同样也是 request:fail。解决把 API 域名加到小程序管理后台的「开发设置-服务器域名-request 合法域名」里要求是 https 域名且证书链完整微信明确不允许用 IP 地址和端口号必须换成合法域名和默认 443 端口。另外后端 nginx 必须补全完整证书链最简单的方式是下载 nginx 版全链证书并确认 TLS 版本不低于 1.2。5.4 翻车现场三两单同时抢到同一间房前台只能手动改房现象客人 A 和客人 B 在几乎同一秒下单后台同时生成两笔已支付订单都对应了同一间房。这是并发下单导致的最典型事故。原因下单代码是先 select 房间状态确认是 0 空闲后再插入订单这中间存在一个时间窗口两个请求都读到空闲然后都走了下一步。解决把「查状态 订房」两步合成一条带条件的 update判断受影响行数# 锁房只有 status0 才能改写成预留 cursor.execute( UPDATE room SET status 4, lock_order_id %s WHERE room_id %s AND status 0 , (order_id, room_id)) affected cursor.rowcount if affected 1: # 抢房成功 else: # 换一间房或者提示客人说清楚这段代码为什么有效MySQL 里单条 UPDATE 语句执行时会对命中的行加锁两个并发请求同时执行时第二个会被阻塞直到第一个提交然后因为条件 status0 已经不成立影响行数是 0。这比在应用层加锁实现简单且可靠代价是只支持单机数据库如果后续做读写分离或分库就要换成 Redis 分布式锁。中小酒店的量级行锁方案足够。5.5 翻车现场四金额差 100 倍订单总额对不上账现象后台一笔订单总额显示 399微信支付商户单显示 39900对账永远对不平。原因后端从数据库取出金额后有的地方按「元」传给支付接口有的地方按「分」存库前端展示时又做了一次 /100小数点在三条数据链路上被挪了两次。解决全链路统一用「分」这个最小单位。数据库存 int 分、后端接口传 int 分、微信支付字段 amount.total 也是分、前端渲染时把分转成元。只有数据模型中的抽象层和前端展示层出现「元」任何接口传输和存储都不要再碰小数。顺手在接口测试里加一个断言任意订单的 paid_amount 对账时必须等于微信支付回调里的 amount.total不等就告警。5.6 翻车现场五跨天订单的日期错位现象客人预订今晚入住、明晚离店后台显示入住日期是昨天离店日期是今天夜次算错。原因前端把时间用 JavaScript 的 Date 对象转成 ISO 字符串又处理了时区导致 UTC 时间和本地时间交错生成了比实际早一天或晚一天的日期。解决日期字段统一用 string 的「2025-06-18」格式传输在小程序端通过日期选择器直接取 yyyy-MM-dd不在端上做 Date 转换后端用 date 类型解析不落时间戳。计算夜次时用 check_out_date - check_in_date 的天数差而不是 endTime-startTime 除以 86400000 再四舍五入后者在跨时区时最容易偏差。到这里五类在生产环境反复出现的翻车问题都有了对应解法。剩下最后一步把系统从「能跑」变成「好用」。6. 上线后第一周全链路验证、对账与小程序的合规收尾6.1 用一个真实订单把整条链路走一遍系统上线后第一周不要急着上活动先用测试订单把链路走一遍。我的固定顺序小程序登录 → 提交订单 → 微信支付 → 后台出现已支付订单且金额一致 → 房间从预留变为在住 → 小程序点餐挂房账 → 前台离店合并结账 → 日结报表与微信对账单核对。这圈覆盖订单、房态、点餐、支付四个模块的全部状态迁移。只测成功路径不够还要额外测两个异常用户支付后立刻取消以及支付回调丢失后由查单接口兜底恢复这两个场景才是上线后最容易先出事的地方。6.2 日结对账微信支付对账单与订单表的逐笔核对对账是财务每日动作也是系统是否可信的最终裁判。微信支付商户平台每天能下载前一天交易对账单里面每行包含商户订单号、交易金额、交易状态。我用脚本把对账单转成字典再和本地订单表昨日支付成功的订单号做差集对比任何一边多出来的记录都是账实不符的线索。对账频率的底线是每天一次隔周再对回调漏更新问题会被时间掩盖处理成本高出好几倍。6.3 小程序发布前的合规和体验收尾小程序发布最容易卡在三个细节。第一微信支付商户号主体要求是企业或个体工商户个人主体无法开通微信支付且商户号主体必须和小程序主体一致否则提审失败。第二用到获取手机号能力时必须先在小程序后台配置隐私保护指引现在拿手机号已经不能直接 getUserInfo要用官方手机号快速验证组件后端拿到加密数据后解密没配隐私协议时真机调用会被直接拦截表现成查不出原因的黑匣子。第三开发版、体验版和正式版要隔离商户参数我给订单号加 dev/prod 前缀从单号就能判断数据来自哪个环境。6.4 持续观察的最后一个习惯这套源码落地到这里已经具备订单、房态、点餐、支付和对账闭环。我自己的教训是支付回调这类环节第一次实现能用不代表真实流量下正确。上线后我固定每周一跑一次对账脚本最多抓出过 7 笔回调延迟导致的未入账订单全靠查单接口兜了回来。像并发锁房、支付回调这种平时很少暴露问题的地方持续观察、主动对账才是让系统沉淀稳定的可靠路径。希望帮到你。本文还有配套的精品资源点击获取
返回列表