拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Android点餐系统实战:离线下单、扫码核销与厨房实时同步

简介本资源是一份面向计算机专业本科生与Android开发初学者的课程设计文档聚焦餐饮行业无线点餐场景提供基于Java语言的完整系统设计方案与实现说明。文档涵盖后台管理系统含用户、餐桌、菜谱分类、订单等模块与前台Android客户端支持点菜、换桌、结账等核心功能并详细阐述JDBC、Ajax、jQuery等关键技术在Web与移动端的协同应用适合作为移动应用开发实践参考或毕业设计选题支撑材料。资源为单文件压缩包内含1个1MB的Word文档.docx内容结构完整含摘要、目录、引言、开发工具介绍Eclipse/MyEclipse/Tomcat/Android平台、需求分析及59页正文便于快速掌握系统架构与模块划分逻辑。目前已有49人学习下载读者可直接获取规范化的技术方案描述、界面交互设计思路及数据库MySQL集成要点助力项目复现与技术迁移。1. 一个能真正在餐厅后厨跑起来的 Android 点餐系统不是 Demo而是带状态同步、离线缓存和扫码核销的闭环很多刚学完 Android 开发的同学把「点餐系统」当成课程设计交作业——界面漂亮、按钮能跳转、数据存在内存里一关 App 全丢。但真实餐饮场景里服务员端着手机在嘈杂环境下单Wi-Fi 时断时续厨房屏要实时刷新新单、标记已做、拒单重派老板想看「今天 18:00–20:00 鱼香肉丝卖了多少份」还得导出 Excel。这些需求光靠ListView ArrayList或RecyclerView Room的基础组合根本撑不住。本篇讲的是基于 Android 原生技术栈不依赖第三方低代码平台从零构建一个可部署、可运维、可扩展的点餐系统它用WorkManager保障弱网下单不丢用RoomFlow实现本地库存与订单状态双向同步用CameraXML Kit Barcode Scanning支持桌码扫码快速核销所有业务逻辑封装在ViewModel中UI 层只负责渲染。适合已有 Android 基础熟悉 Activity/Fragment 生命周期、Jetpack 组件、正准备接小型商用项目或优化内部工具的开发者。2. 用 Android Studio 搭建可落地的点餐系统骨架模块划分、依赖选型与核心架构决策2.1 为什么不用纯 MVC 或 MVPMVVM Clean Architecture 是当前最稳的落地选择点餐系统不是静态展示页它涉及多端状态协同用户端下单、厨房端接单、管理端统计、频繁数据变更菜品库存实时扣减、订单状态流转和强可靠性要求网络中断时不能丢单。MVC 容易导致 Activity 膨胀成上帝类MVP 虽解耦但 Presenter 生命周期难管理且测试成本高。我们采用MVVM 分层 Clean ArchitecturePresentation 层Activity/Fragment只做 UI 渲染和事件分发通过ViewModel获取StateFlowUiStateDomain 层定义用例UseCase如PlaceOrderUseCase、UpdateOrderStatusUseCase不含 Android SDK 依赖Data 层Repository实现接口内部聚合Room本地数据源 Retrofit 远程数据源用NetworkBoundResource统一处理缓存策略Model 层Entity数据库实体、DtoAPI 数据传输对象、UiModelUI 层专用模型三者严格隔离。提示不要在ViewModel中直接 newRetrofit或RoomDatabase。所有数据访问必须经由Repository否则无法 Mock 测试也无法在离线时 fallback 到本地数据。2.2 Gradle 依赖配置聚焦稳定、轻量、可维护的 Jetpack 组合在app/build.gradle中我们放弃过度封装的商业 SDK如某些“点餐 SDK”坚持原生组件可控性// AndroidX Jetpack implementation androidx.core:core-ktx:1.12.0 implementation androidx.appcompat:appcompat:1.6.1 implementation androidx.activity:activity-ktx:1.8.2 implementation androidx.fragment:fragment-ktx:1.6.2 // UI implementation androidx.compose.ui:ui:1.5.4 // 仅用于部分新页面主流程仍用 XML ViewBinding implementation com.google.android.material:material:1.10.0 implementation androidx.constraintlayout:constraintlayout:2.3.1 // 架构 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 implementation androidx.lifecycle:lifecycle-runtime-ktx:2.7.0 implementation androidx.lifecycle:lifecycle-livedata-ktx:2.7.0 implementation androidx.lifecycle:lifecycle-flow:2.7.0 // 数据库 implementation androidx.room:room-runtime:2.6.1 implementation androidx.room:room-ktx:2.6.1 kapt androidx.room:room-compiler:2.6.1 // 网络 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.12.0 // 后台任务 implementation androidx.work:work-runtime-ktx:2.9.0 // 扫码 implementation androidx.camera:camera-core:1.3.0 implementation androidx.camera:camera-camera2:1.3.0 implementation androidx.camera:camera-lifecycle:1.3.0 implementation com.google.mlkit:barcode-scanning:18.4.0 // 其他 implementation androidx.datastore:datastore-preferences:1.1.1注意版本对齐room、lifecycle、camera均使用 2023 Q4 稳定版。避免混用alpha/beta版本尤其CameraX早期版本在部分 vivo/oppo 机型上存在预览黑屏问题1.3.0已修复主流 OEM 适配。2.3 项目模块化结构按业务域拆分而非技术类型拒绝app模块塞满所有代码。我们按实际业务边界划分app/ // 主入口只含启动 Activity 和全局依赖注入 data/ // Data 层实现Room DAO、Retrofit Service、RepositoryImpl domain/ // Domain 层UseCase、Repository 接口、实体定义 presentation/ // Presentation 层各页面的 ViewModel、UiState、Event feature-order/ // 订单相关下单页、订单列表、订单详情 feature-menu/ // 菜单相关分类页、菜品页、购物车 feature-kitchen/ // 厨房屏待处理单、进行中单、已完成单仅接收状态更新 feature-admin/ // 管理后台销售统计、库存预警、员工权限需登录每个feature-*模块api依赖domainimplementation依赖data形成清晰依赖箭头。这样未来若要将厨房屏独立打包为 TV 应用只需新建kitchen-tv模块复用feature-kitchen逻辑无需改一行业务代码。3. 核心功能实现从扫码下单到厨房状态同步的完整链路3.1 桌码扫码下单CameraX ML Kit 实现毫秒级识别与防重复提交用户进入餐厅扫描桌上二维码格式order://table/08A?shopshanghai01App 解析后直接跳转下单页。关键不是「能扫」而是「扫得准、不卡顿、不误触」3.1.1 CameraX 预览与扫码绑定在OrderScanFragment.kt中初始化预览private fun bindCameraUseCases() { val cameraProvider context?.let { ProcessCameraProvider.getInstance(it) } cameraProvider?.unbindAll() val preview Preview.Builder().build().also { preview - preview.setSurfaceProvider(binding.viewFinder.surfaceProvider) } val barcodeScanner BarcodeScanning.getClient( BarcodeScannerOptions.Builder() .setBarcodeFormats(Barcode.FORMAT_QR_CODE) // 仅识别 QR提升速度 .build() ) val imageAnalyzer ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() .also { analyzer - analyzer.setAnalyzer(ContextCompat.getMainExecutor(requireContext())) { imageProxy - val mediaImage imageProxy.image ?: returnsetAnalyzer val inputImage InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) barcodeScanner.process(inputImage) .addOnSuccessListener { barcodes - for (barcode in barcodes) { if (barcode.format Barcode.FORMAT_QR_CODE) { handleQrCode(barcode.rawValue ?: ) imageProxy.close() // 必须关闭否则内存泄漏 returnsetAnalyzer } } imageProxy.close() } .addOnFailureListener { imageProxy.close() } } } try { cameraProvider?.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, imageAnalyzer) } catch (e: Exception) { Log.e(CameraX, Use case binding failed, e) } }3.1.2 QR 解析与防重复逻辑handleQrCode()不是简单跳转而是校验与去重private fun handleQrCode(rawValue: String) { // 1. 格式校验必须是 order://table/{id}?shop{code} val regex order://table/([A-Za-z0-9])\\?shop([a-z0-9]).toRegex() val matchResult regex.find(rawValue) ?: run { showToast(无效桌码请重新扫描) return } val tableId matchResult.groupValues[1] val shopCode matchResult.groupValues[2] // 2. 防重复5 秒内同一桌码不重复触发 if (System.currentTimeMillis() - lastScanTime 5000 lastTableId tableId) { showToast(已进入该桌位无需重复扫描) return } lastScanTime System.currentTimeMillis() lastTableId tableId // 3. 导航并传参 findNavController().navigate( R.id.action_scan_to_order, bundleOf(table_id to tableId, shop_code to shopCode) ) }注意imageProxy.close()必须在onSuccess和onFailure中都调用否则ImageAnalysis会持续占用内存导致预览卡顿甚至 OOM。这是 CameraX 最常见的坑。3.2 订单创建与离线保障WorkManager Room 保证弱网下单不丢用户确认下单后系统需立即响应「已提交」同时确保在网络恢复后可靠送达服务器3.2.1 创建本地订单并触发后台同步// 在 OrderViewModel.kt 中 fun submitOrder(order: Order) { // Step 1: 本地插入Room viewModelScope.launch { try { val localOrderId orderDao.insert(order.copy(status OrderStatus.PENDING)) // Step 2: 提交 WorkRequest带参数 val workData Data.Builder() .putLong(local_order_id, localOrderId) .putString(shop_code, order.shopCode) .build() val constraints Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) // 仅在联网时执行 .build() val request OneTimeWorkRequestBuilderSyncOrderWorker() .setInputData(workData) .setConstraints(constraints) .addTag(sync_order_${localOrderId}) .build() WorkManager.getInstance(appContext).enqueue(request) _uiState.value UiState.Success(订单已提交正在同步...) } catch (e: Exception) { _uiState.value UiState.Error(本地保存失败${e.message}) } } }3.2.2 SyncOrderWorker 实现幂等同步class SyncOrderWorker( private val context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { private val orderDao OrderDatabase.getInstance(context).orderDao() private val apiService RetrofitClient.instance.create(OrderApi::class.java) override suspend fun doWork(): Result { val localOrderId inputData.getLong(local_order_id, -1) if (localOrderId -1L) return Result.failure() return try { val localOrder orderDao.findById(localOrderId) ?: return Result.failure() // 调用 API 提交订单含重试 val response withContext(Dispatchers.IO) { apiService.submitOrder(localOrder.toDto()).execute() } if (response.isSuccessful response.body() ! null) { // 服务器返回成功更新本地状态 orderDao.updateStatus(localOrderId, OrderStatus.CONFIRMED) Result.success() } else { // 服务器返回错误如库存不足标记为 FAILED 并通知用户 orderDao.updateStatus(localOrderId, OrderStatus.FAILED) Result.failure() } } catch (e: Exception) { // 网络异常保留 PENDING 状态下次重试 Result.retry() // WorkManager 自动重试最多 9 次默认 } } }关键点Result.retry()触发指数退避重试Result.failure()永久失败需人工干预。OrderStatus枚举必须包含PENDING、CONFIRMED、FAILED、CANCELED四种状态前端根据状态渲染不同 UI如FAILED显示「库存不足请修改」按钮。3.3 厨房屏实时状态同步用 Flow WebSocket 实现低延迟推送厨房屏不能每 5 秒轮询一次 API必须秒级响应。我们采用OkHttpWebSocket实现轻量推送3.3.1 建立长连接与消息路由在KitchenViewModel.kt初始化private val webSocket OkHttpClient().newWebSocket( Request.Builder() .url(wss://api.yourshop.com/kitchen?shopshanghai01token${authToken}) .build(), object : WebSocketListener() { override fun onOpen(webSocket: WebSocket, response: Response) { Log.d(WS, Connected) } override fun onMessage(webSocket: WebSocket, text: String) { try { val event Json.decodeFromStringKitchenEvent(text) when (event.type) { NEW_ORDER - { // 插入新订单到 Room并触发 UI 更新 orderDao.insert(event.order) _orders.value _orders.value.plus(event.order) } UPDATE_STATUS - { // 更新指定订单状态 orderDao.updateStatus(event.orderId, event.status) _orders.value _orders.value.map { if (it.id event.orderId) it.copy(status event.status) else it } } } } catch (e: Exception) { Log.e(WS, Parse error, e) } } override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { Log.e(WS, Connection failed, t) // 自动重连逻辑省略实际需加指数退避 } } )3.3.2 KitchenEvent 数据结构与状态映射Serializable data class KitchenEvent( val type: String, // NEW_ORDER, UPDATE_STATUS, CANCEL_ORDER val orderId: Long? null, val status: OrderStatus? null, val order: Order? null ) // OrderStatus 枚举需与后端协议一致 enum class OrderStatus { PENDING, // 待接单新订单 ACCEPTED, // 已接单厨师点击「开始制作」 COOKING, // 制作中可细分步骤此处简化 READY, // 已完成可上菜 CANCELED // 已取消 }注意WebSocket 连接必须在onCleared()中显式关闭否则 Activity 销毁后连接仍在后台运行消耗电量并可能引发内存泄漏。webSocket.close(1000, Goodbye)是标准做法。4. 关键参数调优与生产环境避坑指南4.1 Room 数据库性能索引、事务与查询优化实战点餐系统高频读写订单表order表需针对性优化4.1.1 必加索引字段在Entity中声明Entity( tableName orders, indices [ Index(value [table_id, status], unique false), // 查询某桌所有订单 Index(value [created_at], unique false), // 按时间排序 Index(value [shop_code, status], unique false) // 多门店筛选 ] ) data class Order( PrimaryKey(autoGenerate true) val id: Long 0, val table_id: String, val shop_code: String, val status: OrderStatus, val created_at: Long, // ... 其他字段 )4.1.2 批量插入必须用事务单条insert()100 次耗时约 1200ms用事务包裹耗时降至 80ms// 正确事务批量插入 fun insertOrders(orders: ListOrder) { database.withTransaction { orderDao.insertAll(orders) } } // 错误循环单条插入禁止 // orders.forEach { orderDao.insert(it) }4.1.3 查询避免 SELECT *用Query精确字段厨房屏只需显示table_id、status、items_count不必加载整个Order对象Query(SELECT id, table_id, status, items_count, created_at FROM orders WHERE shop_code :shopCode AND status IN (:statuses) ORDER BY created_at DESC LIMIT 20) fun getActiveOrders(shopCode: String, statuses: ListOrderStatus): FlowListOrderSummaryOrderSummary是精简 DTO比全量Order内存占用减少 65%。4.2 WorkManager 配置避免后台任务被系统杀死Android 8.0 对后台服务限制严格WorkManager 是唯一合规方案但需正确配置4.2.1 在AndroidManifest.xml中声明 Provider关键provider android:nameandroidx.startup.InitializationProvider android:authorities${applicationId}.androidx-startup android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroidx.work.WorkManagerInitializer android:valueandroidx.startup / /provider缺失此配置WorkManager 在部分华为/小米机型上初始化失败导致后台同步完全不触发。4.2.2 设置合理的重试策略默认ExponentialBackoffPolicy重试间隔过长首次 30s后续翻倍点餐场景需更快响应val request OneTimeWorkRequestBuilderSyncOrderWorker() .setInitialDelay(10, TimeUnit.SECONDS) // 首次延迟 10s避免冷启动并发 .setBackoffCriteria( BackoffPolicy.LINEAR, 30, TimeUnit.SECONDS // 线性退避每次加 30s最大重试 9 次 ) .build()4.3 CameraX 兼容性兜底当 ML Kit 扫码失败时启用 ZXing部分低端机如 Android 7.0 旧款ML Kit 初始化失败需降级private fun initBarcodeScanner() { try { // 尝试 ML Kit barcodeScanner BarcodeScanning.getClient(...) } catch (e: Exception) { // 降级到 ZXing需额外添加依赖 implementation com.journeyapps:zxing-android-embedded:4.3.0 Log.w(Scanner, ML Kit init failed, fallback to ZXing, e) useZxing true zxingScanner MultiFormatReader() } }ZXing 在低光照下识别率下降 40%但至少保证基础功能可用。5. 真实验证技巧用 adb 命令模拟弱网、强制状态与日志追踪5.1 三步验证离线下单是否真正可靠不要只测「断网时点提交按钮」要验证全流程制造离线环境adb shell settings put global airplane_mode_on 1 adb shell am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true提交订单后检查本地数据库是否写入adb shell sqlite3 /data/data/com.yourshop.app/databases/order_db select count(*) from orders where status\PENDING\; # 应返回 1恢复网络验证 WorkManager 是否自动触发adb shell settings put global airplane_mode_on 0 adb shell am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false # 查看 WorkManager 日志 adb logcat | grep SyncOrderWorker5.2 厨房屏状态同步延迟压测用 adb 模拟高并发订单向服务器发送 50 条订单观察厨房屏是否全部接收且无乱序# 用 curl 模拟批量下单需替换 token 和 shop_code for i in {1..50}; do curl -X POST https://api.yourshop.com/orders \ -H Authorization: Bearer YOUR_TOKEN \ -H Content-Type: application/json \ -d {table_id:T00$i,shop_code:shanghai01,items:[{id:1,qty:2}]} sleep 0.1 # 间隔 100ms模拟真实下单节奏 done厨房屏Logcat中应看到 50 条onMessage日志且orderId严格递增。若出现跳号或重复说明 WebSocket 消息未按序处理需在onMessage中加序列号校验。5.3 关键路径耗时监控在 ViewModel 中埋点在OrderViewModel.submitOrder()开头和结尾打点val startTime SystemClock.elapsedRealtime() // ... 执行插入、启动 WorkManager ... val duration SystemClock.elapsedRealtime() - startTime Log.d(OrderSubmit, Total time: ${duration}ms, DB insert: ${dbTime}ms, Work enqueue: ${workTime}ms)线上监控发现duration 300ms的订单占比超 5%则需优化Room插入逻辑如检查是否误用了Insert(onConflict OnConflictStrategy.REPLACE)导致全表扫描。提示不要用System.currentTimeMillis()做耗时统计它受系统时间调整影响SystemClock.elapsedRealtime()是单调递增的专为性能测量设计。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门