
简介这是一份面向Android初学者与餐饮类应用开发练习者的移动点餐项目源码包含ph2移动客户端与ph2web服务端两部分可用于理解从界面交互到后台数据交换的完整链路。客户端侧涉及菜单展示、购物车与订单提交、网络通信及本地数据保存等典型环节服务端侧则覆盖请求处理、数据库管理与接口设计等思路适合作为课程实验或自学练手素材。资源包共149个文件以class编译文件、png图片资源、java源码与xml布局为主另含jar依赖、apk安装包及少量配置与缓存文件整体约2.68MB结构紧凑便于快速导入查看。目前已有164人学习下载可帮助读者对照源码梳理Android应用的基本开发流程理解模块划分、界面与逻辑分离以及客户端与服务端协作方式为后续独立完成类似点餐或信息管理类项目提供可参考的实现框架。1. 移动点餐源代码从扫码到出单一套能跑通的完整链路长什么样很多人第一次接触移动点餐源代码脑子里浮现的是“一个点菜的 App 界面”。真上手才发现它其实是一条从顾客扫码、选菜、下单到后厨出单、前台结账、后台改价的完整数据链路界面只是最外面那层皮。我见过不少团队花两周把菜单页做得挺漂亮结果卡在“同一桌两个人同时加菜订单金额对不上”这种问题上返工比开发还久。这套源码真正要解决的是让顾客自助下单、让订单准确落到后厨、让老板能随时改菜价和库存同时保证多端数据一致。它适合想自建点餐系统的中小餐饮团队、做私域外卖的开发者以及需要把点餐能力嵌进现有小程序的工程师。下面按“先立住结构、再动手复现、最后讲坑”的顺序拆开讲。2. 移动点餐源代码的骨架四张核心表撑起整条链路一套能用的移动点餐系统剥掉前端花哨的交互后端本质就是围绕四类数据在转菜品、桌台、订单、订单明细。把这四张表的关系理清楚后面写接口、做并发控制、算金额才有依据。很多人一上来就写页面写到“加菜”功能时发现订单结构没设计好只能推倒重来这就是没先立骨架的代价。2.1 菜品、桌台、订单、明细四张表怎么设计菜品表存的是“卖什么”桌台表存的是“在哪吃”订单表存的是“这一单的整体状态”明细表存的是“这一单里每道菜的数量和当时的价格”。关键点在于明细表必须冗余存下单时的单价不能只存菜品 ID 去关联查价。原因很简单老板今天把宫保鸡丁从 28 改成 32昨天的订单不能跟着变。这是血泪经验早期图省事只存 ID月底对账时金额全乱。下面是一份可直接建表的最小 SQL字段做了精简但保留了关键约束-- 菜品表菜单的基础数据 CREATE TABLE dish ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, -- 菜名 price DECIMAL(10,2) NOT NULL, -- 当前售价单位元 stock INT NOT NULL DEFAULT 0, -- 剩余库存-1 表示不限量 status TINYINT NOT NULL DEFAULT 1, -- 1 上架 0 下架 category_id BIGINT NOT NULL, -- 分类用于前端分组 INDEX idx_category (category_id) ); -- 桌台表每张桌子一个二维码扫码后带出 table_id CREATE TABLE dining_table ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL, -- 如 A01、大厅3号 qr_token VARCHAR(64) NOT NULL, -- 二维码里的随机串防止猜 ID status TINYINT NOT NULL DEFAULT 0, -- 0 空闲 1 就餐中 UNIQUE KEY uk_token (qr_token) ); -- 订单表一桌一次就餐对应一条主订单 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, table_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0, -- 0 待支付 1 已支付 2 已出单 3 已完成 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_table_status (table_id, status) ); -- 订单明细每道菜一行冗余存下单时单价 CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, dish_id BIGINT NOT NULL, dish_name VARCHAR(64) NOT NULL, -- 冗余菜名防止菜品被删后查不到 unit_price DECIMAL(10,2) NOT NULL, -- 下单那一刻的价格 quantity INT NOT NULL, INDEX idx_order (order_id) );逻辑说明dish和dining_table是基础数据orders和order_item是交易数据。order_item里冗余dish_name和unit_price是为了让历史订单永远可读、可对账哪怕菜品后来改名或下架。参数上price用DECIMAL(10,2)而不是FLOAT因为浮点数算钱会出现0.10.20.30000000000000004这种玄学问题对账时能让人崩溃。qr_token用随机串而不是直接暴露桌台 ID是防止有人手动改 URL 去操作别人的桌台。2.2 扫码到下单的接口调用顺序前端扫码后拿到的不是桌台 ID而是qr_token。整个下单流程分四步解析 token 换桌台信息、拉取菜单、提交订单、返回订单号。这四步的顺序不能乱尤其是“提交订单”必须在服务端重新算一遍金额不能信前端传来的总价。// 前端扫码后解析出 token调用后端换取桌台 async function scanAndLoadMenu(qrToken) { // 第一步用 token 换桌台信息同时校验桌台是否可用 const tableRes await fetch(/api/table/resolve?token${qrToken}); const table await tableRes.json(); if (table.status ! 0) { throw new Error(该桌台正在就餐中请联系服务员); } // 第二步拉取上架菜品按分类分组 const menuRes await fetch(/api/dish/list?status1); const menu await menuRes.json(); return { table, menu }; } // 提交订单只传 dish_id 和 quantity价格由服务端算 async function submitOrder(tableId, items) { const res await fetch(/api/order/create, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ table_id: tableId, items: items.map(i ({ dish_id: i.dishId, quantity: i.quantity })) }) }); return res.json(); // 返回 order_id 和 total_amount }逻辑说明scanAndLoadMenu先校验桌台状态避免两个人同时扫同一张桌子造成重复开台。submitOrder只传菜品 ID 和数量单价和总价全部由服务端根据当前dish.price计算这是防止前端篡改价格的关键。参数上items里不要带price字段带了也忽略服务端以数据库为准。如果菜品在下单瞬间被下架或库存不足服务端要返回明确的错误码前端提示“XX 已售罄”而不是笼统报错。2.3 服务端算价与库存扣减的原子操作算价和扣库存必须放在同一个事务里否则会出现“钱算对了但库存没扣”或者“库存扣了但订单没生成”的脏数据。常见做法是用数据库事务包住“校验库存、扣减、写订单、写明细”四步。# 服务端创建订单事务内完成算价与扣库存 def create_order(table_id, items): with db.transaction(): total Decimal(0.00) order_id db.insert(orders, { table_id: table_id, status: 0, total_amount: 0 }) for item in items: # 加行锁防止并发超卖 dish db.query_one( SELECT id, name, price, stock FROM dish WHERE id%s FOR UPDATE, item[dish_id] ) if dish is None or dish[status] ! 1: raise BizError(菜品已下架) # stock 为 -1 表示不限量否则校验并扣减 if dish[stock] ! -1: if dish[stock] item[quantity]: raise BizError(f{dish[name]} 库存不足) db.execute( UPDATE dish SET stock stock - %s WHERE id %s, item[quantity], dish[id] ) line_total dish[price] * item[quantity] total line_total db.insert(order_item, { order_id: order_id, dish_id: dish[id], dish_name: dish[name], unit_price: dish[price], quantity: item[quantity] }) db.execute( UPDATE orders SET total_amount %s WHERE id %s, total, order_id ) return {order_id: order_id, total_amount: str(total)}逻辑说明FOR UPDATE是行级锁保证同一道菜在并发下单时库存扣减不会超卖。stock -1表示不限量适合那些现做现卖、不严格控量的菜品。total用Decimal累加避免浮点误差。整个函数在一个事务里任何一步抛异常都会回滚不会留下半截订单。参数上items的quantity要做正整数校验防止传 0 或负数刷单。3. 把源码跑起来本地环境搭建与最小可运行验证拿到一套移动点餐源代码别急着改业务先让它在本地跑通“扫码-点菜-下单”这条最小链路。跑通之后再动代码心里才有底。这一章按环境准备、启动、验证三步走每步都给可抄的命令。3.1 依赖清单与数据库初始化一套典型的移动点餐源码后端常见是 Python 或 Node.js前端是小程序或 H5数据库用 MySQL。先确认版本版本不对会出现各种奇怪的连接错误。组件建议版本用途MySQL5.7 或 8.0存菜品、订单数据Python3.8后端服务若为 Python 栈Node.js16前端构建或 Node 后端Redis5.0缓存菜单、防重复下单数据库初始化就是把上一章的四张表建出来再插几条测试数据-- 插入测试菜品 INSERT INTO dish (name, price, stock, status, category_id) VALUES (宫保鸡丁, 28.00, 100, 1, 1), (麻婆豆腐, 18.00, 100, 1, 1), (米饭, 2.00, -1, 1, 2); -- 插入测试桌台qr_token 用随机串 INSERT INTO dining_table (name, qr_token, status) VALUES (A01, t_9f3a2b7c1d, 0), (A02, t_4e8c6a0b5f, 0);逻辑说明stock -1的米饭表示不限量适合主食类。qr_token用t_前缀加随机串方便识别也方便排查。插入数据后用SELECT * FROM dish确认一下避免字符集问题导致中文乱码。3.2 启动后端与前端的最小命令后端启动前先配好数据库连接通常是一个.env文件或配置项。改完配置直接启动# 安装依赖以 Python 为例 pip install -r requirements.txt # 初始化数据库表结构 python manage.py migrate # 启动后端服务监听 8000 端口 python app.py --host 0.0.0.0 --port 8000前端如果是小程序用开发者工具导入项目目录改app.js里的baseUrl指向http://localhost:8000。如果是 H5直接npm install npm run dev逻辑说明--host 0.0.0.0是为了让同一局域网内的手机也能访问方便真机扫码测试。baseUrl必须和实际后端地址一致端口写错是最常见的“请求失败”原因。启动后先访问http://localhost:8000/api/dish/list?status1能看到 JSON 菜单就说明后端通了。3.3 用一条 curl 验证下单链路是否打通不依赖前端直接用 curl 模拟一次下单能最快定位问题出在后端还是前端。# 模拟扫码换桌台 curl http://localhost:8000/api/table/resolve?tokent_9f3a2b7c1d # 模拟提交订单1 份宫保鸡丁 2 份米饭 curl -X POST http://localhost:8000/api/order/create \ -H Content-Type: application/json \ -d { table_id: 1, items: [ {dish_id: 1, quantity: 1}, {dish_id: 3, quantity: 2} ] }逻辑说明第一条命令验证 token 能否正确解析出桌台。第二条命令验证下单接口预期返回order_id和total_amount按测试数据算应该是28 2*2 32.00。如果返回金额不对去查order_item表里unit_price是否存对如果报库存不足去查dish.stock是否被之前的测试扣掉了。参数上table_id要和dining_table.id对应别把 token 当 ID 传。4. 避坑与排查移动点餐源码落地时最容易翻车的五件事这套源码在本地跑通不难难的是放到真实餐厅环境里不出乱子。下面五条是我和同行踩过的坑每条按“现象、原因、解决”写清楚照着排查能省不少时间。4.1 同一桌重复开台订单金额翻倍现象顾客扫了两次码或者两个人同时扫结果生成了两条订单后厨收到两份一样的菜。原因dining_table.status没有在开台时及时置为“就餐中”或者置状态和创建订单不在一个事务里。解决在create_order开头加一步“开台”操作用UPDATE dining_table SET status1 WHERE id%s AND status0根据影响行数判断是否开台成功影响行数为 0 说明已被别人开台直接返回错误。这一步要和订单创建放在同一事务。4.2 菜品改价后历史订单金额跟着变现象老板把某道菜降价昨天的订单在后台重新打开时金额变了。原因order_item只存了dish_id展示时实时关联dish.price。解决下单时把unit_price和dish_name冗余进order_item展示历史订单只读明细表不再关联菜品表。已经上线的系统如果没存可以写一个一次性脚本按订单创建时间回填当时的价格但回填数据未必准所以最好一开始就设计对。4.3 并发下单导致库存超卖现象库存设了 10 份结果卖出 12 份。原因扣库存时先查后改两个请求同时查到库存 10各自扣 1都以为还剩 9。解决用SELECT ... FOR UPDATE加行锁或者用UPDATE dish SET stock stock - %s WHERE id %s AND stock %s这种带条件的原子更新根据影响行数判断是否扣成功。前者适合事务内多步操作后者适合简单扣减。4.4 二维码被猜出别人桌的订单被操作现象有人手动改 URL 里的桌台 ID给别的桌加菜或结账。原因二维码里直接用了自增 ID容易被遍历。解决二维码里放随机qr_token服务端用 token 换桌台不暴露真实 ID。token 要足够长建议 16 位以上随机串并且每张桌子独立不要所有桌子共用一个。已经用了 ID 的系统可以加一层签名校验但改 token 更彻底。4.5 后厨出单延迟顾客催菜现象顾客下单成功后厨半天没收到单。原因出单逻辑写在下单接口的同步流程里打印机卡纸或网络抖动导致整个请求超时订单状态卡在“待出单”。解决下单和出单解耦下单成功后往消息队列或一张print_task表里写一条待打印记录后厨端轮询或订阅。打印失败要能重试并且在前台能看到“出单失败”的告警而不是静默失败。参数上重试次数建议 3 次间隔 5 秒超过就人工介入。5. 进阶技巧用订单状态机管住整条链路别让状态乱跳前面讲的都是单点问题真正让一套移动点餐源码从“能跑”变成“好维护”的是订单状态机。我见过太多系统订单状态用几个布尔字段拼is_paid、is_printed、is_finished各管各的结果出现“已支付但未出单却显示已完成”这种自相矛盾的状态。后来我统一改成单字段状态机所有流转走一个入口问题少了一大半。状态机的最小实现思路是定义合法状态和合法流转任何状态变更都走transition函数非法流转直接拒绝。下面是一个可直接用的 Python 版本# 订单状态机只允许合法流转 ORDER_STATES { 0: 待支付, 1: 已支付, 2: 已出单, 3: 已完成, 4: 已取消 } # 定义每个状态可以流转到哪些状态 TRANSITIONS { 0: [1, 4], # 待支付 - 已支付 或 已取消 1: [2, 4], # 已支付 - 已出单 或 已取消退款 2: [3], # 已出单 - 已完成 3: [], # 已完成是终态 4: [] # 已取消是终态 } def transition(order_id, to_state): order db.query_one(SELECT id, status FROM orders WHERE id%s FOR UPDATE, order_id) if order is None: raise BizError(订单不存在) from_state order[status] if to_state not in TRANSITIONS.get(from_state, []): raise BizError(f非法流转{ORDER_STATES[from_state]} - {ORDER_STATES[to_state]}) db.execute(UPDATE orders SET status%s WHERE id%s, to_state, order_id) return True逻辑说明TRANSITIONS是白名单只允许列出的流转。FOR UPDATE保证并发下状态不会跳变。参数上状态值用整数而不是字符串省空间也快但要在代码里维护一份映射别在数据库里存中文。这个状态机的好处是任何“订单怎么变成这样了”的疑问都能通过查状态流转日志回答。建议再加一张order_status_log表每次transition写一条记录包含from_state、to_state、操作人、时间。出问题时翻日志比猜快得多。我自己的习惯是任何涉及金额和状态的系统先把状态机画出来再写代码。画不出来说明业务还没想清楚这时候写代码就是给自己埋雷。这套移动点餐源码的骨架不复杂难的是把并发、对账、出单这些边界情况都兜住。希望帮到你。本文还有配套的精品资源点击获取