[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:
parent
318bfde1fb
commit
5b8a0d7cf6
@ -10,7 +10,7 @@ plugins {
|
||||
// 避免不同尺寸機台間版本號產生衝突。
|
||||
// =========================================================================
|
||||
def verMinor = 1
|
||||
def verPatch = 83
|
||||
def verPatch = 84
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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());
|
||||
|
||||
Loading…
Reference in New Issue
Block a user