[FEAT] 新增遠端 update_settings MQTT 指令並收斂系統設定為唯讀;升版 v84

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) <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-12 12:01:50 +08:00
parent 318bfde1fb
commit 5b8a0d7cf6
4 changed files with 89 additions and 68 deletions

View File

@ -10,7 +10,7 @@ plugins {
//
// =========================================================================
def verMinor = 1
def verPatch = 83
def verPatch = 84

View File

@ -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();
}
}
/**

View File

@ -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. 延後重連 MQTTtoken 可能已輪換留時間讓 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");

View File

@ -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());