From 5b8a0d7cf63cdf0153bf015f73a9ad4989848a72 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 12 Jun 2026 12:01:50 +0800 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=E6=96=B0=E5=A2=9E=E9=81=A0=E7=AB=AF?= =?UTF-8?q?=20update=5Fsettings=20MQTT=20=E6=8C=87=E4=BB=A4=E4=B8=A6?= =?UTF-8?q?=E6=94=B6=E6=96=82=E7=B3=BB=E7=B5=B1=E8=A8=AD=E5=AE=9A=E7=82=BA?= =?UTF-8?q?=E5=94=AF=E8=AE=80=EF=BC=9B=E5=8D=87=E7=89=88=20v84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. MqttService 新增 update_settings 指令分支:收到後重抓系統設定(B014), 不串接商品/廣告同步,僅更新設定本身。 2. 忙碌防呆:交易/購物車/對話框進行中(getBusyReason 非空)時拒收並回 failed ACK,後台 RemoteCommand 約 1 分鐘 timeout 後可重發。 3. 重連時序修正:先發成功 ACK,再延遲 1.5 秒重連 MQTT,避免重連把送指令的 連線斷掉導致 ACK 發不出去(涵蓋 token 輪換情境)。 4. StarCloudAPI.downloadSetting 新增可選 reconnectMqtt 參數(saveSettings 同步), 綁定 UI 流程維持 true、update_settings 路徑傳 false 由呼叫端控制重連時機; 無參數多載維持原行為,向下相容。 5. SystemSetActivity 收斂:機台端系統設定全面唯讀,唯一可編輯項為硬體貨道 (彈簧/履帶,僅系統方);移除現金總開關連動,回寫雲端只送 is_spring_slot_*。 6. app/build.gradle verPatch 升至 84。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/build.gradle | 2 +- .../smartdevice/external/StarCloudAPI.java | 21 ++++- .../smartdevice/service/MqttService.java | 45 ++++++++++ .../smartdevice/ui/SystemSetActivity.java | 89 +++++-------------- 4 files changed, 89 insertions(+), 68 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7e95837..d2466ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { // 避免不同尺寸機台間版本號產生衝突。 // ========================================================================= def verMinor = 1 -def verPatch = 83 +def verPatch = 84 diff --git a/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java b/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java index e152c22..1e9884b 100644 --- a/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java +++ b/app/src/main/java/com/unibuy/smartdevice/external/StarCloudAPI.java @@ -199,6 +199,17 @@ public class StarCloudAPI { * 不需要 ApiKey 即可呼叫,僅需機器序號。 */ public void downloadSetting() { + downloadSetting(true); + } + + /** + * B014: 重新下載系統設定。 + * + * @param reconnectMqtt 設定落地後是否立即重連 MQTT。 + * 綁定 UI 流程傳 true;遠端 update_settings 指令傳 false, + * 改由呼叫端在發出 ACK 之後再重連,避免重連把送指令的連線斷掉導致 ACK 發不出去。 + */ + public void downloadSetting(final boolean reconnectMqtt) { final String machineId = MyApp.getInstance().getMachine().getMachineID(); if (machineId.isEmpty()) { Log.e(TAG, "Machine ID is empty, cannot bind."); @@ -260,7 +271,7 @@ public class StarCloudAPI { if (res.optBoolean("success", false)) { JSONObject config = res.optJSONObject("data"); if (config != null) { - saveSettings(config); + saveSettings(config, reconnectMqtt); handlerMain.start("StarCloudAPI", 200, "綁定成功"); } else { handlerMain.start("StarCloudAPI", 500, "設定內容為空,原始資料:" + finalReceive); @@ -977,6 +988,10 @@ public class StarCloudAPI { } private void saveSettings(JSONObject config) { + saveSettings(config, true); + } + + private void saveSettings(JSONObject config, boolean reconnectMqtt) { // 使用 optString 避免欄位缺失導致 JSONException // 1. 儲存 ApiToken 與 序號 @@ -1074,7 +1089,9 @@ public class StarCloudAPI { // 7. 重要:將所有設定同步回資料庫,防止 App 重啟後消失 persistCurrentSettings(); - MyApp.getInstance().initMqtt(); + if (reconnectMqtt) { + MyApp.getInstance().initMqtt(); + } } /** diff --git a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java index 19fa136..7cc8507 100644 --- a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java +++ b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java @@ -470,6 +470,51 @@ public class MqttService extends Service { logs.info("Failed to start remote app update: " + e.getMessage()); mqttManager.publishCommandAck(cmdId, "failed", "Error: " + e.getMessage()); } + } else if ("update_settings".equals(cmd)) { + logs.info("Executing remote settings sync command..."); + + // 🚩 忙碌時拒收:交易/購物車/對話框進行中,避免支付開關等設定中途被改動。 + // 後台 RemoteCommand 約 1 分鐘 timeout,閒置後可重發。 + String busyReason = MyApp.getInstance().getBusyReason(); + if (!busyReason.isEmpty()) { + logs.info("Remote settings sync rejected: " + busyReason); + mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + busyReason); + } else { + // 📢 在主執行緒顯示 Toast 提示使用者 + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> + android.widget.Toast.makeText(getApplicationContext(), + "📢 系統設定更新中...", android.widget.Toast.LENGTH_SHORT).show()); + + com.unibuy.smartdevice.tools.HandlerMain settingHandler = new com.unibuy.smartdevice.tools.HandlerMain( + getApplicationContext(), logs) { + @Override + protected void execute(Context context, int code, String message) { + logs.info("Settings sync event: [" + code + "] " + message); + if (code == 200 && "綁定成功".equals(message)) { + // 1. 先發 ACK,確保在 MQTT 重連之前送達後台 + mqttManager.publishCommandAck(cmdId, "success", + "Settings sync completed successfully"); + + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> + android.widget.Toast.makeText(context, + "✅ 系統設定更新完成", android.widget.Toast.LENGTH_SHORT).show()); + + // 2. 延後重連 MQTT(token 可能已輪換),留時間讓 ACK 先送出 + new android.os.Handler(android.os.Looper.getMainLooper()) + .postDelayed(() -> MyApp.getInstance().initMqtt(), 1500); + } else if (code >= 400) { + mqttManager.publishCommandAck(cmdId, "failed", + "Settings sync failed: " + message); + + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> + android.widget.Toast.makeText(context, + "❌ 系統設定更新失敗: " + message, android.widget.Toast.LENGTH_LONG).show()); + } + } + }; + // 只重抓設定(B014),不串接商品/廣告;reconnectMqtt=false,改由上方 ACK 後再重連 + new com.unibuy.smartdevice.external.StarCloudAPI(settingHandler, settingHandler).downloadSetting(false); + } } else { logs.info("Unknown MQTT command: " + cmd); mqttManager.publishCommandAck(cmdId, "failed", "Unknown command"); diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/SystemSetActivity.java b/app/src/main/java/com/unibuy/smartdevice/ui/SystemSetActivity.java index 19c14d0..72cdf5b 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/SystemSetActivity.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/SystemSetActivity.java @@ -49,7 +49,6 @@ public class SystemSetActivity extends AppCompatActivityAbstract { applyShoppingModeView(); updateCheckBoxes(); - setupMasterChildListeners(); applyRolePermission(); binding.buttonBack.setOnClickListener(v -> finish()); @@ -124,56 +123,47 @@ public class SystemSetActivity extends AppCompatActivityAbstract { binding.checkboxSpringSlot5160.setChecked(MyApp.getInstance().getHardwareSetStructure().isSpringSlot51_60()); } - /** - * 主開關連動:現金總開關關閉時,面額子項反灰(僅系統方模式下生效)。 - * 刷卡機/掃碼子項已歸類為商業政策恆唯讀,不需連動;故只掛現金總開關。 - */ - private void setupMasterChildListeners() { - binding.checkboxDevCash.setOnCheckedChangeListener((b, c) -> refreshChildStates()); - } - - /** 依登入身分決定可否編輯:系統方可編輯機台實體設定並顯示儲存鈕;其餘唯讀。 */ + /** 依登入身分決定可否編輯:系統方僅可編輯硬體貨道並顯示儲存鈕;其餘全唯讀。 */ private void applyRolePermission() { if (isSystem) { - binding.textViewModeHint.setText("系統方:可編輯機台實體設定並儲存回雲端(商業政策項唯讀)"); + binding.textViewModeHint.setText("系統方:僅可編輯硬體貨道並儲存回雲端(其餘系統設定唯讀)"); binding.buttonSave.setVisibility(View.VISIBLE); } else { - binding.textViewModeHint.setText("唯讀檢視(僅系統方可編輯機台實體設定)"); + binding.textViewModeHint.setText("唯讀檢視(僅系統方可編輯硬體貨道)"); binding.buttonSave.setVisibility(View.GONE); } refreshChildStates(); } /** - * 統一刷新所有 CheckBox 的 enabled 狀態,依「商業政策 vs 機台實體」分流: - * - 商業政策項(購物車/發票/各支付方式/取貨模組/來店禮/會員)→ 一律唯讀,真相在雲端後台, - * 即使系統方也不可改(避免單機偏離機隊政策、或與金流合約商戶帳號不一致)。 - * - 機台實體項(刷卡機/掃碼/現金總開關、面額、環溫、硬體貨道)→ 系統方可改並寫回雲端, - * 因為這些只有現場技師最清楚(裝了什麼硬體、收哪些幣別);面額類受現金總開關連動。 + * 統一刷新所有 CheckBox 的 enabled 狀態。機台不可編輯系統設定: + * - 所有系統設定項(購物車/發票/各支付方式/刷卡機/掃碼/現金總開關與面額/取貨模組/ + * 來店禮/會員/環溫)→ 一律唯讀,真相在雲端後台,即使系統方也不可改。 + * - 唯一例外:硬體貨道(彈簧/履帶)→ 僅系統方可改並寫回雲端,因只有現場技師最清楚 + * 各貨道的實際機械型式。 */ private void refreshChildStates() { - // 商業政策項:恆唯讀(與身分、主開關都無關) + // 系統設定項:恆唯讀(與身分、主開關都無關) setEnabled(false, binding.checkboxShoppingCar, binding.checkboxInvoice, - binding.checkboxDevCreditCard, binding.checkboxDevMobilePay, binding.checkboxDevCardPay, - binding.checkboxDevEsunPay, binding.checkboxDevTapPay, binding.checkboxDevLinePay, + binding.checkboxDevNFCPay, binding.checkboxDevCreditCard, binding.checkboxDevMobilePay, + binding.checkboxDevCardPay, + binding.checkboxDevScanPay, binding.checkboxDevEsunPay, binding.checkboxDevTapPay, + binding.checkboxDevLinePay, binding.checkboxDevTapPay30, binding.checkboxDevTapPay31, binding.checkboxDevTapPay32, binding.checkboxDevTapPay33, binding.checkboxDevTapPay34, + binding.checkboxDevCash, + binding.checkboxDevBillF, binding.checkboxDevBillE, binding.checkboxDevBillD, + binding.checkboxDevCoinF, binding.checkboxDevCoinE, binding.checkboxDevCoinD, + binding.checkboxDevCoinC, binding.checkboxPickupModule, binding.checkboxPickupCode, binding.checkboxPassCode, - binding.checkboxWelcomeGift, binding.checkboxMemberSystem); + binding.checkboxWelcomeGift, binding.checkboxMemberSystem, binding.checkboxAmbientTemp); - // 機台實體項:系統方可改(總開關與環溫、貨道恆可改) + // 硬體貨道:唯一可編輯項,僅系統方可改 setEnabled(isSystem, - binding.checkboxDevNFCPay, binding.checkboxDevScanPay, binding.checkboxDevCash, - binding.checkboxAmbientTemp, binding.checkboxSpringSlot110, binding.checkboxSpringSlot1120, binding.checkboxSpringSlot2130, binding.checkboxSpringSlot3140, binding.checkboxSpringSlot4150, binding.checkboxSpringSlot5160); - - // 面額類:實體項,但受現金總開關連動(關閉時反灰) - boolean cashOn = isSystem && binding.checkboxDevCash.isChecked(); - setEnabled(cashOn, binding.checkboxDevBillF, binding.checkboxDevBillE, binding.checkboxDevBillD, - binding.checkboxDevCoinF, binding.checkboxDevCoinE, binding.checkboxDevCoinD, binding.checkboxDevCoinC); } private void setEnabled(boolean enabled, CheckBox... boxes) { @@ -253,44 +243,13 @@ public class SystemSetActivity extends AppCompatActivityAbstract { MyApp.saveSystemSet(getCtx()); } - /** 依 App→後端鍵名對照,組出回寫雲端用的 settings JSON。 */ + /** + * 組出回寫雲端用的 settings JSON。機台僅可編輯硬體貨道,故只送這六個 is_spring_slot_* + * 欄位;B016 後端以 array_key_exists 逐欄位守門,未送的欄位保持原值,避免用本地舊值 + * 覆寫雲端後台可能已更新的其他設定。 + */ private JSONObject buildBackendSettings() throws JSONException { JSONObject s = new JSONObject(); - s.put("shopping_cart_enabled", binding.checkboxShoppingCar.isChecked()); - s.put("tax_invoice_enabled", binding.checkboxInvoice.isChecked()); - - s.put("card_terminal_enabled", binding.checkboxDevNFCPay.isChecked()); - s.put("credit_card_enabled", binding.checkboxDevCreditCard.isChecked()); - s.put("mobile_pay_enabled", binding.checkboxDevMobilePay.isChecked()); - s.put("card_pay_enabled", binding.checkboxDevCardPay.isChecked()); - - s.put("scan_pay_enabled", binding.checkboxDevScanPay.isChecked()); - s.put("scan_pay_esun_enabled", binding.checkboxDevEsunPay.isChecked()); - s.put("scan_pay_tappay_enabled", binding.checkboxDevTapPay.isChecked()); - s.put("scan_pay_linepay_enabled", binding.checkboxDevLinePay.isChecked()); - s.put("tappay_linepay", binding.checkboxDevTapPay30.isChecked()); - s.put("tappay_jkopay", binding.checkboxDevTapPay31.isChecked()); - s.put("tappay_easywallet", binding.checkboxDevTapPay32.isChecked()); - s.put("tappay_pipay", binding.checkboxDevTapPay33.isChecked()); - s.put("tappay_pluspay", binding.checkboxDevTapPay34.isChecked()); - - s.put("cash_module_enabled", binding.checkboxDevCash.isChecked()); - s.put("cash_bill_1000", binding.checkboxDevBillF.isChecked()); - s.put("cash_bill_500", binding.checkboxDevBillE.isChecked()); - s.put("cash_bill_100", binding.checkboxDevBillD.isChecked()); - s.put("cash_coin_50", binding.checkboxDevCoinF.isChecked()); - s.put("cash_coin_10", binding.checkboxDevCoinE.isChecked()); - s.put("cash_coin_5", binding.checkboxDevCoinD.isChecked()); - s.put("cash_coin_1", binding.checkboxDevCoinC.isChecked()); - - s.put("pickup_module_enabled", binding.checkboxPickupModule.isChecked()); - s.put("pickup_code_enabled", binding.checkboxPickupCode.isChecked()); - s.put("pass_code_enabled", binding.checkboxPassCode.isChecked()); - - s.put("welcome_gift_enabled", binding.checkboxWelcomeGift.isChecked()); - s.put("member_system_enabled", binding.checkboxMemberSystem.isChecked()); - s.put("ambient_temp_monitoring_enabled", binding.checkboxAmbientTemp.isChecked()); - s.put("is_spring_slot_1_10", binding.checkboxSpringSlot110.isChecked()); s.put("is_spring_slot_11_20", binding.checkboxSpringSlot1120.isChecked()); s.put("is_spring_slot_21_30", binding.checkboxSpringSlot2130.isChecked());