简介:一套基于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_record |
2.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.0,distributionUrl对应的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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 菜品表: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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表:状态字段是整个流程的核心 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表:一单多菜 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;几点参数说明。金额字段选 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/api,Web管理端可能是http://localhost:9090,不配跨域的话,两边总有一边调不通接口。
# application.yml 里核心配置 server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/restaurant?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/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 Callback<PayResult>() { @Override public void onResponse(Call<PayResult> call, Response<PayResult> response) { if (response.isSuccessful()) { mView.onFacePaySuccess(response.body()); } else { mView.onFacePayFailed(response.message()); } } @Override public void onFailure(Call<PayResult> 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,新环境默认AndroidX,SDK版本超过34时强制要求AndroidX,只能在gradle.properties里加android.useAndroidX=true并替换全部依赖。这是血泪换来的,遇到别改依赖改到一半就放弃,先整体替换再逐个清错。
5.2 MySQL字符集导致中文菜品名写入报错或乱码
现象:SQL脚本执行成功,但后台添加“黄焖鸡米饭”时提示Incorrect string value,或者查出来是问号。原因:建表时用的DEFAULT CHARSET=utf8,而MySQL的utf8不是真正的UTF-8,存不了四字节字符。解决:建表语句里统一CHARSET=utf8mb4,连接串里加characterEncoding=utf8mb4。已建好的表用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字段在生产环境至少要加密存储。希望帮到你。
本文还有配套的精品资源,点击获取