简介一套基于Android Studio开发的美食订餐点餐App完整项目含前端App与后台管理双端系统适合Android学习者、移动开发方向学生及毕业设计/课程设计参考。亮点在于集成人脸识别支付并覆盖登录、美食分类与搜索、轮播图、详情点赞、购物车增减、订单提交与支付、物流发货、确认收货、评论管理、用户信息与头像修改、收藏等完整闭环功能。后台侧提供登录、管理员信息维护、资源/角色/权限分配、字典管理、用户管理、美食管理、订单统计等模块整体结构清晰。压缩包共63个文件包含47张PNG与12张JPG界面截图、2份Markdown说明文档、1个数据库脚本和1个代码压缩包包体约35.3MB素材与脚本齐全方便直接导入数据库并对照截图还原项目。目前已有49人学习浏览可作为实战练手或快速搭建点餐类App的技术蓝本。1. 这不是又一套“只写前端的玩具项目”完整点餐闭环到底在讲什么餐饮点餐App的源码在网上并不稀缺但绝大多数你打开之后会发现只有一个AndroidStudio客户端连登录都是硬编码在Activity里的。而这个标题里有四样东西——源代码、数据库、后台管理、使用说明外加一个带人脸识别支付的亮点它意味着一条能跑通的前后端闭环顾客在手机上选菜下单订单进数据库商家在后台接单改状态最后通过人脸识别身份后完成支付。适合两类人一是想走全栈路线、需要一份能写进简历的完整项目的Android开发者二是小餐饮团队或外包接单者想直接在这套结构上改成自己的营业系统。我先把话说在前面人脸识别支付在这里的正确理解是“用刷脸确认身份并授权扣款”不是让你绕过微信、支付宝从零搭一套支付清算通道。搞清楚这个边界后面所有模块的取舍就都顺了。2. 拆解项目结构AndroidStudio客户端、数据库、后台管理与支付对接各自管什么2.1 客户端的主线功能与页面流转从AndroidStudio工程角度看这个App的主线不算复杂但每个环节都有必须落地的功能。先看点餐侧用户进入后先分角色——普通顾客走点餐流程商家或管理员走订单管理流程。点餐这条线依次是菜品分类列表、菜品详情、购物车、确认订单、选择支付方式、支付结果页商家这条线则是订单列表、订单详情、订单状态流转。对应到工程里的代码组织一般会按功能分包而不是按页面分包。常见的做法是这样的分层com.example.restaurant ├── activity // 页面层LoginActivity, MainActivity, CartActivity... ├── adapter // 列表适配器DishAdapter, OrderAdapter ├── fragment // 首页、分类、订单等碎片页面 ├── model // 实体类Dish, User, Order, OrderItem ├── network // Retrofit 接口定义与请求封装 ├── utils // 登录态管理、金额计算、人脸识别回调封装 └── db // 本地缓存Room 或 SQLite 辅助类页面流转其实决定了数据库表结构的走向。比如购物车如果只是前端内存变量用户杀进程就丢想做到“退出重进购物车还在”就必须有 cart 表或者 SharedPreferences 持久化。很多学生在答辩时被问“购物车数据存哪”答不上来就是因为在设计时没把“页面跳转传参”和“数据持久化”两件事分开。2.2 数据库选型为什么这个项目用MySQL而不是SQLite这是标题里另一个关键词——数据库。移动端本地存储选SQLite没有争议但服务端数据库的选型才是这个项目真正的分水岭。多数可运行的完整项目会选MySQL原因有三一是后台管理端几乎都是Web页面PHP或Java后端连MySQL是主流组合二是订单、菜品、用户之间的关系查询在MySQL里写起来顺三是后期要接真正在线支付的账单对账MySQL生态的中间件和文档明显比SQLite友好。数据库在整个闭环里扮演的是“唯一真相源”的角色。订单状态在客户端显示“待接单”在后台显示“新订单”两处数据必须来自同一条order记录而不是各自存一份再同步。设计上记住一句话客户端不直接改业务核心表所有写操作走后端接口数据库只认后端服务的指令。表结构我会在下一章给出核心部分这里先提一个常见翻车点菜品图片不要以路径字符串存数据库再把图片塞进AndroidStudio的drawable里。正确做法是图片放服务器或对象存储数据库只存URL字段。否则你换个环境跑项目图片全裂。2.3 后台管理端从菜品管理到订单看板后台管理是让这个项目和“课堂作业”拉开差距的关键模块。它通常是一套独立的Web系统跑在Tomcat或直接Spring Boot内嵌容器上端口和App的API服务分开。管理端解决这么几件事菜品增删改查上下架、分类维护、订单查看与状态变更接单、出餐、完成、用户管理以及简单的营业数据统计。后台和App的关系用一张表可以看得很清楚功能App端操作后台管理端操作共用数据表查看菜品顾客浏览菜单商家增删改菜品dish, dish_category下单顾客提交购物车商家查看新订单orders, order_item订单流转顾客看状态商家修改状态orders支付顾客发起支付商家确认到账payment_record2.4 人脸识别支付在链路里的正确位置人脸识别支付不是独立功能它横跨客户端、服务端和数据库三块。完整的时序是用户在支付页选择“人脸支付” → App调用摄像头采集人脸 → 提取特征值与当前登录用户绑定的人脸特征做比对 → 比对通过后客户端调用后端扣款接口 → 后端校验订单状态并更新支付记录 → 返回支付成功。所以你需要先接受一个概念人脸识别在这个项目里做的是身份认证不是资金清算。它回答的是“你是谁”至于“钱怎么从账户A到账户B”在真实商业环境里仍然由持牌支付机构完成。教学项目里最常见的落法是——识别通过后把订单金额从用户账户余额里扣掉余额存数据库里。3. 从零跑通项目AndroidStudio导入、数据库初始化和后台部署3.1 环境确认与AndroidStudio导入拿到源代码之后不要急着双击打开。先确认本机环境JDK版本、AndroidStudio版本、Gradle版本三者要匹配。这个坑我见过太多次——项目是两年前写的用的Gradle 4.x你电脑装的是AndroidStudio最新版默认Gradle 8.x直接打开必然同步失败。建议按这个顺序操作# 1. 确认 Java 环境 java -version # 期望输出 1.8 或 11看项目 build.gradle 里配置 # 2. 打开 AndroidStudio选择 Open定位到项目根目录含 settings.gradle 的目录 # 3. 等待 Gradle Sync 完成如果失败看下方 Messages 面板的具体报错如果Gradle同步报版本问题不要盲目升级。打开项目根目录的 build.gradle 看 classpath 里的 Gradle 插件版本再打开 gradle-wrapper.properties 看 distributionUrl两者要兼容。比如插件是3.1.0distributionUrl对应的Gradle版本应该是4.4左右强行升到6.x会出一堆API废弃报错。3.2 数据库初始化建库、建表、导入示例数据数据库脚本一般会放在项目根的 sql 目录或者 doc 目录下文件名类似 restaurant.sql。这是整套系统的地基务必在MySQL里完整执行一遍。连接MySQL建议直接命令行避免图形工具给你加一些奇怪的默认字符集mysql -u root -p # 输入密码后执行 SOURCE /你的绝对路径/restaurant.sql;执行完之后验证关键表是否建好USE restaurant; SHOW TABLES; -- 期望至少看到user, dish, dish_category, orders, order_item, payment_record核心表的建表逻辑做成代码块更容易看懂。下面这份是我在类似项目里比较偏好的结构字段上做了精简但关系是完整的-- 用户表区分顾客和管理员face_feature 存人脸特征值 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0 COMMENT 0-顾客 1-商家, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 账户余额, face_feature TEXT COMMENT 人脸特征向量JSON数组格式, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 菜品表status 控制上下架 CREATE TABLE dish ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, image_url VARCHAR(255) COMMENT 图片URL不是本地路径, category_id INT, status TINYINT DEFAULT 1 COMMENT 1-在售 0-下架, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表状态字段是整个流程的核心 CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务单号格式建议yyyyMMddHHmmss随机, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-待接单 1-已接单 2-已完成 3-已取消, pay_status TINYINT DEFAULT 0 COMMENT 0-未支付 1-已支付, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表一单多菜 CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, dish_name VARCHAR(100) NOT NULL COMMENT 冗余菜名防止菜品改名后历史订单错乱, price DECIMAL(10,2) NOT NULL COMMENT 下单时的单价快照, quantity INT NOT NULL DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几点参数说明。金额字段选 DECIMAL(10,2) 而不是 FLOAT是为了避免浮点精度问题——0.1加0.2在浮点数体系里会变成0.30000000000000004订单金额出现这种尾差对账会非常痛。人名和菜品名这些字段的字符集选 utf8mb4因为MySQL的utf8是阉割版存不了emoji万一菜品描述里带个表情图标写入直接报错。订单状态字段用TINYINT数字枚举而不是字符串是为了索引效率和状态机流转方便。导入示例数据时注意看有没有外键。很多教学项目的SQL脚本为了省事建表时带了外键约束导入时又因为数据插入顺序不对导致报错。如果SOURCE执行中报错但脚本不中断用SHOW WARNINGS看一下更稳妥的做法是把外键先拆掉在业务层保证引用完整性。3.3 后台管理系统的启动与连接排查后台管理如果是Spring Boot项目启动前要改三处配置数据库连接地址、端口、以及允许跨域的来源。特别是跨域配置——Android端访问后台接口用的是IP加端口比如http://192.168.1.100:8080/apiWeb管理端可能是http://localhost:9090不配跨域的话两边总有一边调不通接口。# application.yml 里核心配置 server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/restaurant?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver连接MySQL时最容易出的问题是serverTimezone没配导致的时间偏差报错。MySQL驱动8.x版本默认要求指定时区你不写它就给你抛异常。我的习惯是直接写Asia/Shanghai不要写UTC否则订单时间显示差8小时排查起来非常玄学。AndroidStudio模拟器访问后台时不能用localhost要用10.0.2.2指向宿主机的端口真机调试则要填电脑的局域网IP。这个差异是新手问得最多的问题之一本质上是Android模拟器的网络重定向机制在代码里把BaseUrl抽成一个常量换环境只改一处即可。4. 人脸识别支付的实现从摄像头采集到账户扣款的完整链路4.1 支付流程的时序设计与状态机把“刷脸支付”拆成两步走能少踩一半的坑。第一步是生物识别认证第二步是支付清算。这两个阶段必须分开不能把“识别成功”直接等同于“支付成功”——如果扣款接口超时或余额不足人脸识别通过但支付失败状态就乱了。所以流程设计上App端和后端接口的约定我建议这样客户端 后端 |--- 1. 请求创建人脸支付订单 -------| |-- 2. 返回本次支付token ----------| |--- 3. 摄像头采集人脸、提取特征 ----| |--- 4. 对比本次用户人脸特征 -------| |-- 5. 本地识别结果 --------------| |--- 6. 携带token请求扣款 --------| |-- 7. 扣款成功返回支付结果 ----|前端代码里对应的核心逻辑是这样一段。注意这里用了回调式的写法方便在Android子线程里做完耗时比对再切回主线程更新UI// 支付Presenter中的核心方法 public void startFacePay(String orderNo, String faceToken) { // step1: 从Camera采集的Bitmap中提取特征各SDK方式不同此处示意 float[] currentFeature faceDetector.extractFeature(faceBitmap); // step2: 从当前登录用户信息里取出注册时保存的特征 float[] registeredFeature parseFeature(loginUser.getFaceFeature()); // step3: 计算余弦相似度高于阈值才继续 float similarity FaceMatcher.cosineSimilarity(currentFeature, registeredFeature); if (similarity FACE_SIMILARITY_THRESHOLD) { mView.onFaceVerifiedFailed(similarity); return; } // step4: 本地识别通过后才带着token去调后端扣款接口 api.payByFace(orderNo, faceToken, userToken) .enqueue(new CallbackPayResult() { Override public void onResponse(CallPayResult call, ResponsePayResult response) { if (response.isSuccessful()) { mView.onFacePaySuccess(response.body()); } else { mView.onFacePayFailed(response.message()); } } Override public void onFailure(CallPayResult call, Throwable t) { mView.onFacePayFailed(t.getMessage()); } }); }这段代码有三个关键参数。FACE_SIMILARITY_THRESHOLD是相似度阈值常见做法是0.75到0.85之间太低了随便拿张照片就能过太高了换个发型就拒绝需要按你选的SDK实测调。faceToken是后端在上一步创建的临时凭证目的是绑定“哪个用户、哪笔订单、什么时候过期”而不是让客户端随便传一个订单号就能乱扣款。整个流程中人脸比对在客户端做扣款动作必须在服务端校验token。4.2 人脸注册别把登录和支付混在一起人脸支付能跑通的前提是用户注册过人脸。这里有个设计细节很多人忽略——用户登录时只是账号密码验证人脸注册应该是一个独立的“开通刷脸支付”步骤放在支付设置页让用户主动确认“我同意绑定我的生物特征用于支付”。注册人脸的本质是什么是把摄像头拍到的人脸转成一个特征向量然后存到数据库的 face_feature 字段里。这个字段存的是[0.0234, -0.1182, ...]这样的浮点数组序列化后的JSON而不是存一张图片。为什么因为特征向量是为了比对而生的图片是为了给人看而生的——比对时你要的是数学距离不是像素差异。存图片再加一个比对SDK每次识别都要重新提取特征性能差且费流量。不过要向你交代一句实话教学项目里用哪种人脸SDK直接决定了这个模块能走多远。从零手写一个卷积神经网络识别器是不现实的常见做法是集成虹软ArcFace免费版、百度人脸识别API或OpenCV的人脸特征提取器。如果你在AndroidStudio里跑离线方案ArcFace免费版有Android SDK可以提取512维特征并在本地比对适合做演示闭环。4.3 从“刷脸通过”到“钱扣了”服务端扣款接口的防重设计服务端扣款接口是最后一道闸也是防重最需要设计的地方。想象一个场景用户刷脸通过客户端在调扣款接口时网络超时用户又点了一次支付。如果没有防重机制订单就被扣了两次钱。处理手段是支付请求必须带业务单号后端对该单号做幂等控制。// 扣款接口的幂等处理先查再扣用事务和唯一索引兜底 Transactional public PayResult facePay(String orderNo, String token) { // 1. 校验token是否有效、是否已过期 FacePayToken payToken tokenMapper.selectByToken(token); if (payToken null || payToken.getExpireTime().before(new Date())) { return PayResult.fail(支付凭证无效或已过期); } // 2. 通过唯一索引防重同一个订单只能有一次成功的扣款记录 Orders order orderMapper.selectByOrderNo(orderNo); if (order null) { return PayResult.fail(订单不存在); } if (order.getPayStatus() 1) { return PayResult.success(orderNo); // 已支付直接返回成功不重复扣款 } // 3. 扣款扣用户余额加支付流水两次写在同一事务 int updated userMapper.deductBalance(payToken.getUserId(), order.getTotalAmount()); if (updated 0) { throw new BusinessException(账户余额不足); } paymentRecordMapper.insert(orderNo, payToken.getUserId(), order.getTotalAmount()); orderMapper.updatePayStatus(orderNo, 1); return PayResult.success(orderNo); }代码里的三处关键说明。第一第一步先校验支付凭证凭证在用户发起人脸支付时生成有效期给两分钟防的是有人拿旧token重复打接口。第二第二步查订单支付状态实现幂等但因为并发场景下查和改之间有竞态窗口所以最稳妥的兜底是在payment_record表的order_no上加唯一索引让数据库来干预第三张牌。第三扣余额和插入流水必须在同一个事务里否则会出现钱扣了但流水没记的对不上账这种错非常难查。5. 避坑指南从编译失败到订单对不上账5个最容易翻车的位置5.1 Gradle版本与依赖冲突导致项目同步失败现象打开AndroidStudio后Gradle Sync报一堆红BuildConfig找不到或者依赖库方法找不到。原因项目里的Gradle插件版本和依赖库版本是一个组合体改了一边另一边就跟不上。解决先用命令行看gradle-wrapper.properties里的 distributionUrl 和 build.gradle 里的 classpath两个版本要匹配再检查依赖冲突最常见的AndroidX和support库混用——老项目用com.android.support新环境默认AndroidXSDK版本超过34时强制要求AndroidX只能在gradle.properties里加android.useAndroidXtrue并替换全部依赖。这是血泪换来的遇到别改依赖改到一半就放弃先整体替换再逐个清错。5.2 MySQL字符集导致中文菜品名写入报错或乱码现象SQL脚本执行成功但后台添加“黄焖鸡米饭”时提示Incorrect string value或者查出来是问号。原因建表时用的DEFAULT CHARSETutf8而MySQL的utf8不是真正的UTF-8存不了四字节字符。解决建表语句里统一CHARSETutf8mb4连接串里加characterEncodingutf8mb4。已建好的表用ALTER TABLE dish CONVERT TO CHARACTER SET utf8mb4;转一下。我每次初始化数据库执行完脚本第一件事是执行SHOW TABLE STATUS LIKE dish;看Collation是不是utf8mb4_general_ci。5.3 Android模拟器连不上后端API现象App在模拟器里启动请求接口立刻失败日志报Failed to connect to localhost/127.0.0.1。原因模拟器里的localhost指的是模拟器自己不是你的电脑。解决把网络BaseUrl里的localhost或127.0.0.1改成10.0.2.2这是Android模拟器预留的宿主机地址。用真机调试时改成电脑的局域网IP且保证手机和电脑在同一个WiFi下如果还连不上检查Windows防火墙有没有拦8080端口。5.4 人脸比对通过但支付失败的“假成功”问题现象刷脸提示成功界面跳到了支付成功页但后台一看订单还没付款或者用户余额没扣。原因客户端把“人脸识别通过”直接当成了支付完成没有等待扣款接口返回或者扣款接口失败后客户端没处理回调。解决支付结果页的跳转必须发生在扣款接口返回成功之后不是人脸比对回调之后。状态机上严格区分FACE_VERIFIED和PAY_SUCCESS两个状态界面可以用onFaceVerifiedSuccess提示“身份确认成功”然后展示“正在支付…”的等待态等接口返回再跳结果页。此外人脸识别的演示项目经常有人用一张照片就能解锁根源是阈值调太低或没有活体检测后续要加的话优先找带动作活体或红外活体的SDK。5.5 订单金额对不上浮点运算的精度陷阱现象点两份9.9元的菜前端计算显示19.8后端订单却是19.8000000000000004。原因客户端用double或float做金额计算浮点数二进制存储有误差。解决所有金额计算统一用字符串传给后端或者在前端先把分单位存整数。AndroidStudio的Kotlin代码里可以用BigDecimal来算Java端也要求用BigDecimal.valueOf(9.9)而不是new BigDecimal(9.9)——后者把double的精度误差又带进来了。后端MySQL字段用DECIMAL(10,2)收尾任何金额入表都按这个精度做约束。6. 还能再做厚一点全链路自测清单与离线上线的取舍整套系统能跑通之后我的习惯是留一张自测清单做回归验证每次改动完代码照着过一遍比临时想测试点靠谱得多。下面这份就是这个项目的关键链路你可以直接用验证项操作预期结果注册登录新用户注册老用户登录密码加密入库非明文菜品浏览顾客App查看分类和菜品图片正常显示下架菜品不出现下单流程加购2份菜品生成订单数据库orders出现待接单记录金额正确商家接单后台将订单置为“已接单”App端订单状态同步变化人脸注册用户绑定人脸特征face_feature字段写入特征向量人脸支付支付页选刷脸比对成功余额减少payment_record生成流水幂等重试同一订单二次请求扣款返回成功但不再扣第二次异常路径余额不足时刷脸支付提示余额不足订单保持未支付状态上表中最后两行是我最看重的地方因为大多数演示项目只做通了“正常路径”把异常路径补上回项目才拿得出手。关于这套方向值不值得投入我给一点个人判断如果你是在校学生这套全栈链路足够撑起毕业设计或求职项目但强烈建议把“人脸识别支付”改成“人脸识别登录支付业务模拟”并且在简历里明确写是模拟支付切勿写成与权威机构合作的真实支付通道这是红线。如果你是外包接活这套结构可以快速改造成小餐厅的扫码点餐系统但生产环境的人脸支付一定要对接持牌支付机构的开放平台接口自己搭的那套只能做演示。最后说一个我自己栽过的跟头最开始为了演示效果把人脸比对的日志打到了控制台结果调试时一不小心截到日志里输出的特征向量之后凡是涉及用户生物信息的调试输出一律脱敏。这个习惯我保留到了现在。人脸特征和密码一样属于敏感数据数据库的face_feature字段在生产环境至少要加密存储。希望帮到你。本文还有配套的精品资源点击获取