先说结论:拿 Python 写酒店餐饮的点餐小程序,技术上完全走得通,但真正能落地的关键不在代码,而在你对业务场景的理解。酒店餐饮和街边小馆子的点餐逻辑不是一回事,住店客人、宴会散客、西餐厅住客这几种身份的用餐路径完全不同,如果一上来就照着普通扫码点餐的模板套,后面必然反复改需求。
我去年帮某家综合型酒店做过一套类似的系统,用的是 Python + Flask 做后端,微信小程序做前端,数据库选的是 MySQL 加 Redis 做缓存,整套系统从点菜、下单、支付到后厨打印、房客送餐,全部跑通,前后用了大概两个月业余时间。这篇文章把我踩过的坑、总结的方案、核心代码和设计思路完整分享出来,给正在做或准备做类似项目的朋友一个可以直接参考的范本。
1. 业务场景拆解:酒店点餐比你想的复杂得多
1.1 先搞清楚三种完全不同的用餐路径
酒店餐饮的客群大致能分成三类,每一类的需求细节都不一样,这直接决定了系统功能的设计边界。
第一类是住店客人,他们可能在客房里面通过电视或手机扫码点餐,要求送餐到房间。这类客人最看重的不是菜品多丰富,而是送餐时间承诺和客房号绑定。我做完复盘才发现,客房点餐如果不做房号核验,很容易出现客人退房后餐还没送到、挂账变成坏账的情况。
第二类是宴会和会议用餐,这类场景的菜品通常是固定套餐,讲究的是批量上菜和分桌管理。一个宴会厅可能同时坐着几十桌客人,每桌的菜单可能是提前配好的,服务员只需要确认桌号和菜单即可,不需要客人扫码操作。
第三类是常规的零点散客,和街边餐厅逻辑类似,扫码看菜单、加购物车、下单、支付或呼叫服务员结账。但酒店里的零点餐厅往往同时包含中餐、西餐、日料等不同档口,每个档口的出菜口和后厨打印逻辑不一样,系统在设计时就必须支持按档口分单打印。
如果你把三类场景混在一起,用一套通用点餐流程去套,后面一定会被酒店运营人员拉着改需求改到怀疑人生。我当时的做法是:在一个项目里拆分出三种消费入口,但共用同一套菜单和库存中心,这样菜品数据只维护一份,但订单流向完全独立。
1.2 酒店点餐的技术需求清单
结合上面的业务场景,我把需求量化成了一份可以直接用于开发的清单:
- 房客点餐需要校验房号和入住状态,支持挂房账(退房时一并结算)
- 宴会场景需要桌台管理和套餐模板,支持一键整单下单
- 零点餐厅支持扫码落座,桌号二维码绑定餐桌
- 后厨需要按档口分单,并自动打印到对应出菜口的打印机
- 菜品需要支持每日估清(售罄自动下架),晚到时段自动切换菜单(早餐、午餐、晚餐、夜宵)
- 需要配套营销基础能力:优惠券、满减、会员价
- 所有订单数据要能汇总到酒店已有的 PMS(Property Management System)或财务系统
提示:如果你想用这套系统卖给多家酒店复用,不要一开始就深度对接各家已有的 PMS。每家酒店的系统字段格式差别很大,后来我才明白,对接的工作量远超点餐本身,最好的方案是预留一个标准 JSON 接口,让酒店方的技术人员自己做中间适配层。
1.3 技术选型的心路历程
我从一开始就确定用 Python,原因很简单:项目是我一个人搞定,Python 写接口效率最高,Flask 轻量、文档多、社区问题库丰富。虽然 Django 自带 Admin 后台很香,但 Flask 的灵活度在对接酒店内部系统时反而更友好。后来事实也证明了这一点——某个对接需求的字段顺序特殊,我直接用 Flask 的 before_request 钩子改了请求解析逻辑,五分钟搞定。
小程序端用的原生语言,不引入第三方 UI 框架。原生小程序虽然写起来啰嗦,但对这种页面结构不复杂的点餐项目来说,稳定性就是最大的优势,而且微信官方提供的基础库升级不会出现兼容性断档。npm 上的第三方组件库虽然好看,但有时候小程序基础库一更新,组件库还没来得及适配,页面就白屏了。这种教训在线上出过不止一次。
数据库选了 MySQL 8.0,缓存用 Redis,部署直接用 Docker Compose 管理 MySQL、Redis、后端服务、Nginx 四个容器,服务器是 2 核 4G 的云主机,实测高峰期完全够用。订单类数据不存在海量写入问题,MySQL 完全能扛住,没必要上复杂的读写分离架构。
2. Python 后端设计与数据模型规划
2.1 项目目录结构与核心模块划分
后端我按模块职责拆分了几个 Blueprint,目录结构如下:
hotel_order_backend/ ├── app.py # 应用入口 ├── config.py # 配置文件 ├── models/ # ORM 模型 │ ├── user.py │ ├── menu.py │ ├── order.py │ ├── table.py │ └── room.py ├── api/ # 接口蓝图 │ ├── auth.py # 登录鉴权 │ ├── menu_api.py # 菜单查询 │ ├── order_api.py # 下单、订单管理 │ ├── payment_api.py # 支付回调 │ └── print_api.py # 后厨打印 ├── services/ │ ├── order_service.py # 订单核心逻辑 │ ├── payment_service.py # 支付处理 │ └── inventory_service.py# 库存与估清 ├── utils/ │ ├── jwt_utils.py # 鉴权工具 │ └── response.py # 统一返回封装 └── requirements.txt模块划分是最容易被新手忽略的事情。我接过不少外包项目,看过很多人的代码是几百行堆在一个文件里,刚开始写起来很爽,后面每次改需求都要小心翼翼,生怕动一行代码把别的功能弄坏。如果你打算长期维护这个项目,模块化设计这笔账早晚要还。
2.2 数据库设计:最关键的几张表
我把核心表结构拿出来说一下,你可以直接拿去做建表参考。
菜品表(dish)
CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, member_price DECIMAL(10,2), image_url VARCHAR(255), description VARCHAR(500), status TINYINT DEFAULT 1, -- 1上架 0下架 stock INT DEFAULT 999, -- 库存,-1表示不限量 kitchen_code VARCHAR(20), -- 所属档口编码 sort_order INT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );kitchen_code字段是我后期加的,为了做档口分单打印。一开始没想清楚,结果上线后那家酒店的西冷牛排和港式烧鹅是在两个完全不同的厨房做的,如果一单里同时有这两道菜,整单打印到同一个出菜口,厨师根本不知道去哪拿食材,部门之间还会互相推诿。后来加了厨房编码,依赖 RabbitMQ 做消息分发才彻底解决。
订单主表(orders)
CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, channel_type TINYINT NOT NULL COMMENT '1房客点餐 2宴会 3扫码点餐', room_id INT, room_account TINYINT DEFAULT 0 COMMENT '是否挂房账', table_id INT, guest_count INT DEFAULT 1, total_amount DECIMAL(10,2) NOT NULL, pay_status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已挂账', order_status TINYINT DEFAULT 0 COMMENT '0已下单 1制作中 2已完成 3已取消', remark VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );order_no的生成规则我后来踩了一个大坑,一开始用的是时间戳加随机数,结果并发高的时候偶尔重号,导致订单数据被覆盖。后来改成了日期 + 酒店编号 + 用户ID后四位 + 随机数再拼接,测试了下,无论如何并发都不会重了。
订单明细表(order_items)
CREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待制作 1制作中 2已完成', kitchen_code VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP );明细表里的dish_name和price是冗余字段。为什么要存冗余?就是因为菜品价格和名称经常调整,如果直接关联菜品表,历史订单的价格信息就会被改掉。最典型的场景:昨天客人点了这道菜 68 元,今天调价成 88 元,财务查昨天的订单报表时发现金额对不上账,那就真要炸锅了。
2.3 Flask 应用工厂与核心配置
我用应用工厂模式初始化应用,方便后续测试和扩展:
def create_app(): app = Flask(__name__) app.config.from_object(Config) db.init_app(app) redis_client.init_app(app) jwt = JWTManager(app) # 注册蓝图 app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(menu_bp, url_prefix='/api/menu') app.register_blueprint(order_bp, url_prefix='/api/order') app.register_blueprint(payment_bp, url_prefix='/api/pay') # 统一异常处理 @app.errorhandler(400) def handle_bad_request(e): return response.error(msg='参数错误', code=400) return app这个应用工厂的好处是一方面能通过传入不同配置来跑测试环境和线上环境,另一方面注册蓝图做模块解耦,任何人接手代码都能一眼看出接口分布在哪里。这些小习惯短期内看不出差别,但项目运行三个月后就很明显了。
3. 微信小程序前端设计与核心流程实现
3.1 小程序页面结构划分
小程序端的页面我做了下面几个划分:
pages/ ├── index/ # 入口页,根据角色自动跳转 ├── menu/ # 菜单展示页 ├── cart/ # 购物车页 ├── confirm/ # 确认订单页 ├── order-list/ # 订单列表页 ├── order-detail/ # 订单详情页 ├── payment/ # 收银台页 ├── room-order/ # 房客点餐页 ├── table-qrcode/ # 桌台码绑定页 └── user/ # 个人中心页index页面会读取 URL 参数或用扫码结果判断入口类型。这个逻辑放在前端的好处是用户无感知,扫码进入后直接看到对应的场景页面,不用手动切换。
3.2 微信登录与用户体系
微信点餐场景下用户体系不能沿用传统的用户名密码注册,核心是走微信小程序授权机制。小程序端调用wx.login获取临时凭证code,后端拿着这个 code 去向微信服务端换取 openid,再把这个 openid 作为用户唯一标识存下来。
这里有个问题:新版微信基础库之后,直接通过wx.getUserInfo拿用户手机号和昵称的接口已经大幅收紧,不能强制要求用户授权,否则用户直接拒绝使用。我当时的方案是:首屏只拿 openid 建立匿名身份,允许游客完成点餐,结账时如果需要开票或绑定会员再引导完善资料。实测下来,这个流程的转化率最高,因为点餐的核心动线没有被任何弹窗打断。
JWT 签发和鉴权部分的核心代码如下:
@auth_bp.route('/login', methods=['POST']) def login(): code = request.json.get('code') # 调用微信接口换取 openid openid = wx_code_to_openid(code) user = User.query.filter_by(openid=openid).first() if not user: user = User(openid=openid, nickname='微信用户') db.session.add(user) db.session.commit() token = create_access_token(identity=str(user.id)) return response.success({'token': token, 'user_id': user.id})3.3 点菜下单的完整交互链路
点菜下单主链路完整流程如下,我用文字而不是图表描述,更贴合开发时的顺序思考:
用户进入小程序保存userId和sessionToken到本地缓存,登录后请求菜单接口,菜单接口返回分类列表和分类下的菜品数组,前端一次性渲染,前后端接口直接返回 JSON,由前端循环拼装。这一步需要注意控制单次返回的菜品数量,如果超过 100 道菜,建议分页加载或按分类懒加载,否则首屏在弱网环境下会白屏很久。
用户加菜后购物车数据保存在前端本地存储里,而不是每次加减都请求后端接口。这样的好处是响应速度极快,用户不停点加菜按钮没有网络等待感。结算时才把完整购物车提交到后端,如果菜品中有某道菜刚好在这段时间售罄,后端下单接口会返回明确的错误代码和菜品名称,前端弹窗提示用户哪些菜需要替换。
下单接口收到订单数据后开启一个 MySQL 事务,先锁定菜品库存行,再扣减库存,然后创建订单主表和明细表,最后调用支付接口。这整个过程必须保证原子性,否则会出现库存扣了订单没生成,或者订单生成了支付不了钱的问题。
@order_bp.route('/create', methods=['POST']) @jwt_required() def create_order(): user_id = get_jwt_identity() items = request.json.get('items') # [{dish_id: 1, quantity: 2}, ...] channel = request.json.get('channel_type', 3) # 事务开始 with db.session.begin(): total = 0 order_items = [] for item in items: dish = Dish.query.filter_by(id=item['dish_id'], status=1).with_for_update().first() if not dish: raise APIException('菜品不存在或已下架') if dish.stock != -1 and dish.stock < item['quantity']: raise APIException(f'{dish.name} 库存不足') # 扣库存 if dish.stock != -1: dish.stock -= item['quantity'] total += dish.price * item['quantity'] order_items.append(OrderItem( dish_id=dish.id, dish_name=dish.name, price=dish.price, quantity=item['quantity'], total_price=dish.price * item['quantity'], kitchen_code=dish.kitchen_code )) order = Orders( order_no=generate_order_no(), channel_type=channel, total_amount=total, user_id=user_id ) db.session.add(order) db.session.flush() # 获取order.id for oi in order_items: oi.order_id = order.id db.session.add_all(order_items) return response.success({'order_id': order.id, 'amount': total})这段代码里最关键的是with_for_update()这一行,它把菜品记录行锁定,后续同时并发下单的请求会排队等待,有效避免超卖。曾经测试时我用 Jmeter 压了 200 个并发下单同一道库存只剩 10 份的菜品,加了行锁之后,数据库慢慢处理但一笔超卖都没有,这个防线必须有。
3.4 微信支付的接入与对账
微信支付在点餐小程序里通常走 JSAPI 支付。流程是前端把订单 ID 传给后端,后端生成预支付订单返回paySign等参数,前端调用wx.requestPayment拉起收银台,用户在弹出界面完成支付。支付结果分为两步:前端会收到支付成功的回调,后端还需要等微信服务器发来的异步通知,两者必须都处理,因为任何时候网络都可能有延迟,支付宝微信这些支付通道都遵循异步回调这个终极原则。
异步通知的处理有几个容易踩的坑:
- 回调接口必须是公网可访问,如果是纯内网环境做开发测试,回调不进来,支付状态就会卡住
- 回调接口要做幂等处理,微信可能因为网络重试发多次通知,收到第一次通知更新订单状态,后续重复通知要直接返回成功
- 回调验签失败必须记录日志,排查问题的时候,这个日志就是救命稻草
关于订单状态和生产者的关系,我推荐直接在支付成功回调里修改库存和订单状态,而不是用消息队列异步处理。理由是餐饮场景的单量规模适中,没必要引入更高的复杂度。我这边前后跑了一年多,从没遇到回调堆积的情况。
4. 后厨档口分发、打印与估清联动
4.1 一个暧昧需求带来的架构调整
前面提过,酒店餐饮的关键痛点之一是档口分离。一桌客人可能点了蒜蓉粉丝虾(中厨热菜)、炭烧牛扒(西厨)、三文鱼刺身(日料档),三个档口出的菜完全不同。如果整单只打印一张单子,传到某个档口,另外两个档口就漏菜了。
我的方案是:下单成功后,把 order_items 按 kitchen_code 分组,每个档口生成一张独立的制作单,推送到对应档口的打印机。打印机用的是 58mm 热敏小票打印机,走 ESC/POS 指令。Python 后端通过 HTTP 调用打印服务,打印服务把格式化的文本转成 ESC/POS 字节流发给打印机。
这里额外做了个细节:同一档口的菜品中,如果有需要同时出的关联菜(比如一桌点了两个凉菜,通常凉菜间一起出菜),相邻的单子会合并到一个批次打印,减少厨师撕单的次数。这个小优化是酒店后厨负责人当面跟我提的需求,说单子扯来扯去太乱,做了之后对方特别满意。
4.2 估清与前端动态联动
估清本质上就是餐厅里的菜卖完了。系统必须做到:
前端菜品列表实时标记“沽清”,不能再点击加购,这一层通过轮询接口实现。轮询频率我设置了 30 秒一次,太频繁服务器压力大,太慢用户会点到已经卖完的菜,体验不好。这道菜一旦沽清,Redis 里设置一个短时间缓存标记,后续请求直接命中缓存返回,避免每次轮询都打到 MySQL 查库存。
估清信息单独封装了一个/api/menu/status接口,返回结构如下:
{ "code": 0, "data": { "sold_out_ids": [101, 102, 103], "updated_at": "2024-01-15 18:30:00" } }前端拿到 id 列表,把对应菜品渲染成置灰状态。新增的存量检查在下单接口再做一次终检。这个双重校验很重要,因为前端轮询 30 秒内可能有很多人同时下单,最后的一道防线必须在后端。
夜间菜单切换也是同样机制。我加了menu_template表,支持按时段切换不同菜单模板,比如早餐只展示豆浆油条这类产品,午餐加推商务套餐,晚上切换为宴会套餐选项。切换逻辑我放在后端,因为前端时间不可信(用户可能改了手机时间),后端根据服务器时间自动返回不同菜单。滑稽的是,为了解决这个需求我写了几十行代码,但效果立竿见影,早餐时段客户不再反复投诉能看到正餐菜品却不能下单了。
4.3 房客点餐与挂房账
房客点餐的独特之处在于可以挂房账。酒店运营的逻辑是,住店客人不用即时支付,退房结算时统一在前台结清。技术上做的就是校验房号、校验入住状态、标记挂账标识。
具体流程是这样的:客人在小程序里输入房号,前端把房号传给后端,后端去酒店的 PMS 系统查询该房号是否存在且处于入住状态(调用 PMS 接口或同步入住数据到本地表)。验证通过后生成订单时把订单标记为挂账,支付状态为待结算,订单照常推送到后厨制作。退房时前台根据房号汇总该房号所有挂账订单收款结账。
这一步如果酒店 PMS 是半自动半人工的,就需要做一个数据同步定时任务,每天凌晨把入住登记信息从 PMS 同步到本地 Redis 缓存。这个同步动作要记得在夜间非用餐高峰运行,否则同步过程中大量写库会影响点餐接口响应。
5. 上线后的真实问题与排查笔记
5.1 问题一:点餐高峰期接口响应从 100ms 飙升到 3s
上线第二周,某天中午十二点半高峰期,用户反馈页面加载非常慢。查看日志发现 MySQL 的 CPU 飙高到 90%,慢查询日志里出现大量针对 dish 表的全表扫描。
定位原因:估清轮询接口虽然加了 Redis 缓存,但如果 Redis 里没有命中,就回源到 MySQL 查询所有菜品状态,而菜品数量较大的情况下这会产生很大的查询开销。加上这个接口 30 秒会被每个在线用户各请求一次,直接把数据库打挂了。
解决思路:
一是把菜品状态缓存时间从 30 秒的弱缓存改成 60 秒强缓存,命中率提升 50% 以上。
二是给 dish 表加组合索引,(status, kitcken_code)联合索引覆盖了查询条件。
三是把轮询接口从定时轮询升级成 WebSocket 主动推送,前端页面作为 WebSocket 客户端连接后端,后端不主动推送变更时前端不会反复请求。测试下来 CPU 负载下降了 80%,体验也稳定多了。这部分改动量不大,收益却特别明显。
5.2 问题二:后厨打印机突然不出单
某天下午,后厨打印机集体罢工,排查后竟然是打印服务的连接对象是旧 IP。酒店内网的 DHCP 分配策略在下午重启了一次交换机,打印机的 IP 变了,但打印服务还连接旧地址。
遇到这种内网环境问题,比较成熟的方案是打印服务启动时做设备发现,播报 UDP 包让各个打印机响应,动态更新 IP 列表。如果对稳定性要求更高,就直接给打印机配置固定 IP,并把打印服务做成重连机制。这个坑在企业内网环境特别典型,公共区域网络一重启,所有设备都可能掉线重连。
5.3 问题三:退款场景下的重复退款风险
有一次客人在小程序上退了一道已制作的菜,客服操作退款后又被线下系统退了一次,同一个订单退款金额翻倍。定位后发现退款接口没有做幂等性控制,前端和财务系统同时发起退款就产生了问题。
后来在退款接口里加了 refund 记录表,每次退款前先检查该订单已退金额,超过应付金额直接拒绝。这个消费场景虽然发生概率低,但一旦发生就是不可逆的账务问题。
5.4 高频问题排查速查表
| 现象 | 可能原因 | 排查路径 |
|---|---|---|
| 下单接口报菜品不存在 | 菜品估清或已下架仍显示在前端 | 检查 Redis 缓存是否过期,确认前端轮询是否更新了状态 |
| 订单显示待支付,用户却已扣款 | 支付异步通知未到达 | 检查回调日志,验证签名,手动幂等方式补单 |
| 后厨没出单,前端显示已下单 | 打印服务连接断开 | 检查打印机 IP 是否变化,打印队列是否阻塞 |
| 房客无法挂房账 | PMS 同步失败或入住状态过期 | 查看同步定时任务执行日志,检查 Redis 房间状态缓存 |
| 菜品图片加载慢 | 图片未走 CDN | 改用对象存储配合 CDN 加速,别放在业务服务器本地 |
6. 部署方案与后续扩展构想
6.1 Docker Compose 一键部署
整个系统分四个容器跑,我用 docker-compose 管理,配置文件核心段落如下:
version: '3.8' services: mysql: image: mysql:8.0 restart: always environment: MYSQL_ROOT_PASSWORD: 你的密码 MYSQL_DATABASE: hotel_order volumes: - ./mysql_data:/var/lib/mysql ports: - "3306:3306" redis: image: redis:6.2 restart: always ports: - "6379:6379" backend: build: ./backend restart: always depends_on: - mysql - redis environment: DATABASE_URL: mysql+pymysql://root:密码@mysql:3306/hotel_order REDIS_URL: redis://redis:6379/1 ports: - "8000:8000" nginx: image: nginx:1.24 restart: always ports: - "80:80" - "443:443" volumes: - ./nginx_conf:/etc/nginx/conf.d - ./ssl_cert:/etc/nginx/ssl depends_on: - backend容器编排的好处是一键拉起来全套环境,换服务器不用重新配环境。但有一个细节必须说:MySQL 数据目录要挂载到宿主机卷,否则容器删掉数据全没了,某天手滑执行了 docker-compose down -v,连裤衩都不剩。
小程序端不像后端有容器方案,本质上是通过微信开发者工具上传代码,然后在微信公众平台配置合法域名。这一步很容易漏,我之前就是把后端接口地址填成 HTTP 了,没添加 HTTPS 合法域名,结果真机调试所有请求直接失败,报“不在以下 request 合法域名列表中”,排查了半天才想起来。微信小程序强制要求 HTTPS 和域名备案,域名没有备案之前不要抱任何侥幸心理。
6.2 营销中心:优惠券、会员卡与储值
点餐系统上线稳定后,运营一定会上营销需求。我在基础版本里预留好了扩展空间:
- 优惠券表支持新人券、满减券、折扣券三种类型,发放逻辑和核销逻辑分开
- 会员分为普通会员、银卡会员、金卡会员,折扣率权重不同
- 储值账户和支付流程打通,消费时优先走储值余额
营销这部分每个酒店玩法的细节差异极大,代码上要做的就是把规则配置化,别写死。把优惠规则写死在代码里的结果就是每次改营销方案都要重新发版,不值得。我用了一个策略模式,把券类型和优惠计算函数绑定,新增一种券类型只需要新增一个策略类就行。
6.3 多餐厅家具与多酒店部署
最开始的系统是给单体酒店用的,但是到项目后期,一个集团性酒店往往带多家餐厅,还可能有分店。如果代码里到处写死了单餐厅 ID,后面要支持多餐厅就会痛苦到怀疑人生。
在设计数据库时,从第一张表开始就加上restaurant_id字段,默认值为 1。后续要扩展时只需要在 Redis 缓存 key 上拼上餐厅 ID 作为命名空间,例如menu:1:category、menu:2:category,每个餐厅独立的菜单模板。这个习惯当时纯属顺手,后来遇到扩展需求真的省了非常多重构时间。
7. 做这个项目,我最后想分享的几个体会
做一个餐饮点餐小程序,技术上占三成难度,剩下七成是业务理解、流程梳理和现场沟通。如果只埋头写代码,你可能写出了一个很好用的工具,但后厨不用、服务员不用、老板看不到数据报表,项目基本就是自嗨。
有几个做得对的地方,现在回想仍觉得必要:
用了自定义代称的模拟数据反复测试并发场景。尤其是下单扣库存下的并发测试,我用 Jmeter 做了多种场景的压测,真实模拟酒店中午高峰期同一时间几十桌同时扫同一个菜单下单,这个测试帮我发现了很多隐藏的上限问题。
给运维留了足够的日志观测能力。所有接口关键步骤都埋了带业务含义的日志,比如订单创建时带order_no、user_id、source字段,出问题时通过日志能直接锁定是哪一步出了问题,不用全流程翻代码。
没有一开始就做复杂的对接。第一版全部用独立数据表和假接口运行,等酒店方确认了完整流程和字段格式,再开始对接 PMS 和财务系统。这样避免了一开始陷入对接泥潭导致主流程迟迟无法交付。
最后再分享一个小技巧:酒店点餐小程序千万不要把菜品图片全部塞进小程序包内,一定要走网络 URL 加载。小程序包体积限制目前是 2MB 主包,超过限制直接上传失败,而且就算侥幸传上去,用户打开时加载体验也极差。图片交给对象存储加 CDN,数据库只存 URL,这样无论后续菜品加到几千道,前端性能都不会有明显下降。
这个项目的完整代码结构和方案设计我整理成了可复用的脚手架模式,后续如果有朋友在做相同的酒店餐饮项目,可以顺着这篇文章的思路直接搭数据模型和后端骨架,省去前期探索的大量时间。其中踩过的坑,希望你能绕开,顺利完成自己的项目。