ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于Android的电子点餐系统:架构、SQLite存储与结账逻辑实现

基于Android的电子点餐系统:架构、SQLite存储与结账逻辑实现 简介这是一份针对餐饮信息化场景的Android电子点餐系统毕业论文适合需要完成Android方向毕业设计的学生、餐饮信息系统研发人员以及对移动点餐应用感兴趣的学习者。论文以弥补传统纸质菜单易脏乱、点餐效率低等问题为切入点基于C/S架构采用Java语言、Eclipse开发环境与SQLite数据库完整论述了系统需求分析、功能结构设计、界面实现与测试过程。从内容预览可见文中包含中英文摘要、绪论、各功能模块说明及结账实现细节覆盖显示菜品分类、单价、口味、已点数量与总价等核心功能并针对系统性能与稳定性进行了全面测试。包体方面压缩包共1个文件为docx格式文档大小仅58KB便于直接阅读与编辑。该资源已有170人学习结构完整脉络清晰既可作为毕业论文撰写范本也能为Android应用开发教学或餐厅信息化升级提供参考设计思路与实现细节。1. 基于Android的电子点餐系统设计与实现先想清楚架构再写界面基于Android的电子点餐系统在餐饮场景里不是简单的“菜单App”它要解决纸质菜单易脏、服务员手写单易错、结账对账耗时这三件事。我接手这个课题后的第一个教训是一上来画界面后台数据流一塌糊涂只好推倒重来。这个课题采用C/S架构Android终端负责展示菜品分类、单价、口味、已点数量和总价后台负责接收点餐数据并完成结算开发环境从当年的Eclipse ADT迁移到Android Studio也很顺利。它适合两类人一是做餐饮信息化改造的开发者想快速落地一个可演示的点餐终端二是准备做Android毕业设计的学生需要理解Activity跳转、SQLite存储和简单网络通信的完整链路。2. 电子点餐系统的C/S架构与SQLite数据层设计先从结论说起这个系统采用C/S架构而不是B/S不是因为浏览器做不了点餐而是因为点餐场景对响应速度和离线容忍度有要求。服务生拿着终端在桌边操作每点一个菜都要等页面刷新体验会很差。C/S架构把菜品缓存到客户端大部分UI操作在本地完成只有结账和同步时才走网络这样即使餐厅WLAN出现波动点餐动作也不会中断。2.1 为什么桌面端与移动端要分开设计论文里把服务器放在厨房客户端用Android模拟器或真机这个划分本身就是从实际分工出发的厨房需要看到已点菜品前台需要开台和结账服务员需要拿着终端在餐桌边操作。三者的数据源是同一条订单链路如果全部写在一个程序里后续加一个后厨打印功能就要动所有代码。所以我的建议是先按角色拆模块服务员终端负责选菜和下单后厨端负责显示订单收银端负责结算。即使毕业设计只实现了服务员终端部分也要在数据库设计里预留后厨和收银的字段否则后期扩展要改表结构。数据库是整个系统里最先要定的部分。我用SQLite做客户端本地缓存服务端用MySQL或SQL Server都行。SQLite是嵌入式关系型数据库Android系统自带不需要额外启动服务适合存放菜品、订单状态这类结构化数据。原论文里配了Tomcat实际Android客户端用不到除非你想把点餐数据通过HTTP发到JSP页面再展示。四张核心表的关系可以简化为下面的表格表名主键作用关联字段dishdish_id菜品信息无table_infotable_id桌台状态无ordersorder_id一桌一次点餐table_noorder_itemitem_id订单中的每道菜明细order_id、dish_id2.2 核心表结构与建表SQL我一般把数据表拆成四张菜品表、桌台表、订单表、订单明细表。菜品表存菜单本身桌台表管开台和翻台订单表和订单明细表是一对多关系避免每点一个菜就生成一条订单记录。下面是精简后的建表语句CREATE TABLE dish ( dish_id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, -- 特色菜/热菜/凉菜/汤/酒/套餐 name TEXT NOT NULL UNIQUE, -- 菜名 price REAL NOT NULL DEFAULT 0, taste TEXT DEFAULT , -- 口味比如微辣/中辣/免葱 image_path TEXT, status INTEGER DEFAULT 1 -- 1在售0下架 ); CREATE TABLE table_info ( table_id INTEGER PRIMARY KEY AUTOINCREMENT, table_no INTEGER NOT NULL UNIQUE, status INTEGER DEFAULT 0 -- 0空桌1有人2待清洁 ); CREATE TABLE orders ( order_id INTEGER PRIMARY KEY AUTOINCREMENT, table_no INTEGER NOT NULL, status INTEGER DEFAULT 0, -- 0开台1已点餐2已结账 create_time TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE order_item ( item_id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, dish_id INTEGER NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, price REAL NOT NULL, -- 下单时的菜价快照 FOREIGN KEY (order_id) REFERENCES orders(order_id), FOREIGN KEY (dish_id) REFERENCES dish(dish_id) );执行前要注意三点第一price字段用REAL只适合做演示真实收银一定要用整数分或DECIMAL后面章节我会专门讲金额精度问题第二order_item里存price快照这样以后菜品改价不影响已经生成的订单第三taste字段不要用逗号分隔多个口味宁可拆成口味表否则统计口味销量时要写模糊查询。我用这几张表跑通“开台—点菜—结账”的所有流程。2.3 线程队列与点餐数据上传客户端的点餐动作产生订单数据后不能在主线程里直接访问网络或写数据库。Android主线程负责UI一旦阻塞就会ANR。原论文里提到“后台主要负责利用线程队前台的数据进行传输与处理”这个思路是对的但没有给出具体实现。我用的办法是单线程的ExecutorService配合SQLite同步状态字段把待上传的订单先写到本地再按顺序逐条上传public class OrderUploader { private final ExecutorService uploadQueue Executors.newSingleThreadExecutor(); private final Context context; public OrderUploader(Context context) { this.context context.getApplicationContext(); } public void enqueueOrder(final long orderId) { uploadQueue.execute(() - { OrderRepository repo new OrderRepository(context); Order order repo.getById(orderId); boolean success HttpUtils.postJson( http://192.168.1.20:8080/order, order.toJson() ); if (success) { repo.markSynced(orderId); } else { repo.markPendingRetry(orderId); } }); } }这段代码的关键是把队列和UI线程隔离。newSingleThreadExecutor保证订单按提交顺序上传避免并发插入导致的两条明细乱序markSynced是把SQLite里的同步标志位改成1下次启动时只查标志位为0的数据补传。HttpUtils.postJson里我一般设置5秒连接超时、10秒读取超时失败时不弹Toast而是静默重试否则服务员站在餐桌旁一直看到报错提示会很烦躁。3. 从Eclipse迁移到Android StudioActivity跳转与点餐界面实现这一章讲界面层的实现。原设计跑在Eclipse 3.x ADT插件上我建议直接迁移到Android Studio。理由不是追新而是ADT在现在的macOS和Windows 10以上系统里经常崩溃模拟器启动也慢Android Studio的Gradle构建能自动处理资源引用错误对排查布局问题帮助很大。3.1 老项目结构与迁移步骤Eclipse Android项目的核心目录有三个src放Java源文件、res放布局和图片、AndroidManifest.xml注册组件。迁移到Android Studio只要新建一个空项目把src目录下的包结构复制到app/src/main/java把res复制到app/src/main/res再检查AndroidManifest.xml的package属性构建脚本里写上android { compileSdk 34 defaultConfig { applicationId com.restaurant.ordering minSdk 19 targetSdk 34 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 }迁移时最容易出问题的不是代码而是资源名。Eclipse时代允许数字开头的drawable文件名比如1.jpg放到Android Studio里会直接编译报错需要全部改成dish_1.jpg这样的合法命名。另一个坑是AndroidManifest里用android:name指向Activity时如果漏写.前缀旧环境可能容忍新环境会报ClassNotFoundException。我一般建议按包名逐条核对manifest中的四个组件Activity、Service、BroadcastReceiver、ContentProvider。3.2 用Intent完成页面跳转与携带桌号点餐系统里有大量页面切换欢迎页到主菜单、主菜单到分类页、分类页到结算页。在Android里页面切换的标准做法是Intent。原论文里用六个Intent分别跳特色菜、热菜、凉菜等代码重复很高。我会用一个方法收敛private void openCategory(Class? extends Activity target, String category) { Intent intent new Intent(this, target); intent.putExtra(category, category); intent.putExtra(tableNo, currentTableNo); startActivity(intent); }调用时按钮监听器里传目标Activity和分类字符串Button coldBtn findViewById(R.id.btn_cold); coldBtn.setOnClickListener(v - openCategory(ColdDishActivity.class, 凉菜));这里的putExtra是Activity之间传递参数的常用接口。category用于分类页过滤菜品tableNo让结算页知道是哪张桌在结账。如果你在真机上调试发现跳转后参数为空先检查目标Activity的onCreate里是否用了getIntent().getStringExtra(category)两个地方的键拼写必须完全一致。另外startActivity之后如果不想让返回键回到上一页比如开台之后可以在startActivity后调用finish()这样返回键会把整个会话退出。3.3 点餐界面CheckBox选择、EditText数量与实时价格统计分类页的每一行菜品我用一个CheckBox表示是否点这道菜一个EditText让顾客输入数量下面放一个TextView显示这道菜的小计。CheckBox的监听器需要在setOnCheckedChangeListener里根据选中状态决定把菜品加入或移出“已点列表”private ListOrderItem selectedItems new ArrayList(); private void bindDishItem(View itemView, Dish dish) { CheckBox cb itemView.findViewById(R.id.cb_dish); EditText etCount itemView.findViewById(R.id.et_count); TextView tvSubtotal itemView.findViewById(R.id.tv_subtotal); cb.setOnCheckedChangeListener((buttonView, isChecked) - { if (isChecked) { int count parseCount(etCount.getText().toString()); selectedItems.add(new OrderItem(dish, count)); } else { removeDishFromSelected(dish); } refreshTotalPrice(); }); }parseCount方法要做输入保护空字符串返回1解析失败返回1大于99就截断到99。为什么这么做因为EditText是自由输入顾客可能填0或填一个超大数如果不限制结算页就会算出负总价或者溢出。refreshTotalPrice里遍历selectedItems用dish.getPrice()乘数量求和再把结果显示到底部的“已点数量和总价”区域。这里有一个很容易踩的坑CheckBox在列表滚动时会被回收复用导致选中状态错乱。我建议使用RecyclerView而不是ScrollView包LinearLayout并且在onBindViewHolder里用holder的位置信息设置监听器不要用全局的selectedItems直接驱动UI否则你会看到第5行的勾跑到第3行上。4. 结账逻辑与数据一致性价格统计、退菜与异常处理原论文的结账实现是典型的学生写法在按钮点击里用一连串if分支叠加金额代码里a、b、c、d、e五个变量累计菜品价格。我最初也这么写后来发现两个问题一是新增菜品要改点击方法二是连续点击按钮会把金额重复叠加。这种写法的来源是界面上的固定分类——特色菜、热菜、凉菜、汤、酒、套餐每个分类对应一个按钮于是很自然用不同变量保存。但菜单是动态的今天加一道菜明天换一道菜分类数量和菜品种类是变化的固定变量无法支撑这种变化。4.1 为什么用动态列表替代固定变量累加让我把原论文的代码结构还原一下if (isChecked(a)) { a quantity * 45; total a; } if (isChecked(b)) { b quantity * 26; total a b; } // ... 后续 if 继续叠加这段代码如果运行在模拟器上手工点几道菜看不出问题。但把它部署到真实餐厅场景就会发现一个关键缺陷每个菜品的单价是硬编码的一旦厨师调整菜价需要改代码重新打包而且if分支的顺序耦合了金额累计的顺序如果取消一道菜total不会自动减掉它。更好的做法是维护一个List每次结账时重新计算。我在上一节已经引入了selectedItems列表这里只需把总价计算收敛到一个方法中private double computeTotal(ListOrderItem items) { double total 0; for (OrderItem item : items) { total item.getPrice() * item.getQuantity(); } return total; }这个方法的优点是数据来源只有一个列表任何对勾选状态的修改都会触发refreshTotalPrice不会出现金额残留。我建议把数据放到Room或SQLite中而不是只保存在内存里否则用户切到后台再回来Android可能回收Activity已点列表就丢了。另外如果服务端也要统计营业额客户端直接传明细比传总价更可靠。总价在服务端重新计算客户端被篡改时也能发现。4.2 金额精度float不能用于收银菜单上的单价一般写成26、15、20看起来是整数但打折后可能出现19.9、49.9这样的值。用double累加会出现19.9 0.1 20.0显示成19.999999这类精度问题。我在测试时就遇到过一次三道菜总价101.5界面却显示101.49999999999999。这不是Android的问题而是二进制浮点数的表示限制。收银系统必须用BigDecimal。因为Kotlin和Java里BigDecimal构造字符串是精确的而构造double仍然会有精度问题。比如new BigDecimal(19.9)会得到一个19.900000000000001...的数所以我的工具方法先转成字符串private BigDecimal toBigDecimal(String priceText) { return new BigDecimal(priceText).setScale(2, RoundingMode.HALF_UP); } private BigDecimal computeTotalWithScale(ListOrderItem items) { BigDecimal total BigDecimal.ZERO; for (OrderItem item : items) { BigDecimal price toBigDecimal(String.valueOf(item.getPrice())); BigDecimal quantity BigDecimal.valueOf(item.getQuantity()); total total.add(price.multiply(quantity)); } return total.setScale(2, RoundingMode.HALF_UP); }参数说明setScale(2, RoundingMode.HALF_UP)表示保留两位小数四舍五入。为什么不用Math.round因为在多次乘法后直接round可能产生累计误差所以每一步都用BigDecimal最后统一截取小数位。SQLite中存储时我也建议把价格乘以100存成INTEGER分显示时再除以100这样即使将来做折扣计算存取都不会丢精度。4.3 取消点菜的状态同步原论文结束语里提到“取消点菜的过程中存在一点问题”这是这套设计最常见的坑当用户在A分类勾选了一道菜又到B分类勾选另一道菜回到A分类取消刚才那道菜如果CheckBox的选中状态没有和selectedItems同步就会出现UI上取消成功、结账金额里却仍然包含这道菜的情况。解决办法是让列表成为唯一事实来源。在取消时先按dishId找到对应的OrderItem而不是按对象相等移除。因为同一个菜品如果被添加了两次对象不同但dishId相同。删除方法如下private void removeDishFromSelected(Dish dish) { for (int i 0; i selectedItems.size(); i) { if (selectedItems.get(i).getDishId() dish.getDishId()) { selectedItems.remove(i); break; // 只移除一个 } } }这样即使CheckBox因为列表复用导致UI状态错乱最终计算总价时仍以selectedItems为准不会多算也不会少算。为了让这个结论可验证建议在点餐页放一个“已点明细”的只读列表实时显示每道菜的id、名称、数量和当前金额方便和结账页对账。已点明细和结账页的数据源要保持一致避免出现“点餐页显示5道菜、结算只算4道”的诡异问题。如果同一道菜在UI上需要多次添加比如先点一份再加一份建议在列表里增加quantity累加而不是添加两条相同的dishId。这样取消时要处理“减一份”还是“全取消”的交互逻辑我在演示项目里只处理了单选勾选但正式环境至少要做长按菜品弹窗修改数量的功能。5. 系统测试与发布从模拟器到真机的验证清单这套系统涉及界面跳转、数据库写入、网络上传三条链路单靠手点不能证明逻辑正确。我把测试重点放在三个场景重复点菜、切换页面后返回、断网状态下结账。以下测试用例表是这套流程的核心用例编号操作预期结果TC01连续点击“结账”5次只生成一个订单金额不翻倍TC02点A分类2道菜进B分类再返回A取消1道已点列表和结账总价同时减少1道TC03开启飞行模式后点餐并结账不崩溃提示“网络不可用”本地订单标记待同步TC04修改系统日期后重新开台开台时间取当天本地时间订单不串桌TC05输入数量0或99数量被重置为1或截断为99其中TC03需要打一个点飞行模式网络不可用HttpUtils.postJson抛出的IOException必须被捕获。我一般会为这个用例在代码里埋一个boolean开关模拟失败避免每次测试都断网。Android自带的logcat配合adb命令可以在命令行里完成大部分验证adb install -r app-debug.apk adb shell am start -n com.restaurant.ordering/.MainActivity adb logcat -s AndroidRuntime:E ActivityTaskManager:I这里-R表示覆盖安装保留本地SQLite数据am start用来从命令行启动一个Activity便于验证冷启动路径logcat按标签过滤崩溃。客户端崩溃时优先看AndroidRuntime的FATAL EXCEPTION信息页面跳转异常则看ActivityTaskManager的START日志。发布前的最后一步我建议做一次“清数据”操作在模拟器的设置里清除应用数据然后完整跑一遍开台、点菜、结账。完成后查看SQLite数据库adb shell run-as com.restaurant.ordering cat databases/order.db如果表里的订单数和支付金额与UI展示一致就可以签名发布。签名的注意事项只有一个使用同一把keystore签名升级包否则用户升级时会出现“未安装该应用”的提示。整套流程跑通后我最后做的一件事就是改系统日期跨天测试确认开台时间字段跟随系统日期变化防止次日凌晨营业时订单时间错乱。本文还有配套的精品资源点击获取
返回列表