餐系統(tǒng):SQLite+ContentProvider輕量架構(gòu)實(shí)戰(zhàn))
簡(jiǎn)介本資源是一份面向計(jì)算機(jī)專業(yè)本科生及移動(dòng)應(yīng)用開發(fā)初學(xué)者的Android智能點(diǎn)餐系統(tǒng)課程設(shè)計(jì)文檔聚焦餐飲場(chǎng)景下的移動(dòng)端Web端協(xié)同開發(fā)實(shí)踐解決傳統(tǒng)點(diǎn)餐流程效率低、信息同步難、管理粗放等實(shí)際問(wèn)題。文檔完整呈現(xiàn)了基于EclipseJavaTomcatMySQL的技術(shù)棧選型依據(jù)、MVC分層架構(gòu)設(shè)計(jì)邏輯、WebService跨平臺(tái)數(shù)據(jù)交互實(shí)現(xiàn)方案以及用戶注冊(cè)登錄、供/點(diǎn)餐信息發(fā)布、購(gòu)物車、訂單管理、顧客反饋等核心功能模塊的設(shè)計(jì)與測(cè)試過(guò)程。資源為單個(gè)PDF文件1.4MB內(nèi)容涵蓋開發(fā)工具配置、關(guān)鍵技術(shù)原理JDBC數(shù)據(jù)庫(kù)訪問(wèn)、HTTP通信機(jī)制、客戶端Activity交互設(shè)計(jì)、C/S架構(gòu)部署示意圖及系統(tǒng)功能測(cè)試用例圖文結(jié)合結(jié)構(gòu)清晰便于理解前后端協(xié)同邏輯。目前已有152人學(xué)習(xí)下載適合開展課程設(shè)計(jì)、畢業(yè)設(shè)計(jì)參考或AndroidWeb全棧開發(fā)入門實(shí)踐。1. 一個(gè)跑在安卓手機(jī)上的點(diǎn)餐系統(tǒng)為什么不能只靠“做個(gè)App”就完事你手里的外賣App背后是千萬(wàn)級(jí)并發(fā)的云服務(wù)、實(shí)時(shí)庫(kù)存校驗(yàn)、支付風(fēng)控和騎手調(diào)度算法而校園食堂、社區(qū)小餐館、連鎖茶飲店的智能點(diǎn)餐系統(tǒng)恰恰相反——它必須在無(wú)穩(wěn)定Wi-Fi、低配安卓設(shè)備、離線可操作、老板自己會(huì)改菜單的前提下把“掃碼→選品→下單→打印小票”閉環(huán)跑通。這不是炫技的Demo而是每天早八點(diǎn)排隊(duì)高峰時(shí)掃碼槍掃不出、菜品圖片加載失敗、訂單卡在“提交中”就直接導(dǎo)致顧客轉(zhuǎn)身離開的真實(shí)戰(zhàn)場(chǎng)。本篇講的就是如何用Android原生能力不依賴第三方SDK或云端中間件構(gòu)建一個(gè)可離線運(yùn)行、菜單熱更新、本地打印支持、權(quán)限精簡(jiǎn)可控的輕量級(jí)智能點(diǎn)餐系統(tǒng)。面向的是中小餐飲場(chǎng)景的技術(shù)負(fù)責(zé)人、高職院校電子系統(tǒng)設(shè)計(jì)課程學(xué)生以及需要交付可維護(hù)、可審計(jì)、無(wú)黑盒依賴的嵌入式軟件工程師——它不追求高并發(fā)但必須每一步都可追溯、可調(diào)試、可降級(jí)。2. 用Android Studio構(gòu)建最小可行點(diǎn)餐架構(gòu)Activity SQLite ContentProvider 的三支柱設(shè)計(jì)2.1 為什么放棄Room和Jetpack Compose從部署現(xiàn)場(chǎng)倒推技術(shù)選型在社區(qū)老年食堂部署時(shí)我們遇到過(guò)三類典型設(shè)備2018年款紅米Note5Android 8.1、2020年款華為暢享20Android 10、2022年款vivo Y33sAndroid 12。它們共性是系統(tǒng)WebView老舊、Google Play Services缺失、后臺(tái)進(jìn)程被廠商強(qiáng)殺。此時(shí)若采用Room數(shù)據(jù)庫(kù)其編譯期注解處理器在低版本AGPAndroid Gradle Plugin下易報(bào)annotationProcessor沖突若用Compose需強(qiáng)制升級(jí)到AndroidX Activity 1.8而舊設(shè)備上androidx.activity:activity-compose的rememberLauncherForActivityResult在Android 8.1上會(huì)觸發(fā)NoSuchMethodError。因此本系統(tǒng)采用原生SQLiteOpenHelper 手動(dòng)Cursor管理 ContentProvider封裝數(shù)據(jù)訪問(wèn)層——它不依賴任何Jetpack組件API Level 14Android 4.0起即完全兼容且所有SQL語(yǔ)句可直接在ADB shell中驗(yàn)證adb shell sqlite3 /data/data/com.example.smartorder/databases/order.db .tables SELECT name FROM sqlite_master WHERE typetable;提示ContentProvider在此并非為跨應(yīng)用共享數(shù)據(jù)而是為統(tǒng)一約束CRUD入口、避免多Activity直接操作SQLiteDatabase實(shí)例導(dǎo)致的線程鎖死。即使單進(jìn)程它也提供了query()方法的CancellationSignal支持防止列表滾動(dòng)時(shí)查詢阻塞UI線程。2.2 數(shù)據(jù)庫(kù)表結(jié)構(gòu)設(shè)計(jì)兼顧離線一致性與菜單動(dòng)態(tài)擴(kuò)展核心表僅兩張menu_items菜品主表和orders訂單主表無(wú)外鍵約束SQLite外鍵默認(rèn)關(guān)閉且影響寫入性能通過(guò)應(yīng)用層邏輯保證一致性表名字段類型說(shuō)明menu_items_idINTEGER PRIMARY KEY AUTOINCREMENT自增主鍵非業(yè)務(wù)IDitem_codeTEXT UNIQUE NOT NULL業(yè)務(wù)唯一碼如TEA-001用于掃碼匹配nameTEXT NOT NULL菜品名稱UTF-8支持中文priceREAL NOT NULL DEFAULT 0.0單價(jià)REAL類型避免浮點(diǎn)精度問(wèn)題categoryTEXT DEFAULT default分類標(biāo)簽用于RecyclerView分組image_pathTEXT本地文件路徑如/storage/emulated/0/SmartOrder/images/tea001.jpgis_availableINTEGER DEFAULT 10下架1上架控制前臺(tái)可見(jiàn)性orders_idINTEGER PRIMARY KEY AUTOINCREMENT訂單自增IDorder_noTEXT UNIQUE NOT NULL業(yè)務(wù)訂單號(hào)格式Y(jié)YYYMMDDHHmmSS-XXXXXstatusTEXT CHECK(status IN (draft,submitted,printed,cancelled))狀態(tài)機(jī)避免非法流轉(zhuǎn)created_atINTEGER NOT NULL時(shí)間戳毫秒用于排序和超時(shí)判斷items_jsonTEXT NOT NULLJSON字符串存儲(chǔ)菜品明細(xì)含item_code、quantity、notes2.2.1 關(guān)鍵設(shè)計(jì)決策說(shuō)明item_code而非_id作為掃碼匹配字段掃碼槍輸出為純文本若用自增ID每次APP重裝后ID重置導(dǎo)致歷史菜單無(wú)法關(guān)聯(lián)而item_code由運(yùn)營(yíng)人員在后臺(tái)Excel中維護(hù)導(dǎo)出CSV后由ContentProvider批量導(dǎo)入。items_json字段替代關(guān)聯(lián)表避免多表JOIN在低端機(jī)上卡頓JSON解析用org.json.JSONObjectAPI Level 1起內(nèi)置不引入Gson或Jackson增加包體積訂單詳情頁(yè)用TextView.setText()直接渲染JSON格式化字符串省去RecyclerView嵌套。is_available字段實(shí)現(xiàn)軟下架前端ListView的Adapter.getView()中過(guò)濾is_available0項(xiàng)無(wú)需刪除數(shù)據(jù)保留銷售統(tǒng)計(jì)依據(jù)。2.3 ContentProvider實(shí)現(xiàn)暴露安全的數(shù)據(jù)訪問(wèn)接口在AndroidManifest.xml中聲明Provider禁用android:exportedtrueAndroid 12強(qiáng)制要求provider android:name.data.MenuContentProvider android:authoritiescom.example.smartorder.menu android:exportedfalse android:permissioncom.example.smartorder.permission.MENU_ACCESS /MenuContentProvider.java中query()方法嚴(yán)格校驗(yàn)projection只允許返回指定字段和selection禁止11注入Override public Cursor query(NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteDatabase db dbHelper.getReadableDatabase(); // 白名單校驗(yàn)projection ListString allowedColumns Arrays.asList(_id, item_code, name, price, category, is_available); if (projection ! null) { for (String col : projection) { if (!allowedColumns.contains(col)) { throw new IllegalArgumentException(Invalid column: col); } } } // 防注入selectionArgs必須非空且selection中?占位符數(shù)等于args長(zhǎng)度 if (selection ! null (selectionArgs null || selectionArgs.length 0)) { throw new IllegalArgumentException(Selection requires arguments); } return db.query(menu_items, projection, selection, selectionArgs, null, null, sortOrder); }注意selectionArgs參數(shù)必須使用這是SQLite防SQL注入的唯一可靠方式。曾有團(tuán)隊(duì)直接拼接name LIKE % keyword %導(dǎo)致輸入% OR 11 --即可繞過(guò)所有條件。3. 掃碼與本地打印不依賴廠商SDK的硬件交互落地方案3.1 通用掃碼集成用Intent Filter捕獲USB/藍(lán)牙掃碼槍輸入多數(shù)商用掃碼槍工作在HID Keyboard模式——即插即用無(wú)需驅(qū)動(dòng)。其原理是將掃描結(jié)果模擬為鍵盤輸入因此無(wú)需申請(qǐng)CAMERA權(quán)限也不用調(diào)用CameraX API。只需在MainActivity中重寫onKeyDown()捕獲連續(xù)輸入的字符流private StringBuilder scanBuffer new StringBuilder(); private long lastInputTime 0; Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (event.getAction() KeyEvent.ACTION_DOWN) { if (keyCode KeyEvent.KEYCODE_0 keyCode KeyEvent.KEYCODE_9) { scanBuffer.append((char) (0 keyCode - KeyEvent.KEYCODE_0)); } else if (keyCode KeyEvent.KEYCODE_ENTER) { String scannedCode scanBuffer.toString().trim(); if (!scannedCode.isEmpty()) { handleScannedItem(scannedCode); // 查找菜品并加入購(gòu)物車 } scanBuffer.setLength(0); // 清空緩沖區(qū) return true; } else if (keyCode KeyEvent.KEYCODE_DEL) { if (scanBuffer.length() 0) scanBuffer.deleteCharAt(scanBuffer.length() - 1); } } return super.onKeyDown(keyCode, event); } private void handleScannedItem(String code) { Cursor cursor getContentResolver().query( Uri.parse(content://com.example.smartorder.menu/menu_items), new String[]{_id, name, price}, item_code ? AND is_available 1, new String[]{code}, null ); if (cursor ! null cursor.moveToFirst()) { String name cursor.getString(cursor.getColumnIndexOrThrow(name)); double price cursor.getDouble(cursor.getColumnIndexOrThrow(price)); addToCart(name, price, code); cursor.close(); } else { Toast.makeText(this, 未找到商品 code, Toast.LENGTH_SHORT).show(); } }3.1.1 適配不同掃碼槍的回車鍵差異部分掃碼槍發(fā)送KEYCODE_NUMPAD_ENTER數(shù)字小鍵盤回車需在onKeyDown()中補(bǔ)充判斷} else if (keyCode KeyEvent.KEYCODE_NUMPAD_ENTER) { // 同KEYCODE_ENTER處理邏輯 }提示測(cè)試時(shí)用adb shell input keyevent KEYCODE_0模擬按鍵比反復(fù)插拔掃碼槍更高效。若掃碼后無(wú)反應(yīng)先檢查L(zhǎng)ogcat中是否輸出onKeyDown日志確認(rèn)焦點(diǎn)在Activity上非Dialog或EditText獲取了焦點(diǎn)。3.2 本地?zé)崦舸蛴C(jī)驅(qū)動(dòng)用BluetoothSocket直連ESC/POS指令避開廠商SDK如Bixolon、Star的閉源jar包采用標(biāo)準(zhǔn)藍(lán)牙SPP協(xié)議發(fā)送ESC/POS指令。關(guān)鍵步驟3.2.1 獲取已配對(duì)藍(lán)牙設(shè)備列表BluetoothAdapter adapter BluetoothAdapter.getDefaultAdapter(); SetBluetoothDevice pairedDevices adapter.getBondedDevices(); for (BluetoothDevice device : pairedDevices) { if (device.getName().toLowerCase().contains(printer)) { // 按名稱模糊匹配 connectPrinter(device); break; } }3.2.2 發(fā)送打印指令以打印小票為例private void printReceipt(String orderNo, ListOrderItem items, double total) { try { OutputStream os bluetoothSocket.getOutputStream(); // 初始化打印機(jī) os.write(new byte[]{0x1B, 0x40}); // 設(shè)置居中對(duì)齊 os.write(new byte[]{0x1B, 0x61, 0x01}); os.write(【智能點(diǎn)餐】\n.getBytes(StandardCharsets.UTF_8)); os.write((訂單號(hào) orderNo \n).getBytes(StandardCharsets.UTF_8)); os.write(----------------------------\n.getBytes(StandardCharsets.UTF_8)); for (OrderItem item : items) { String line String.format(%-12s %dx %.2f\n, item.name, item.quantity, item.price * item.quantity); os.write(line.getBytes(StandardCharsets.UTF_8)); } os.write((總計(jì)¥ String.format(%.2f, total) \n).getBytes(StandardCharsets.UTF_8)); os.write(----------------------------\n.getBytes(StandardCharsets.UTF_8)); os.write(謝謝惠顧\n.getBytes(StandardCharsets.UTF_8)); // 切紙指令部分機(jī)型需 os.write(new byte[]{0x1D, 0x56, 0x01}); os.flush(); } catch (IOException e) { Log.e(Printer, Print failed, e); Toast.makeText(this, 打印失敗 e.getMessage(), Toast.LENGTH_LONG).show(); } }3.2.3 權(quán)限與Android 12適配要點(diǎn)AndroidManifest.xml中聲明uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / !-- Android 12掃描必需 --Android 12需在build.gradle中添加android { compileSdk 33 defaultConfig { targetSdk 33 } }運(yùn)行時(shí)請(qǐng)求ACCESS_FINE_LOCATION僅用于發(fā)現(xiàn)藍(lán)牙設(shè)備不要請(qǐng)求BLUETOOTH_CONNECT該權(quán)限需用戶手動(dòng)在設(shè)置中開啟體驗(yàn)極差。4. 菜單熱更新與離線優(yōu)先策略用Asset目錄版本號(hào)控制實(shí)現(xiàn)零停機(jī)更新4.1 菜單數(shù)據(jù)源設(shè)計(jì)CSV文件托管于assets目錄APP啟動(dòng)時(shí)自動(dòng)加載將menu.csv放入app/src/main/assets/目錄格式如下item_code,name,price,category,image_filename,is_available COFFEE-001,美式咖啡,18.0,飲品,coffee001.jpg,1 TEA-002,茉莉花茶,12.0,飲品,tea002.jpg,1 SNACK-003,薯?xiàng)l,8.0,小吃,snack003.jpg,0首次安裝或版本升級(jí)時(shí)Application.onCreate()中執(zhí)行private void loadMenuFromAssets() { try { InputStream is getAssets().open(menu.csv); BufferedReader reader new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); String line; ContentValues values new ContentValues(); while ((line reader.readLine()) ! null) { String[] cols line.split(,); if (cols.length 6) continue; // 跳過(guò)標(biāo)題行或格式錯(cuò)誤行 values.clear(); values.put(item_code, cols[0].trim()); values.put(name, cols[1].trim()); values.put(price, Double.parseDouble(cols[2].trim())); values.put(category, cols[3].trim()); values.put(image_path, /assets/images/ cols[4].trim()); // 統(tǒng)一前綴 values.put(is_available, Integer.parseInt(cols[5].trim())); getContentResolver().insert( Uri.parse(content://com.example.smartorder.menu/menu_items), values ); } reader.close(); } catch (IOException | NumberFormatException e) { Log.e(MenuLoader, Load menu failed, e); } }4.2 版本控制機(jī)制用SharedPreferences記錄當(dāng)前菜單版本號(hào)在assets/menu.csv同級(jí)放置menu_version.txt內(nèi)容僅為純數(shù)字如20231015。APP啟動(dòng)時(shí)對(duì)比本地版本與assets中版本private boolean isMenuOutdated() { SharedPreferences prefs getSharedPreferences(config, MODE_PRIVATE); String currentVersion prefs.getString(menu_version, 0); try { InputStream is getAssets().open(menu_version.txt); BufferedReader reader new BufferedReader(new InputStreamReader(is)); String latestVersion reader.readLine().trim(); reader.close(); return !currentVersion.equals(latestVersion); } catch (IOException e) { return false; } } private void updateMenu() { // 先清空舊數(shù)據(jù) getContentResolver().delete( Uri.parse(content://com.example.smartorder.menu/menu_items), null, null ); // 再加載新CSV loadMenuFromAssets(); // 更新版本號(hào) SharedPreferences prefs getSharedPreferences(config, MODE_PRIVATE); prefs.edit().putString(menu_version, getCurrentVersionFromAssets()).apply(); }提示此方案規(guī)避了網(wǎng)絡(luò)請(qǐng)求失敗導(dǎo)致的菜單空白風(fēng)險(xiǎn)——即使服務(wù)器宕機(jī)APP仍能加載assets中上一版菜單。運(yùn)營(yíng)人員只需替換APK中的assets文件重新簽名發(fā)布即可完成菜單更新無(wú)需后臺(tái)系統(tǒng)。5. 實(shí)戰(zhàn)排錯(cuò)從Logcat定位5類高頻崩潰與ANR根源5.1 數(shù)據(jù)庫(kù)事務(wù)未關(guān)閉導(dǎo)致的ANRApplication Not Responding現(xiàn)象點(diǎn)擊“提交訂單”后界面卡死10秒彈出ANR對(duì)話框。根因ContentProvider.insert()中開啟了db.beginTransaction()但未調(diào)用db.setTransactionSuccessful()和db.endTransaction()。修復(fù)代碼模板Override public Uri insert(NonNull Uri uri, Nullable ContentValues values) { SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); try { long id db.insert(orders, null, values); if (id -1) throw new SQLException(Failed to insert order); db.setTransactionSuccessful(); // 必須在endTransaction前調(diào)用 return ContentUris.withAppendedId(uri, id); } finally { db.endTransaction(); // 必須在finally中確保執(zhí)行 } }5.2 圖片加載OOMBitmapFactory.Options.inSampleSize計(jì)算失誤現(xiàn)象打開菜單頁(yè)閃退Logcat顯示java.lang.OutOfMemoryError: Failed to allocate a 12582924 byte allocation。根因直接BitmapFactory.decodeFile(path)加載全尺寸圖未按ImageView尺寸縮放。正確做法在ListView.Adapter.getView()中private Bitmap decodeSampledBitmapFromPath(String path, int reqWidth, int reqHeight) { final BitmapFactory.Options options new BitmapFactory.Options(); options.inJustDecodeBounds true; // 先讀取尺寸不分配內(nèi)存 BitmapFactory.decodeFile(path, options); options.inSampleSize calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds false; return BitmapFactory.decodeFile(path, options); } private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height options.outHeight; final int width options.outWidth; int inSampleSize 1; if (height reqHeight || width reqWidth) { final int halfHeight height / 2; final int halfWidth width / 2; while ((halfHeight / inSampleSize) reqHeight (halfWidth / inSampleSize) reqWidth) { inSampleSize * 2; } } return inSampleSize; }5.3 藍(lán)牙連接超時(shí)Socket連接未設(shè)timeout現(xiàn)象點(diǎn)擊“連接打印機(jī)”后無(wú)響應(yīng)Logcat無(wú)錯(cuò)誤日志。根因bluetoothSocket.connect()默認(rèn)無(wú)限等待需顯式設(shè)置// 創(chuàng)建Socket后connect前 bluetoothSocket device.createRfcommSocketToServiceRecord(MY_UUID); // 設(shè)置10秒超時(shí) bluetoothSocket.connect(); // 此處可能阻塞應(yīng)改為Method m device.getClass().getMethod(createRfcommSocket, int.class); bluetoothSocket (BluetoothSocket) m.invoke(device, 1); // 使用反射調(diào)用帶timeout的connectAndroid 10原生支持 if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { bluetoothSocket.connect(10000); // 10秒超時(shí) } else { // 舊版本用線程Handler模擬超時(shí) new Thread(() - { try { bluetoothSocket.connect(); handler.sendEmptyMessage(0); // 連接成功 } catch (IOException e) { handler.sendEmptyMessage(1); // 連接失敗 } }).start(); }5.4 權(quán)限拒絕后功能不可用未檢查Permission Granted狀態(tài)現(xiàn)象Android 12設(shè)備上首次啟動(dòng)點(diǎn)擊掃碼無(wú)反應(yīng)。根因未在onResume()中檢查BLUETOOTH_CONNECT權(quán)限雖不推薦請(qǐng)求但需兼容。檢測(cè)邏輯private boolean isBluetoothPermissionGranted() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { return ActivityCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) PackageManager.PERMISSION_GRANTED; } return true; // Android 11及以下無(wú)需此權(quán)限 }5.5 ContentProvider URI匹配失敗authority拼寫錯(cuò)誤現(xiàn)象getContentResolver().query()返回nullLogcat提示java.lang.IllegalArgumentException: Unknown URL。根因Uri.parse(content://com.example.smartorder.menu/menu_items)中的authority與AndroidManifest.xml中provider android:authorities...不一致。驗(yàn)證命令adb shell dumpsys package com.example.smartorder | grep authorities # 輸出應(yīng)包含authoritiescom.example.smartorder.menu若不一致修改Manifest中android:authorities值并同步更新所有URI字符串。6. 進(jìn)階技巧用ADB命令快速驗(yàn)證系統(tǒng)狀態(tài)與數(shù)據(jù)一致性6.1 一鍵導(dǎo)出SQLite數(shù)據(jù)庫(kù)供離線分析當(dāng)用戶報(bào)告“訂單提交后消失”需確認(rèn)數(shù)據(jù)是否真正寫入。在開發(fā)機(jī)上執(zhí)行# 進(jìn)入設(shè)備shell adb shell # 切換到APP數(shù)據(jù)目錄需root或debuggable APK cd /data/data/com.example.smartorder/databases/ # 將數(shù)據(jù)庫(kù)復(fù)制到sdcard無(wú)需root cp order.db /sdcard/order_debug.db exit # 拉取到本地 adb pull /sdcard/order_debug.db ./order_debug.db然后用DB Browser for SQLite打開order_debug.db執(zhí)行SQL驗(yàn)證-- 檢查最新5條訂單 SELECT order_no, status, created_at, items_json FROM orders ORDER BY _id DESC LIMIT 5; -- 檢查是否存在未提交的草稿訂單 SELECT COUNT(*) FROM orders WHERE status draft; -- 檢查菜品表數(shù)據(jù)完整性 SELECT COUNT(*) FROM menu_items WHERE item_code IS NULL OR name IS NULL;6.2 模擬弱網(wǎng)環(huán)境測(cè)試離線能力用ADB禁用網(wǎng)絡(luò)驗(yàn)證菜單加載與下單流程# 禁用所有網(wǎng)絡(luò) adb shell settings put global airplane_mode_on 1 adb shell am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true # 執(zhí)行點(diǎn)餐操作掃碼、加購(gòu)、提交 # ... 操作完成后恢復(fù)網(wǎng)絡(luò) adb shell settings put global airplane_mode_on 0 adb shell am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false若訂單在恢復(fù)網(wǎng)絡(luò)后自動(dòng)同步本系統(tǒng)不自動(dòng)同步需手動(dòng)觸發(fā)則說(shuō)明設(shè)計(jì)符合離線優(yōu)先原則。6.3 打印指令調(diào)試用串口助手發(fā)送原始字節(jié)當(dāng)熱敏打印機(jī)不響應(yīng)時(shí)跳過(guò)APP直接發(fā)送ESC/POS指令驗(yàn)證硬件# 將打印機(jī)通過(guò)USB轉(zhuǎn)串口連接電腦 # 用Serial Port UtilityWindows或screenmacOS/Linux連接 # 發(fā)送十六進(jìn)制指令1B 40 初始化 echo -ne \x1B\x40 /dev/ttyUSB0 # 發(fā)送1B 61 01 居中 echo -ne \x1B\x61\x01 /dev/ttyUSB0 # 發(fā)送文字UTF-8編碼 echo -e 測(cè)試打印\n /dev/ttyUSB0若硬件正常打印則問(wèn)題在APP的BluetoothSocket數(shù)據(jù)流若不打印則檢查打印機(jī)波特率通常9600、協(xié)議模式SPP及電源狀態(tài)。提示所有ADB命令均需在android.app.Application子類的onCreate()中啟用android:debuggabletrue生產(chǎn)APK發(fā)布前必須設(shè)為false。本文還有配套的精品資源點(diǎn)擊獲取