[FEAT] 整合 MQTT 交易結案邏輯與優化 NFC 生命週期管理

1. 在 MqttService 中實作遠端出貨指令的結案回報 (Transaction Finalize),確保庫存同步。
2. 優化 TransactionFinalizeBuilder,統一使用 FlowIdGenerator 生成 flow_id 與 order_no,確保端到端一致性。
3. 更新 TransactionFinalizePayload 結構,新增 remote_command_id 欄位以支援遠端指令追蹤。
4. 實作 NFC 讀卡生命週期 gated 機制,在 CheckoutNfcCardDialog 等對話框關閉時正確釋放硬體資源。
5. 修復 DevXinYuan 與 Rs232Dev 在異常通訊時的穩定性問題。
This commit is contained in:
sky121113 2026-05-11 17:16:18 +08:00
parent 5579d937e3
commit e5feac1608
12 changed files with 190 additions and 40 deletions

Binary file not shown.

View File

@ -131,12 +131,12 @@ public abstract class DialogAbstract extends AlertDialog {
.newInstance(context, handlerMain);
// 顯示對話框
if (!newDialog.isShow()) {
if (!newDialog.isShowing()) {
newDialog.show();
}
// 取消當前對話框
super.cancel();
this.cancel();
getLogs().debug("dialog is change:" + cls.getSimpleName());
} catch (Exception e) {
@ -177,6 +177,7 @@ public abstract class DialogAbstract extends AlertDialog {
dialogInstances.put(cls, this);
super.show();
MyApp.getInstance().setShowDialog(true);
}
public boolean isShow() {
@ -188,8 +189,11 @@ public abstract class DialogAbstract extends AlertDialog {
@Override
public void dismiss() {
super.dismiss();
// 在關閉對話框時移除記錄
// 在關閉對話框時移除記錄並重置全域狀態
dialogInstances.remove(setCls());
if (dialogInstances.isEmpty()) {
MyApp.getInstance().setShowDialog(false);
}
}
protected abstract void onCreate(Context context);
@ -197,7 +201,7 @@ public abstract class DialogAbstract extends AlertDialog {
@Override
public void cancel() {
super.cancel();
MyApp.getInstance().setShowDialog(false);
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.SHOW_MULTILANGUAGE_BUTTON.getOption(), "hidden button");
// MyApp.getInstance().setShowDialog(false); // 移至 dismiss 統一處理
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.SHOW_MULTILANGUAGE_BUTTON.getOption(), "show button");
}
}

View File

@ -11,6 +11,7 @@ import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
import com.unibuy.smartdevice.tools.HandlerMain;
import com.unibuy.smartdevice.tools.HandlerMainScheduler;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@ -32,6 +33,9 @@ public class DevNFCPayController extends HandlerMainScheduler {
private Rs232Dev comPort2;
private DevNFCPay devNFCPay;
private int payType;
private int totalPrice;
private boolean isShuttingDown = false; // 新增標記是否正在關閉中
public DevNFCPayController(HandlerMain handlerMain) {
super(handlerMain);
@ -50,6 +54,8 @@ public class DevNFCPayController extends HandlerMainScheduler {
public void start(int payType, int totalPrice) throws LogsIOException {
if (devNFCPay != null) {
this.payType = payType;
this.totalPrice = totalPrice;
clearOldBuffer();
byte[] sendBuffer = devNFCPay.swipe(payType, totalPrice);
String sendData = new String(sendBuffer);
@ -105,12 +111,30 @@ public class DevNFCPayController extends HandlerMainScheduler {
1003 ? CreditCard settlement is failure, 信用卡結帳失敗電票結帳成功
*/
@Override
public void shutdown() {
isShuttingDown = true; // 標記關閉
super.shutdown();
close(getHandlerMain());
}
@Override
protected void execute(HandlerMain handlerMain) {
if (!MyApp.getInstance().isShowDialog()) {
getLogs().info("⚠️ App 不在對話框狀態,忽略 NFC 感應訊號並停止服務");
shutdown();
return;
}
try {
byte[] readBuffer = comPort2.readToMatch(new byte[]{0x03}, 4096);
getLogs().info("readBuffer length:" + readBuffer.length);
if (!MyApp.getInstance().isShowDialog()) {
getLogs().info("⚠️ 讀取完成後發現對話框已關閉,捨棄本次交易內容");
return;
}
if (readBuffer.length != 404) {
getLogs().info("❌ 長度不符,立即中止");
// 讀取長度異常直接結束
@ -123,17 +147,30 @@ public class DevNFCPayController extends HandlerMainScheduler {
getLogs().info("電文接收:" + readData);
MyApp.getInstance().getReportFlowInfoData().setFlowRequestInfo(readData);
HashMap<String, String> readMap = devNFCPay.read(readData);
logReadMap(readMap);
while (isRun() && MyApp.getInstance().isShowDialog()) { // 增加狀態檢查
byte[] buffer = comPort2.readListen(30, 200);
String code = readMap.get("code");
if ("0003".equals(code)) {
// 這裡仍可選擇是否要重試依實務需求來看
getLogs().info("操作錯誤 code:0003不重試直接回報錯誤");
reportError("0003", "操作錯誤");
} else {
getLogs().info("交易完成,回傳代碼: " + code);
getHandlerMain().start(getClass().getSimpleName(), Option.RESPONSE.getOption(), code);
if (buffer.length > 0) {
getLogs().info("NFC 收到資料: " + PortTools.showHex(buffer));
String rxData = new String(buffer, StandardCharsets.UTF_8);
if (rxData.contains(String.valueOf((char) 0x03))) { // ETX
HashMap<String, String> response = devNFCPay.read(rxData);
String code = response.get("code");
if (isRun() && !isShuttingDown && MyApp.getInstance().isShowDialog()) { // 最後回報前的檢查
getHandlerMain().start(getClass().getSimpleName(), Option.RESPONSE.getOption(), code);
} else {
getLogs().info("🚫 控制器已停止或對話框已關閉,捨棄 NFC 回傳結果");
}
break;
}
}
if (!isRun() || isShuttingDown || !MyApp.getInstance().isShowDialog()) {
getLogs().info("NFC 讀取中斷: 控制器關閉或對話框消失");
break;
}
}
} catch (LogsParseException | LogsIOException e) {
getLogs().warning(e);
@ -142,6 +179,7 @@ public class DevNFCPayController extends HandlerMainScheduler {
}
private void reportError(String code, String logMessage) {
if (isShuttingDown) return; // 關閉中不報錯
getLogs().info("錯誤回報 code: " + code + " 描述: " + logMessage);
getHandlerMain().start(getClass().getSimpleName(), Option.RESPONSE.getOption(), code);
}

View File

@ -100,7 +100,7 @@ public class DevXinYuan {
SLOT_STATUS_02_PDT_EMPTY(new byte[]{0x02}),
SLOT_STATUS_03_SLOT_EMPTY(new byte[]{0x03}),
SLOT_STATUS_04_SLOT_TMP_USE(new byte[]{0x04}),
SLOT_STATUS_05(new byte[]{0x05}),
SLOT_STATUS_05_HAS_PRODUCT(new byte[]{0x05}),
SLOT_STATUS_06_NOT_CLOSE(new byte[]{0x06}),
SLOT_STATUS_07_ERROR(new byte[]{0x07}),
SLOT_STATUS_08_ERROR(new byte[]{0x08}),
@ -146,7 +146,7 @@ public class DevXinYuan {
BUY_STATUS_02_OK(new byte[]{0x02}),
BUY_STATUS_03_K_PDT(new byte[]{0x03}),
BUY_STATUS_04_NOT_STOP(new byte[]{0x04}),
BUY_STATUS_05(new byte[]{0x05}),
BUY_STATUS_05_HAS_PRODUCT(new byte[]{0x05}),
BUY_STATUS_06_NOT_FOUND(new byte[]{0x06}),
BUY_STATUS_07_ERROR(new byte[]{0x07}),
BUY_STATUS_08_ERROR(new byte[]{0x08}),

View File

@ -304,24 +304,24 @@ public class Rs232Dev {
}
public void closePort() throws LogsIOException {
if (this.serialPort != null) {
// OutputStream outputStream = this.serialPort.getOutputStream();
// if (outputStream != null) {
// outputStream.close();
// outputStream = null;
// }
//
// InputStream inputStream = this.serialPort.getInputStream();
// if (inputStream != null) {
// inputStream.close();
// inputStream = null;
// }
SerialPort serialPort = this.serialPort;
if (serialPort != null) {
serialPort.close();
if (this.serialPort != null) {
try {
if (inputStream != null) {
inputStream.close();
inputStream = null;
}
if (outputStream != null) {
outputStream.close();
outputStream = null;
}
} catch (IOException e) {
// 忽略流關閉異常繼續嘗試關閉 serialPort
}
this.serialPort.close();
this.serialPort = null;
Log.i("Rs232Dev", "Serial port closed successfully.");
}
}
// 新增真正非阻塞讀取方法

View File

@ -18,6 +18,14 @@ import androidx.core.app.NotificationCompat;
import com.unibuy.smartdevice.HomeActivity;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.external.mqtt.MqttManager;
import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Locale;
import java.util.UUID;
/**
* Foreground Service負責持有 WakeLock 並管理 MQTT 連線
@ -214,10 +222,58 @@ public class MqttService extends Service {
@Override
public void onSuccess() {
Log.i(TAG, "Remote dispense success for slot: " + finalSlot);
Log.i(TAG, "Remote dispense success for slot: " + finalSlot + " (cmdId: " + cmdId + ")");
mqttManager.publishCommandAck(cmdId, "success", "Product dispensed from slot: " + finalSlot);
// 觸發交易結案流程 (使庫存扣除與訂單紀錄正式化)
try {
Log.i(TAG, "Preparing to send Transaction Finalize for remote dispense...");
String flowId = com.unibuy.smartdevice.tools.FlowIdGenerator.next(getApplicationContext());
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.TAIWAN).format(new Date());
TransactionFinalizePayload finalize = new TransactionFinalizePayload();
finalize.setFlow_id(flowId);
finalize.setPayment_type(100); // 遠端管理出貨
// 嘗試解析 cmdId 為數字若為非數字例如 UUID則填 0
int numericCmdId = 0;
try {
if (cmdId != null) numericCmdId = Integer.parseInt(cmdId);
} catch (NumberFormatException e) {
Log.w(TAG, "cmdId is not a number: " + cmdId + ", using 0 for code_id");
}
finalize.setCode_id(numericCmdId);
finalize.setRemote_command_id(numericCmdId);
TransactionFinalizePayload.OrderPayload order = new TransactionFinalizePayload.OrderPayload();
order.setOrder_no(flowId);
order.setPayment_type(100);
order.setPayment_status(1);
order.setMachine_time(time);
order.setTotal_amount(0);
order.setPay_amount(0);
finalize.setOrder(order);
TransactionFinalizePayload.DispenseRecord disp = new TransactionFinalizePayload.DispenseRecord(
String.valueOf(finalSlot),
0, // 此處不帶商品 ID由後端根據貨道對應
0,
0,
1, // 成功
-1, // 庫存由後端遞減
time
);
finalize.setDispense(Collections.singletonList(disp));
mqttManager.publishTransactionFinalize(finalize);
Log.i(TAG, "Remote dispense transaction finalized sent (flow_id: " + flowId + ")");
} catch (Exception e) {
Log.e(TAG, "Failed to construct/send finalize for remote dispense: " + e.getMessage(), e);
}
}
@Override
public void onFailure(String error) {
Log.e(TAG, "Remote dispense failed: " + error);

View File

@ -24,6 +24,16 @@ public class TransactionFinalizePayload {
*/
private int code_id;
/**
* 遠端指令 ID (僅用於 payment_type = 100)
*/
private int remote_command_id;
/**
* 支付類型 (Root 級別用於後端快速判斷)
*/
private int payment_type;
/** 訂單資訊 */
private OrderPayload order;
@ -40,6 +50,12 @@ public class TransactionFinalizePayload {
public int getCode_id() { return code_id; }
public void setCode_id(int code_id) { this.code_id = code_id; }
public int getRemote_command_id() { return remote_command_id; }
public void setRemote_command_id(int remote_command_id) { this.remote_command_id = remote_command_id; }
public int getPayment_type() { return payment_type; }
public void setPayment_type(int payment_type) { this.payment_type = payment_type; }
public OrderPayload getOrder() { return order; }
public void setOrder(OrderPayload order) { this.order = order; }

View File

@ -97,6 +97,9 @@ public class TransactionFinalizeBuilder {
payload.setCode_id(0);
}
// --- payment_type支付類型 (Root 級別) ---
payload.setPayment_type(flowInfo.getFlowType());
// --- 組裝 order ---
TransactionFinalizePayload.OrderPayload order = buildOrderPayload(flowInfo);
payload.setOrder(order);
@ -117,7 +120,7 @@ public class TransactionFinalizeBuilder {
private TransactionFinalizePayload.OrderPayload buildOrderPayload(ReportFlowInfoStructure flowInfo) {
TransactionFinalizePayload.OrderPayload order = new TransactionFinalizePayload.OrderPayload();
order.setOrder_no(flowInfo.getOrderId());
order.setOrder_no(flowId);
// 金額資訊
int totalAmount = flowInfo.getTotalPrice();

View File

@ -5,7 +5,7 @@ import android.content.Context;
import com.unibuy.smartdevice.DialogAbstract;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.controller.DevNFCPayController;
import com.unibuy.smartdevice.databinding.DialogAccessCodeBinding;
import com.unibuy.smartdevice.databinding.DialogCustomerIdentifierBinding;
import com.unibuy.smartdevice.devices.DeviceType;

View File

@ -181,6 +181,7 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
private DialogCheckoutNfcCardBinding binding;
private CloseDialogOnThreadHandler closeDialogCountdown;
private DevNFCPayController nfcPayController;
private boolean isResponse;
private boolean isChangeDispatch;
private HttpAPI httpAPI;
@ -251,7 +252,8 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
MyApp.getInstance().getReportFlowInfoData().setShoppingCartInfoByJsonArray(jsonArray);
try {
new DevNFCPayController(getHandlerMain()).start(1, getTotalPrice());
nfcPayController = new DevNFCPayController(getHandlerMain());
nfcPayController.start(1, getTotalPrice());
} catch (LogsIOException e) {
getLogs().warning(e);
binding.textStatus.setText("刷卡機無法開啟");
@ -304,12 +306,18 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
public void changeDispatch() {
if (isChangeDispatch) {
isChangeDispatch = false;
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
changeDialog(DispatchDialog.class);
}
}
public void changeBuy() {
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
changeDialog(BuyDialog.class);
}
@ -318,6 +326,9 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
if (this.isShowing()) {
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
MyApp.getInstance().getBuyList().clear();
cancel();

View File

@ -180,6 +180,7 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
private DialogCheckoutNfcPhoneBinding binding;
private CloseDialogOnThreadHandler closeDialogCountdown;
private DevNFCPayController nfcPayController;
private boolean isResponse;
private boolean isChangeDispatch;
private HttpAPI httpAPI;
@ -254,7 +255,8 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
MyApp.getInstance().getReportFlowInfoData().setShoppingCartInfoByJsonArray(jsonArray);
try {
new DevNFCPayController(getHandlerMain()).start(4, getTotalPrice());
nfcPayController = new DevNFCPayController(getHandlerMain());
nfcPayController.start(4, getTotalPrice());
} catch (LogsIOException e) {
getLogs().warning(e);
binding.textStatus.setText("刷卡機無法開啟");
@ -307,12 +309,18 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
public void changeDispatch() {
if (isChangeDispatch) {
isChangeDispatch = false;
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
changeDialog(DispatchDialog.class);
}
}
public void changeBuy() {
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
changeDialog(BuyDialog.class);
}
@ -321,6 +329,9 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
if (this.isShowing()) {
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
MyApp.getInstance().getBuyList().clear();
cancel();

View File

@ -180,6 +180,7 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
private DialogCheckoutNfcRechargeBinding binding;
private CloseDialogOnThreadHandler closeDialogCountdown;
private DevNFCPayController nfcPayController;
private boolean isResponse;
private boolean isChangeDispatch;
private HttpAPI httpAPI;
@ -252,7 +253,8 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
MyApp.getInstance().getReportFlowInfoData().setShoppingCartInfoByJsonArray(jsonArray);
try {
new DevNFCPayController(getHandlerMain()).start(2, getTotalPrice());
nfcPayController = new DevNFCPayController(getHandlerMain());
nfcPayController.start(2, getTotalPrice());
} catch (LogsIOException e) {
getLogs().warning(e);
binding.textStatus.setText("刷卡機無法開啟");
@ -304,13 +306,19 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
public void changeDispatch() {
if (isChangeDispatch) {
isChangeDispatch = true;
isChangeDispatch = false;
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
changeDialog(DispatchDialog.class);
}
}
public void changeBuy() {
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
changeDialog(BuyDialog.class);
}
@ -319,6 +327,9 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
if (this.isShowing()) {
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "");
if (nfcPayController != null) {
nfcPayController.shutdown();
}
closeDialogCountdown.shutdown();
MyApp.getInstance().getBuyList().clear();
cancel();