[FIX] 攔截硬體 Enter 鍵防止誤觸並修正遠端出貨交易紀錄

1. 在 FontendActivity 中攔截購物畫面非預期的 Enter 鍵事件,防止讀卡機誤觸員工卡對話框。
2. 關閉 VMC 與 Electric 商品列表圖片的 focusable 屬性,杜絕硬體 Enter 鍵觸發點擊事件。
3. 修正 MqttService 中的遠端出貨邏輯,改用 TransactionFinalizeBuilder 走完整交易結案流程。
4. 確保遠端出貨時正確填充 BuyList 與 ReportFlowInfoData,使後台能產生完整的訂單紀錄。
5. 在 NFC 支付對話框取消時,確保呼叫 nfcPayController.shutdown() 釋放硬體資源。
This commit is contained in:
sky121113 2026-05-11 18:07:12 +08:00
parent e5feac1608
commit f5619e754e
9 changed files with 95 additions and 42 deletions

View File

@ -119,6 +119,8 @@ public class MyApp extends Application {
public ReportDispatchInfoStructure reportDispatchInfoData;
public CreditCardReceiptStructure creditCardReceiptInfoData;
public boolean isShowDialog;
public volatile boolean isNfcPaymentActive; // 用於控制 NFC 支付感應器的活動門禁
public boolean isDetectHost;
public String suAccess;
@ -830,5 +832,14 @@ public class MyApp extends Application {
isDetectHost = detectHost;
}
public boolean isNfcPaymentActive() {
return isNfcPaymentActive;
}
public void setNfcPaymentActive(boolean nfcPaymentActive) {
isNfcPaymentActive = nfcPaymentActive;
}
}

View File

@ -57,6 +57,7 @@ public class DevNFCPayController extends HandlerMainScheduler {
this.payType = payType;
this.totalPrice = totalPrice;
clearOldBuffer();
MyApp.getInstance().setNfcPaymentActive(true);
byte[] sendBuffer = devNFCPay.swipe(payType, totalPrice);
String sendData = new String(sendBuffer);
getLogs().info("電文送出:" + sendData);
@ -113,19 +114,22 @@ public class DevNFCPayController extends HandlerMainScheduler {
@Override
public void shutdown() {
isShuttingDown = true; // 標記關閉
isShuttingDown = true; // 標記正在關閉避免再次回報錯誤
MyApp.getInstance().setNfcPaymentActive(false); // 關閉 NFC 支付感應
super.shutdown();
close(getHandlerMain());
}
@Override
protected void execute(HandlerMain handlerMain) {
if (!MyApp.getInstance().isShowDialog()) {
getLogs().info("⚠️ App 不在對話框狀態,忽略 NFC 感應訊號並停止服務");
if (!MyApp.getInstance().isShowDialog() || !MyApp.getInstance().isNfcPaymentActive()) {
getLogs().info("⚠️ NFC 支付狀態未啟動或對話框已關閉,停止服務");
shutdown();
return;
}
try {
byte[] readBuffer = comPort2.readToMatch(new byte[]{0x03}, 4096);
getLogs().info("readBuffer length:" + readBuffer.length);

View File

@ -17,8 +17,15 @@ import androidx.core.app.NotificationCompat;
import com.unibuy.smartdevice.HomeActivity;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.devices.SlotField;
import com.unibuy.smartdevice.exception.LogsEmptyException;
import com.unibuy.smartdevice.external.mqtt.MqttManager;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.ReportFlowInfoStructure;
import com.unibuy.smartdevice.structure.SlotStructure;
import com.unibuy.smartdevice.structure.mqtt.TransactionFinalizePayload;
import com.unibuy.smartdevice.tools.TransactionFinalizeBuilder;
import java.text.SimpleDateFormat;
import java.util.Collections;
@ -225,51 +232,56 @@ public class MqttService extends Service {
Log.i(TAG, "Remote dispense success for slot: " + finalSlot + " (cmdId: " + cmdId + ")");
mqttManager.publishCommandAck(cmdId, "success", "Product dispensed from slot: " + finalSlot);
// 觸發交易結案流程 (使庫存扣除與訂單紀錄正式化)
// [修正] 使用 TransactionFinalizeBuilder 走完整交易流程確保後台有訂單紀錄
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");
// Step 1: 從記憶體中找到對應貨道的商品資料
SlotStructure slotStructure = null;
for (SlotField slotField : SlotField.values()) {
try {
slotStructure = MyApp.getInstance().getSlotData(slotField.getField(), finalSlot);
break; // 找到就停止
} catch (LogsEmptyException ignored) {
// field 找不到繼續找下一個
}
}
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);
if (slotStructure == null) {
Log.e(TAG, "Remote dispense finalize failed: slot " + finalSlot + " not found in memory");
return;
}
TransactionFinalizePayload.DispenseRecord disp = new TransactionFinalizePayload.DispenseRecord(
String.valueOf(finalSlot),
0, // 此處不帶商品 ID由後端根據貨道對應
0,
0,
1, // 成功
-1, // 庫存由後端遞減
time
// Step 2: 清空並重新設定 BuyList模擬使用者點選此商品
MyApp.getInstance().getBuyList().clear();
MyApp.getInstance().getBuyList().add(
new BuyStructure(slotStructure.getField(), finalSlot, slotStructure.getProduct(), 1)
);
finalize.setDispense(Collections.singletonList(disp));
mqttManager.publishTransactionFinalize(finalize);
Log.i(TAG, "Remote dispense transaction finalized sent (flow_id: " + flowId + ")");
// Step 3: 設定 ReportFlowInfoDatapayment_type=100 代表遠端管理出貨
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
flowInfo.setFlowType(100); // 遠端管理出貨
flowInfo.setTotalPrice(0); // 免費出貨
flowInfo.setGiveChange(0);
flowInfo.setUsePoints(0);
flowInfo.setFlowBarCode("");
flowInfo.setFlowSendInfo(""); // 無支付請求封包
flowInfo.setFlowRequestInfo(""); // 無支付回應封包
flowInfo.setInvoiceInfo("");
// Step 4: Builder 組裝並送出 Finalize Payload
TransactionFinalizeBuilder builder = new TransactionFinalizeBuilder();
int remaining = slotStructure.getCount() - 1;
int productIdInt = 0;
try { productIdInt = Integer.parseInt(slotStructure.getProduct().getProductID()); } catch (Exception ignored) {}
builder.addDispenseRecord(finalSlot, productIdInt, 0, 0, true, remaining);
MqttManager.getInstance().publishTransactionFinalize(builder.build());
Log.i(TAG, "[Remote Dispense] Transaction finalize sent via TransactionFinalizeBuilder, slot=" + finalSlot);
// 清空 BuyList 避免影響後續正常交易
MyApp.getInstance().getBuyList().clear();
} catch (Exception e) {
Log.e(TAG, "Failed to construct/send finalize for remote dispense: " + e.getMessage(), e);
Log.e(TAG, "Failed to send finalize for remote dispense: " + e.getMessage(), e);
}
}

View File

@ -1008,6 +1008,16 @@ public class FontendActivity extends AppCompatActivityAbstract {
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// [修正] 防止讀卡機 (HID) 在購物畫面送出 Enter 導致誤點擊商品
if (!MyApp.getInstance().isShowDialog()) {
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
if (event.getAction() == KeyEvent.ACTION_UP) {
getLogs().info("攔截到購物畫面非預期的 Enter 鍵,已忽略防止誤觸");
}
return true; // 消耗事件
}
}
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (event.getAction() == KeyEvent.ACTION_DOWN && event.isLongPress()) {
Toast.makeText(this, getString(R.string.back_home), Toast.LENGTH_SHORT).show();

View File

@ -101,6 +101,8 @@ public class RecyclerElectricBuyListAdpter extends RecyclerView.Adapter<Recycler
}
if (count > 0 && !isLock) {
holder.binding.imageProductPicture.setFocusable(false);
holder.binding.imageProductPicture.setFocusableInTouchMode(false);
holder.binding.imageProductPicture.setOnClickListener(new ImageProductPictureOnClickListener(position));
} else {
holder.binding.imageProductPicture.setOnClickListener(null);

View File

@ -117,7 +117,9 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
}
if (count > 0 && !isLock) {
holder.binding.imageProductPicture.setOnClickListener(new imageProductPictureOnClickListener(position));
holder.binding.imageProductPicture.setFocusable(false);
holder.binding.imageProductPicture.setFocusableInTouchMode(false);
holder.binding.imageProductPicture.setOnClickListener(new imageProductPictureOnClickListener(position));
} else {
holder.binding.imageProductPicture.setOnClickListener(null);
}

View File

@ -336,10 +336,14 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
}
public void cancelOnThreadCountdown() {
if (nfcPayController != null) {
nfcPayController.shutdown();
}
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText("取消("+countdown+")");
}

View File

@ -339,10 +339,14 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
}
public void cancelOnThreadCountdown() {
if (nfcPayController != null) {
nfcPayController.shutdown();
}
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText("取消("+countdown+")");
}

View File

@ -337,10 +337,14 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
}
public void cancelOnThreadCountdown() {
if (nfcPayController != null) {
nfcPayController.shutdown();
}
MyApp.getInstance().getBuyList().clear();
cancel();
}
public void setButtonCancelText(long countdown) {
binding.buttonCancel.setText("取消("+countdown+")");
}