[REFACTOR] 統一重啟邏輯安全防護與 MQTT 交易結案規格

1. 統一 MQTT 交易結案規格:移除 TransactionFinalizePayload 與 Builder 中的 remote_command_id 欄位,強制使用 code_id。
2. 強化定時重啟安全性:將 HomeActivity 與 FontendActivity 的重啟邏輯統一為「閒置 90 秒 + 無開啟對話框」才執行硬重啟。
3. 統一 12:00 重啟行為:將中午 12 點的軟重啟改為受保護的硬重啟,確保資源清理。
4. 加固遠端指令安全性:MqttService 收到 reboot 或 unlock 指令時,若偵測到有人操作(閒置 < 60 秒或對話框開啟)則回傳失敗 ACK,不執行重啟。
This commit is contained in:
sky121113 2026-05-12 14:57:26 +08:00
parent 98cb59c431
commit ccab0905c6
6 changed files with 61 additions and 66 deletions

View File

@ -119,14 +119,9 @@ public class HomeActivity extends AppCompatActivityAbstract {
if (timeByM.equals("23:45")) {
new NfcCheckout(this).start();
}
if (timeByM.equals("12:00") ) {
getLogs().info("當前時間:" + timeByM + ",符合條件,執行 ReStartAPP...");
new RestartAPP(this).start();
}
if (timeByM.equals("23:59") || timeByM.equals("04:00") || timeByM.equals("08:00")
|| timeByM.equals("16:00") || timeByM.equals("20:00")) {
getLogs().info("當前時間:" + timeByM + ",符合條件,執行 ReStartAPP2...");
|| timeByM.equals("12:00") || timeByM.equals("16:00") || timeByM.equals("20:00")) {
getLogs().info("當前時間:" + timeByM + ",符合條件,啟動 RestartAPP2 (硬重啟等待模式)...");
new RestartAPP2(this).start();
}
@ -330,50 +325,42 @@ public class HomeActivity extends AppCompatActivityAbstract {
}
}
/**
* 硬重啟確認閒置秒數後執行真重啟
* - 只要閒置 >= 90 立刻啟動 HomeActivity killProcess
* - 若有人正在使用閒置 < 90 則等待 5 秒後再重新判斷最多等待 10 分鐘
*/
private class RestartAPP2 extends HandlerMainScheduler {
private int testCount = 0;
public RestartAPP2(HandlerMain handlerMain) {
super(handlerMain);
testCount = 0;
}
@Override
protected HandlerMain setHandlerMain() {
return getSrcHandlerMain();
}
protected HandlerMain setHandlerMain() { return getSrcHandlerMain(); }
@Override
protected void execute(HandlerMain handlerMain) {
for (int testIndex=0; testIndex < 30; testIndex++) {
if (testCount < 3) {
if (!MyApp.getInstance().isShowDialog()) {
getLogs().debug("RestartHost2 testCount:" + testCount);
testCount++;
} else {
testCount = 0;
}
// 最多等待 10 分鐘120 * 5 確保機台有機會重啟
for (int i = 0; i < 120; i++) {
long idle = MyApp.getInstance().getIdleSeconds();
boolean isDialog = MyApp.getInstance().isShowDialog();
try {
Thread.sleep(1000*20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
break;
if (idle >= 90 && !isDialog) {
getLogs().info("RestartAPP2閒置 " + idle + " 秒且無對話框,執行真重啟");
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
ReSetAPP2();
});
return;
}
getLogs().debug("RestartAPP2閒置 " + idle + "Dialog:" + isDialog + ",等待 5 秒後重新判斷 (" + (i+1) + "/120)");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
return;
}
}
if (testCount < 3) {
return;
}
ReSetAPP2();
getLogs().info("RestartAPP2等待超過 10 分鐘仍有人在使用,放棄本次重啟");
}
@Override
protected Class<?> setCls() {
return getClass();
}
protected Class<?> setCls() { return getClass(); }
}
private class RestartHost extends HandlerMainScheduler {

View File

@ -93,9 +93,19 @@ public class MqttService extends Service {
try {
if ("reboot".equals(cmd)) {
logs.info("Executing remote reboot command...");
long idle = MyApp.getInstance().getIdleSeconds();
boolean isDialog = MyApp.getInstance().isShowDialog();
if (idle < 60 || isDialog) {
String reason = isDialog ? "Dialog is open" : "User interacting (idle: " + idle + "s)";
logs.info("Remote reboot rejected: " + reason);
mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + reason);
return;
}
mqttManager.publishCommandAck(cmdId, "success", "Rebooting soon...");
// 🚩 優雅重啟ACK 發出後先 gracefulDisconnect不觸發 LWT killProcess
// 重啟到初始畫面 (HomeActivity)購物去再經 RestartActivity 釋放硬體不会再次 killProcess
new android.os.Handler(android.os.Looper.getMainLooper())
.postDelayed(() -> mqttManager.gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
@ -125,6 +135,17 @@ public class MqttService extends Service {
});
} else if ("unlock".equals(cmd)) {
logs.info("Executing remote unlock command...");
long idle = MyApp.getInstance().getIdleSeconds();
boolean isDialog = MyApp.getInstance().isShowDialog();
if (idle < 60 || isDialog) {
String reason = isDialog ? "Dialog is open" : "User interacting (idle: " + idle + "s)";
logs.info("Remote unlock/reboot rejected: " + reason);
mqttManager.publishCommandAck(cmdId, "failed", "Machine busy: " + reason);
return;
}
com.unibuy.smartdevice.database.SettingsDao settingsDao = new com.unibuy.smartdevice.database.SettingsDao(
getApplicationContext());
settingsDao.setMachineLocked(false);

View File

@ -24,11 +24,6 @@ public class TransactionFinalizePayload {
*/
private String code_id;
/**
* 遠端指令 ID (僅用於 payment_type = 100)
*/
private String remote_command_id;
/**
* 支付類型 (Root 級別用於後端快速判斷)
*/
@ -62,13 +57,6 @@ public class TransactionFinalizePayload {
this.code_id = code_id;
}
public String getRemote_command_id() {
return remote_command_id;
}
public void setRemote_command_id(String remote_command_id) {
this.remote_command_id = remote_command_id;
}
public int getPayment_type() {
return payment_type;

View File

@ -26,16 +26,20 @@ public class FlowIdGenerator {
private static final String KEY_COUNTER = "counter";
private static final SimpleDateFormat DATE_FORMAT =
new SimpleDateFormat("yyyyMMdd", Locale.getDefault());
private static final SimpleDateFormat TIME_FORMAT =
new SimpleDateFormat("HHmmss", Locale.getDefault());
/**
* 取得下一個流水號每次呼叫自動遞增
*
* @param context Android Context
* @return 格式為 "YYYYMMDDXXXX" 的流水號字串例如 "202605110001"
* @return 格式為 "YYYYMMDDHHMMSSXXXX" 的流水號字串
*/
public static synchronized String next(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
String today = DATE_FORMAT.format(new Date());
Date now = new Date();
String today = DATE_FORMAT.format(now);
String timePart = TIME_FORMAT.format(now);
String lastDate = prefs.getString(KEY_LAST_DATE, "");
int counter;
@ -48,13 +52,13 @@ public class FlowIdGenerator {
if (counter > 9999) counter = 1;
}
// 持久化
// 持久化使用 commit() 確保同步寫入磁碟防止異常重啟導致跳號重置
prefs.edit()
.putString(KEY_LAST_DATE, today)
.putInt(KEY_COUNTER, counter)
.apply();
.commit();
// 格式化日期 + 4 位補零序號
return today + String.format(Locale.getDefault(), "%04d", counter);
// 格式化日期 + 時分秒 + 4 位補零序號
return today + timePart + String.format(Locale.getDefault(), "%04d", counter);
}
}

View File

@ -40,7 +40,6 @@ public class TransactionFinalizeBuilder {
private final List<TransactionFinalizePayload.DispenseRecord> dispenseRecords = new ArrayList<>();
private String codeId = "0";
private String remoteCommandId = "0";
public TransactionFinalizeBuilder() {
this.orderMachineTime = SDF.format(new Date());
@ -53,9 +52,6 @@ public class TransactionFinalizeBuilder {
this.codeId = codeId;
}
public void setRemoteCommandId(String remoteCommandId) {
this.remoteCommandId = remoteCommandId;
}
/**
* 加入一筆出貨結果記錄每個貨道出貨後呼叫一次
@ -110,8 +106,6 @@ public class TransactionFinalizeBuilder {
}
}
// --- remote_command_id遠端指令 ID ---
payload.setRemote_command_id(this.remoteCommandId);
// --- payment_type支付類型 (Root 級別) ---
payload.setPayment_type(flowInfo.getFlowType());

View File

@ -749,8 +749,9 @@ public class FontendActivity extends AppCompatActivityAbstract {
// 最多等待 10 分鐘120 * 5 確保機台有機會重啟
for (int i = 0; i < 120; i++) {
long idle = MyApp.getInstance().getIdleSeconds();
if (idle >= 90) {
getLogs().info("RestartAPP2閒置 " + idle + " 秒,符合條件,執行真重啟");
boolean isDialog = MyApp.getInstance().isShowDialog();
if (idle >= 90 && !isDialog) {
getLogs().info("RestartAPP2閒置 " + idle + " 秒且無對話框,執行真重啟");
// 切回主執行緒執行重啟
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);