ARTICLE DETAIL

资讯详情

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

基于AndroidStudio的美食订餐App全栈源码解析:数据库、后台与人脸支付实践

基于AndroidStudio的美食订餐App全栈源码解析:数据库、后台与人脸支付实践 简介一套基于 Android Studio 开发的完整美食订餐点餐 App 项目源码包适合移动开发学习者、毕业设计选型者以及餐饮行业数字化初探者。项目采用前后端分离思路后台管理端覆盖登录退出、管理员信息维护、资源与角色权限分配、字典管理、用户管理、美食管理及订单统计等模块App 端实现美食列表、轮播图、搜索、点赞、购物车增减删、订单提交与支付并创新性地引入人脸识别支付同时具备评论、收藏、个人资料与密码修改等完整闭环功能。资源共 63 个文件以 png 运行截图与 jpg 项目素材为主包含可直接导入的 SQL 数据库脚本、项目代码压缩包及中英文使用说明整体约 35.3MB目录层次清晰便于按模块检索。已有 48 人学习下载适合用于课程设计、毕业设计或作为人脸识别支付场景的参考实现。1. 一个订餐点餐App源码项目该怎么拆客户端、后台与数据库的边界先立住拿到“基于AndroidStudio的美食订餐点餐app源代码数据库后台管理使用说明人脸识别支付”这样的项目包第一反应通常都是赶紧用Android Studio打开点一下Run。但这类全栈小项目真正容易翻车的地方不在客户端界面——而是后台服务、数据库结构和人脸识别支付这三块。它们只要有一处对不上App编译得再干净也登录不进去、下单不成功。先把这个包拆开看Android客户端负责点餐、购物车、下单和刷脸支付入口后台管理负责菜品上下架、订单状态更新数据库负责把用户、菜品、订单、人脸特征这些数据存下来。人脸识别支付是整套系统的溢价点用户注册一张人脸支付时刷脸后从预充值余额或虚拟账户扣款。适合的人群也很明确——想拿一个完整全栈项目做毕业设计、面试作品或者给中小餐饮店做内部点餐演示的人。2. 从数据库表设计看整体架构六张表怎么支撑点餐与人脸支付2.1 一条点餐订单从手机到后台的完整链路先把数据流说清楚否则后面写代码容易绕晕。用户在Android端浏览菜品列表把菜品加入购物车购物车只是本地状态下单时才把“用户ID、菜品ID列表、数量、是否人脸支付”组装成一个JSON请求发给后台管理的接口。后台先查菜品表和状态确认没有下架算出总价不能信客户端传的金额然后把订单写入orders表把每个菜品明细写入order_detail表。如果走人脸支付再调人脸识别服务成功后回调后台更新orders表的状态整个过程才结束。后台管理端是另一条分支管理员在Web页面登录后改food表的status字段实现上架下架查orders表筛订单点“完成”把订单状态从未完成改成已完成。Android端不直接连MySQL必须经过后台接口这是这套项目最重要的架构约定。本地SQLite只做菜单缓存和购物车持久化不是业务主库。2.2 六张核心表的建表SQL与字段说明我见过不少项目包把数据库做得特别随意user表连手机号都没有订单表没有状态字段导致后面做支付回调无从下手。这套项目数据库建议按下面的结构建六张表各管一块CREATE DATABASE IF NOT EXISTS food_order DEFAULT CHARSET utf8mb4; USE food_order; CREATE TABLE admin ( admin_id INT NOT NULL AUTO_INCREMENT, username VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 建议存加盐哈希, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (admin_id) ) ENGINEInnoDB; CREATE TABLE user ( user_id INT NOT NULL AUTO_INCREMENT, nickname VARCHAR(30) DEFAULT , phone VARCHAR(20) DEFAULT , face_token TEXT COMMENT 人脸特征值JSON数组格式, status TINYINT DEFAULT 1 COMMENT 1正常 0禁用, PRIMARY KEY (user_id) ) ENGINEInnoDB; CREATE TABLE food ( food_id INT NOT NULL AUTO_INCREMENT, category VARCHAR(20) DEFAULT 热菜, name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, image_url VARCHAR(255) DEFAULT , PRIMARY KEY (food_id), KEY idx_status (status) ) ENGINEInnoDB; CREATE TABLE orders ( order_id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, pay_type TINYINT DEFAULT 0 COMMENT 0普通 1人脸, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (order_id), KEY idx_user (user_id) ) ENGINEInnoDB; CREATE TABLE order_detail ( detail_id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL, food_id INT NOT NULL, food_name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, PRIMARY KEY (detail_id), KEY idx_order (order_id) ) ENGINEInnoDB; CREATE TABLE face_pay_log ( log_id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, order_id INT DEFAULT NULL, face_token_hash VARCHAR(255) DEFAULT , match_score DECIMAL(5,2) DEFAULT 0 COMMENT 相似度分值, status TINYINT DEFAULT 0 COMMENT 0失败 1成功, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (log_id), KEY idx_order (order_id) ) ENGINEInnoDB;这里有三个字段值得单独说明。orders.status是整套订单流程的状态机核心从0到3的流转必须由后台控制不能让客户端随意改。order_detail里的food_name和price是下单时的快照因为菜品表后来可能改价、改名等你对账时查明细价格必须还原成下单那一刻的数据。face_pay_log是给人脸支付单独加的流水表每次刷脸识别状态、相似度分值都记下来出纠纷时有据可查。2.3 人脸特征数据到底存哪存JSON还是存二进制人脸识别支付听起来复杂工程上拆开就是“注册”和“核对”两步。注册时用SDK把用户正脸照片转成一串特征向量——常见方案是提取128维float数组而不是把照片原图传给服务器。所以user表里的face_token字段设计成TEXT存的就是这128个浮点数组成的JSON串比如[0.1245, -0.0821, …]。识别时客户端再采一张脸同样转成128维特征拿到后台和face_token比对。两个容易踩的点必须说。第一不建议在user表里加一个photo字段直接存人脸照片的BLOB或URL。照片体积大、涉及隐私而且识别时还要再做一次特征提取接口响应时间会从几百毫秒变成几秒。第二face_token的精度要统一。客户端SDK提取的float如果直接用Java默认的Float.toString序列化会产生很长的浮点尾巴同一张脸两次提取的特征值在小数点后第7位可能不一样比对时容易被阈值卡掉。做法是提取后统一格式化成5位小数再入库存JSON比对时用余弦相似度而不是全等比较。3. Android客户端落地三步走登录、加购物车、提交订单的代码骨架3.1 请求层怎么搭OkHttp的封装和超时参数Android客户端和后端交互这套项目里最稳的组合是OkHttp加Gson。登录是最典型的POST请求拿着用户名和密码去换一个登录token回来。下面是一段可以直接放进项目的登录代码private void login(String username, String password) { OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); JSONObject json new JSONObject(); try { json.put(username, username); json.put(password, password); } catch (JSONException e) { e.printStackTrace(); } RequestBody body RequestBody.create( MediaType.parse(application/json; charsetutf-8), json.toString()); Request request new Request.Builder() .url(BASE_URL /user/login) .post(body) .build(); client.newCall(request).enqueue(new Callback() { Override public void onFailure(Call call, IOException e) { // 网络层失败提示检查后台是否启动 } Override public void onResponse(Call call, Response response) throws IOException { String result response.body().string(); // 解析返回的 token 和 userId } }); }connectTimeout设成10秒readTimeout也设成10秒对校园网和局域网演示环境都够用。如果你只在模拟器里跑注意BASE_URL不能用https://localhost模拟器访问宿主机要写http://10.0.2.2:8080真机调试要写电脑的局域网IP。这一项能卡掉一半初次跑通项目的人。3.2 点菜与购物车的数据结构数量、合计金额怎么算点餐界面用RecyclerView做菜单列表每一个菜品item右侧有个“加入购物车”的按钮。购物车本质是一个MapInteger foodId, Integer quantity推荐单独拎一个CartManager类维护别把数量散落在各个Fragment里。public class CartManager { // key: foodId, value: 数量 private MapInteger, Integer items new LinkedHashMap(); public void add(int foodId) { items.put(foodId, items.getOrDefault(foodId, 0) 1); } public void reduce(int foodId) { if (!items.containsKey(foodId)) return; int count items.get(foodId) - 1; if (count 0) { items.remove(foodId); } else { items.put(foodId, count); } } public int getTotalCount() { int count 0; for (int v : items.values()) { count v; } return count; } public MapInteger, Integer getItems() { return items; } }这段的关键是getOrDefault加购时不需要先判断Map里有没有这个key一行搞定数量累计。减购时减到0必须remove避免出现“数量为0还占着条目”的脏数据。购物车金额不要在客户端自己用double算浮点误差会让总额显示成39.999999正确做法是提交订单后由后台根据数据库里的菜品单价重新算总价客户端只展示一次参考金额。3.3 提交订单请求参数、返回码和token的坑下单接口是这套系统的核心链路。客户端把购物车内容、用户ID、是否人脸支付一起提交。如果勾选了人脸支付还要带上faceToken字段后台先验证人脸身份再走扣款逻辑。public void submitOrder(int userId, MapInteger, Integer cart, String faceToken) { JSONObject itemsJson new JSONObject(); for (Map.EntryInteger, Integer e : cart.entrySet()) { itemsJson.put(String.valueOf(e.getKey()), e.getValue()); } JSONObject payload new JSONObject(); try { payload.put(userId, userId); payload.put(items, itemsJson); payload.put(facePay, faceToken ! null); if (faceToken ! null) { payload.put(faceToken, faceToken); } } catch (JSONException e) { e.printStackTrace(); } RequestBody body RequestBody.create( MediaType.parse(application/json; charsetutf-8), payload.toString()); Request request new Request.Builder() .url(BASE_URL /order/create) .post(body) .build(); // 后续 enqueue 逻辑略 }后台要区分“用户没有勾选人脸支付”和“用户勾选了但没传faceToken”。前一种情况facePayfalse走普通下单后一种情况facePaytrue但faceToken为空后台应当直接返回“人脸未注册”的提示而不是把订单创建成待支付。返回码也要统一约定我一般用三个状态200表示成功400表示参数错误401表示登录失效500表示服务端异常。客户端拿到401必须跳回登录页不要让用户反复重试。4. 后台管理端怎么接住订单事务、接口约定与数据库并发要点4.1 后台管理的接口风格统一返回体与状态码这套项目里的后台管理端建议直接用Spring Boot加JdbcTemplate比MyBatis少一层XML配置对拿到源码想快速跑起来的用户更友善。所有接口统一返回一个Result对象里面包含code、message、data三个字段这样Android端和Web管理端解析时不用各写一套逻辑。public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(int code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }关键约定是任何时候都不要把后端异常堆栈直接返回给客户端。数据库连不上、SQL写错统一返回“服务异常”具体错误看后台日志。不然客户端拿到一串英文异常用户看不懂调试时也分不清是谁的问题。4.2 创建订单时要注意的事务与锁下单不是一条insert语句就完事的它涉及orders和order_detail两张表以及菜品价格核对。如果中间某一步失败可能出现“订单主表有记录明细表空着”的问题。解决办法就是把整个过程放进一个事务Transactional public Long createOrder(OrderDTO dto) { BigDecimal total BigDecimal.ZERO; for (ItemDTO item : dto.getItems()) { Food food foodDao.getById(item.getFoodId()); // 菜品不存在或已下架直接拒绝 if (food null || food.getStatus() 0) { throw new BizException(菜品已下架 item.getFoodId()); } // 服务端重新算总价不信任客户端 total total.add(food.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); } // 插入主表 Order order new Order(); order.setUserId(dto.getUserId()); order.setTotalPrice(total); order.setStatus(0); orderDao.insert(order); // 插入明细表 for (ItemDTO item : dto.getItems()) { Food food foodDao.getById(item.getFoodId()); OrderDetail detail new OrderDetail(); detail.setOrderId(order.getOrderId()); detail.setFoodId(food.getFoodId()); detail.setFoodName(food.getName()); detail.setPrice(food.getPrice()); detail.setQuantity(item.getQuantity()); orderDetailDao.insert(detail); } return order.getOrderId(); }这段代码里最容易被新手忽略的是“重新读取菜品价格”。客户端传来的quantity可以信但价格必须以数据库为准。后台管理端并发高时还要注意MySQL的默认隔离级别两个用户同时抢最后一个库存时先对food表加行锁再扣库存避免超卖。实际项目里如果菜品数量很大可以在food表加一个stock字段下单时用UPDATE food SET stock stock - 1 WHERE food_id ? AND stock 0靠这个条件判断是否抢到。4.3 菜品上下架与后台管理的SQL后台管理的菜品列表页本质上就是一组增删改查。上架下架不需要改业务逻辑就是一条UPDATEPutMapping(/admin/food/{foodId}/status) public ResultVoid updateStatus(PathVariable Integer foodId, RequestParam Integer status) { if (status null || (status ! 0 status ! 1)) { return Result.error(400, status只接受0或1); } int rows jdbcTemplate.update( UPDATE food SET status ? WHERE food_id ?, status, foodId ); if (rows 0) { return Result.error(404, 菜品不存在); } return Result.success(null); }这里status参数必须做白名单校验防止有人传2、3之类的非法值。数据库里最好也加一个CHECK约束做兜底。菜品分页查询时如果表里数据过万记得在category和status上建联合索引否则后台页面打开会明显卡顿。我见过不少项目包把菜品列表查询写成SELECT *连个WHERE都不带数据一多后台管理直接黑匣子一样转圈。5. 人脸识别支付最容易翻车的4个坑现象、原因、解决5.1 把整张人脸照片存进数据库导致注册和识别都超时现象用户在App上传一张自拍请求转圈5、6秒才返回数据库表体积涨得飞快。原因注册接口直接把Bitmap转ByteArray后塞进BLOB字段后台收到照片还要现场调人脸SDK提取特征网络传输和计算叠加在一起自然慢。解决把“提取特征”放到客户端做只上传128维特征向量数据库里只存特征JSON。这样还能避免手机和服务器传原图照片带来的隐私包袱无论从性能还是合规角度都是更合理的方案。5.2 支付回调迟迟不来订单状态停在“待支付”现象用户刷脸成功后App提示支付完成但后台管理端看到订单状态还是0过一会儿才变成1甚至一直是0。原因这类项目常用的做法是前端轮询订单状态而人脸支付回调是异步的回调通知延迟时前端轮询先拿到了旧状态把界面错误地标记为失败。解决后端在回调接口里做幂等处理以orderId为唯一键先查orders当前状态只有状态为0时才改成1前端收到回调结果后延时2秒再查一次订单详情以第二次结果为准。同时face_pay_log表里记录每次回调的订单号和识别分数方便对账。5.3 人脸注册和支付校验共用一个接口分不清业务边界现象用户只是想刷脸支付接口却要求先传手机号、昵称、人脸照片流程被拉得很长。原因设计接口时没区分“注册人脸”和“验证人脸”两个动作。注册是低频操作一年可能只做一次支付验证是高频操作每次下单都要做。解决拆成两个接口。POST /user/face/register负责把人脸特征关联到userId写入user.face_tokenPOST /order/create里收到faceToken后只是调内部的matchFace方法比对现有特征。这样也方便以后接入真的人脸支付SDK替换的只是内部实现不改变接口约定。5.4 特征向量精度不一致同一张脸第二次识别失败现象第一次注册能用第二次刷脸支付时相似度只有0.75低于阈值直接拒绝。原因特征向量从float转JSON时用了不同精度的格式化方式或者SDK内部对图片缩放算法不同导致两次提取出的128维数组在小数点后第六位开始发散。解决注册和识别两条链路都用同一套SDK、同一个缩放尺寸特征值统一用BigDecimal保留5位小数后再入库存JSON。比对算法用余弦相似度不要用欧氏距离阈值建议设在0.82到0.88之间太高会误拒太低会被他人照片蒙混过关。6. 最后用一条模拟链路验证整套系统不接真实支付也能演示人脸支付这一节讲一个我反复用的验证方法。很多人拿到项目后想立刻对接真实支付渠道其实第一次跑通根本没必要。先把整套系统跑起来用一条模拟链路验证“注册人脸→下单→刷脸回调→订单状态变更→后台看得到订单”这个闭环确认无误后再决定接不接真实通道否则问题混在一起连bug都不知道是谁的。模拟链路分四步在后台服务启动的前提下操作。第一步注册人脸直接在命令行用curl模拟客户端上传特征值curl -X POST http://127.0.0.1:8080/user/face/register \ -H Content-Type: application/json \ -d {userId:1,faceToken:[0.12,0.84,-0.32,0.56]}这段请求会写入user表的face_token正常返回200即可。第二步模拟提交订单带上facePay参数curl -X POST http://127.0.0.1:8080/order/create \ -H Content-Type: application/json \ -d {userId:1,items:[{foodId:1,quantity:2}],facePay:true}注意这里故意不传faceToken后台应该返回“人脸未注册”的400错误这样才能验证状态码分支。第三步带上faceToken再提交一次后台校验通过后返回订单IDcurl -X POST http://127.0.0.1:8080/order/create \ -H Content-Type: application/json \ -d {userId:1,items:[{foodId:1,quantity:2}],facePay:true,faceToken:[0.12,0.84,-0.32,0.56]}第四步模拟支付回调把订单状态从待支付改成已支付curl -X POST http://127.0.0.1:8080/pay/face/notify \ -d orderId1status1matchScore0.86跑完这四步去后台管理页面刷新订单列表如果这条订单显示已支付且金额和菜品都对得上整套系统的主链路就已经验证通过了。最后把face_pay_log表和orders表的数据对上确认流水存在。我自己的习惯是永远先跑模拟链路再谈真实支付通道这个习惯让我少踩了很多“接口还没调通就忙着接SDK”的坑。就算以后要换成人脸识别支付的真实服务商也只是替换verifyFace这个内部方法的实现客户端和后台管理都不用动。希望帮到你。本文还有配套的精品资源点击获取
返回列表