diff --git a/VMC-上位机_V3.2.1_全部指令(带插图).doc b/VMC-上位机_V3.2.1_全部指令(带插图).doc new file mode 100644 index 0000000..c4aa8a0 Binary files /dev/null and b/VMC-上位机_V3.2.1_全部指令(带插图).doc differ diff --git a/app/src/main/java/com/unibuy/smartdevice/DialogAbstract.java b/app/src/main/java/com/unibuy/smartdevice/DialogAbstract.java index 0a6e236..e6a4906 100644 --- a/app/src/main/java/com/unibuy/smartdevice/DialogAbstract.java +++ b/app/src/main/java/com/unibuy/smartdevice/DialogAbstract.java @@ -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"); } } diff --git a/app/src/main/java/com/unibuy/smartdevice/controller/DevNFCPayController.java b/app/src/main/java/com/unibuy/smartdevice/controller/DevNFCPayController.java index c0d45eb..f70e6a3 100644 --- a/app/src/main/java/com/unibuy/smartdevice/controller/DevNFCPayController.java +++ b/app/src/main/java/com/unibuy/smartdevice/controller/DevNFCPayController.java @@ -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 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 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); } diff --git a/app/src/main/java/com/unibuy/smartdevice/devices/DevXinYuan.java b/app/src/main/java/com/unibuy/smartdevice/devices/DevXinYuan.java index 18d3d7d..7ecb081 100644 --- a/app/src/main/java/com/unibuy/smartdevice/devices/DevXinYuan.java +++ b/app/src/main/java/com/unibuy/smartdevice/devices/DevXinYuan.java @@ -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}), diff --git a/app/src/main/java/com/unibuy/smartdevice/devices/Rs232Dev.java b/app/src/main/java/com/unibuy/smartdevice/devices/Rs232Dev.java index 0a16e70..86e2c09 100644 --- a/app/src/main/java/com/unibuy/smartdevice/devices/Rs232Dev.java +++ b/app/src/main/java/com/unibuy/smartdevice/devices/Rs232Dev.java @@ -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."); + } } // ✅ 新增真正非阻塞讀取方法 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 1a2fcf3..a50a1c5 100644 --- a/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java +++ b/app/src/main/java/com/unibuy/smartdevice/service/MqttService.java @@ -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); diff --git a/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java b/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java index 71cdd04..7314c92 100644 --- a/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java +++ b/app/src/main/java/com/unibuy/smartdevice/structure/mqtt/TransactionFinalizePayload.java @@ -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; } diff --git a/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java b/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java index b118069..77b06e0 100644 --- a/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java +++ b/app/src/main/java/com/unibuy/smartdevice/tools/TransactionFinalizeBuilder.java @@ -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(); diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutAccessCodeDialog.java b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutAccessCodeDialog.java index 85acaaa..a74600f 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutAccessCodeDialog.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutAccessCodeDialog.java @@ -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; diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcCardDialog.java b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcCardDialog.java index f618d47..9aca423 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcCardDialog.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcCardDialog.java @@ -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(); diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcPhoneDialog.java b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcPhoneDialog.java index 85363d4..6c0898b 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcPhoneDialog.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcPhoneDialog.java @@ -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(); diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcRechargeDialog.java b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcRechargeDialog.java index 206ff52..a0cc661 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcRechargeDialog.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/CheckoutNfcRechargeDialog.java @@ -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();