Merge remote-tracking branch 'origin/feat/general-cart-and-payment' into ui-design

This commit is contained in:
TaiwanStar Developer 2026-06-16 09:53:26 +08:00
commit f700881788
12 changed files with 162 additions and 31 deletions

View File

@ -10,7 +10,7 @@ plugins {
//
// =========================================================================
def verMinor = 1
def verPatch = 91
def verPatch = 93

View File

@ -151,30 +151,17 @@ public class DevNFCPayController extends HandlerMainScheduler {
getLogs().info("電文接收:" + readData);
MyApp.getInstance().getReportFlowInfoData().setFlowRequestInfo(readData);
while (isRun() && MyApp.getInstance().isShowDialog()) { // 增加狀態檢查
byte[] buffer = comPort2.readListen(30, 200);
// 修復 付款成功卻不出貨交易結果 responseCode就在這段 404 byte 電文裡
// 直接解析本段即可先前版本誤改成再用 readListen 第二段電文才解析
// 但刷卡機只送這一段第二段永遠等不到 倒數逾時 RESPONSE 從未送出 不出貨
HashMap<String, String> readMap = devNFCPay.read(readData);
String code = readMap.get("code");
getLogs().info("交易完成,回傳代碼: " + code);
if (buffer.length > 0) {
getLogs().debug("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;
}
if (isRun() && !isShuttingDown && MyApp.getInstance().isShowDialog()) { // 回報前的生命週期檢查
getHandlerMain().start(getClass().getSimpleName(), Option.RESPONSE.getOption(), code);
} else {
getLogs().info("🚫 控制器已停止或對話框已關閉,捨棄 NFC 回傳結果");
}
} catch (LogsParseException | LogsIOException e) {
getLogs().warning(e);

View File

@ -15,6 +15,9 @@ public class ReportFlowInfoStructure {
int giveChange;
int usePoints;
String reportFlowId;
// MQTT 交易結案流水號進入付款時生成一次pending completed/failed 全程共用同一個
// 讓後台依同一 flow_id 做狀態轉移 reportFlowId取貨碼/通行碼驗證碼B600 legacy語意不同獨立一欄
String mqttFlowId;
JSONArray shoppingCartInfoByJsonArray;
public ReportFlowInfoStructure(String orderId, int flowType, String flowSendInfo, String flowRequestInfo, String productId, int totalPrice, String invoiceInfo, String flowBarCode, int flowStatusType, int giveChange, int usePoints, JSONArray shoppingCartInfoByJsonArray) {
@ -136,6 +139,14 @@ public class ReportFlowInfoStructure {
this.reportFlowId = reportFlowId;
}
public String getMqttFlowId() {
return mqttFlowId;
}
public void setMqttFlowId(String mqttFlowId) {
this.mqttFlowId = mqttFlowId;
}
@Override
public String toString() {
return "ReportFlowInfoStructure{" +

View File

@ -191,10 +191,17 @@ public class TransactionFinalizePayload {
private int payment_type;
/**
* 支付狀態1 = 成功
* 支付狀態1 = 成功, 0 = 失敗/未付款
*/
private int payment_status;
/**
* 交易生命週期狀態後台 Order.status
* "pending"進付款待結果/ "completed"付款成功/ "failed"付款失敗/ "abandoned"
* 不帶null時後台預設 "completed"確保線上 main 機台行為不變
*/
private String status;
/**
* 訂單出貨狀態0 = 全部失敗, 1 = 全部成功, 2 = 部分失敗
*/
@ -291,6 +298,14 @@ public class TransactionFinalizePayload {
this.payment_status = payment_status;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public int getDelivery_status() {
return delivery_status;
}

View File

@ -4,6 +4,7 @@ import android.content.Context;
import android.util.Log;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.external.mqtt.MqttManager;
import com.unibuy.smartdevice.structure.BuyStructure;
import com.unibuy.smartdevice.structure.InvoiceStructure;
import com.unibuy.smartdevice.structure.ReportFlowInfoStructure;
@ -48,14 +49,45 @@ public class TransactionFinalizeBuilder {
private String codeId = "0";
/** 交易生命週期狀態pending/completed/failed/abandoned預設 completed 維持成功路徑原行為。 */
private String status = "completed";
/** 支付狀態1=成功、0=失敗/未付款;預設 1 維持成功路徑原行為。 */
private int paymentStatus = 1;
/** 失敗結果碼(如刷卡機 0015非空時折進 payment_response 方便後台查閱。 */
private String resultCode = null;
public TransactionFinalizeBuilder() {
this.orderMachineTime = SDF.format(new Date());
// 在進入出貨流程時立即生成本次交易流水號一次交易只生成一次
this.flowId = FlowIdGenerator.next(MyApp.getInstance());
// flow_id 共用進入付款時生成的 mqttFlowId pending completed/failed 為同一筆
// 後台才能依 flow_id 做狀態轉移若尚未生成防禦則即時生成並回存 flowInfo
ReportFlowInfoStructure flowInfo = MyApp.getInstance().getReportFlowInfoData();
String existing = flowInfo != null ? flowInfo.getMqttFlowId() : null;
if (existing != null && !existing.isEmpty()) {
this.flowId = existing;
} else {
this.flowId = FlowIdGenerator.next(MyApp.getInstance());
if (flowInfo != null) {
flowInfo.setMqttFlowId(this.flowId);
}
}
this.orderNo = this.flowId;
Log.i(TAG, "TransactionFinalizeBuilder created, flow_id=" + this.flowId);
}
public void setStatus(String status) {
this.status = status;
}
public void setPaymentStatus(int paymentStatus) {
this.paymentStatus = paymentStatus;
}
public void setResultCode(String resultCode) {
this.resultCode = resultCode;
}
public void setFlowId(String flowId) {
this.flowId = flowId;
}
@ -72,6 +104,52 @@ public class TransactionFinalizeBuilder {
this.codeId = codeId;
}
// =========================================================================
// 交易生命週期上報pending / failed只在 basic 購物模式本專案 general/demo才送
// - basic進付款時生成的同一 flow_id上報後台依 flow_id pending completed/failed 轉移
// - employee_card(晟崴)/pickup_sheet(中國醫)無實際付款直接 return 不送後台資料不受影響
// shopping_mode 判斷雲端 B014 已逐機台下發不另設旗標
// 成功(completed)仍由 DispatchDialog 出貨完成後的既有流程上報不在此處
// =========================================================================
/** 僅 basic 購物模式才上報生命週期;其餘模式不送。 */
private static boolean lifecycleLoggingEnabled() {
return "basic".equals(MyApp.getInstance().getShoppingMode());
}
/** 進入付款時上報 pending進付款一律留底。非 basic 模式則不送。 */
public static void publishPending() {
if (!lifecycleLoggingEnabled()) {
return;
}
try {
TransactionFinalizeBuilder b = new TransactionFinalizeBuilder();
b.setStatus("pending");
b.setPaymentStatus(0);
MqttManager.getInstance().publishTransactionFinalize(b.build());
Log.i(TAG, "published pending finalize, flow_id=" + b.getFlowId());
} catch (Exception e) {
Log.w(TAG, "publishPending failed: " + e.getMessage());
}
}
/** 付款失敗/逾時時上報 failed救回原本默默消失的失敗單。非 basic 模式則不送。 */
public static void publishFailed(String resultCode) {
if (!lifecycleLoggingEnabled()) {
return;
}
try {
TransactionFinalizeBuilder b = new TransactionFinalizeBuilder();
b.setStatus("failed");
b.setPaymentStatus(0);
b.setResultCode(resultCode);
MqttManager.getInstance().publishTransactionFinalize(b.build());
Log.i(TAG, "published failed finalize code=" + resultCode + ", flow_id=" + b.getFlowId());
} catch (Exception e) {
Log.w(TAG, "publishFailed failed: " + e.getMessage());
}
}
/**
* 加入一筆出貨結果記錄每個貨道出貨後呼叫一次
@ -141,7 +219,9 @@ public class TransactionFinalizeBuilder {
// --- 電子發票改由後台開立---
// 機台不開票只把開立輸入載具/愛心碼/統編/金額帶上去狀態固定 pending
// 後台建 pending 發票後派 Job 去綠界開立無發票需求invoiceInfo 為空時不帶後台不建立發票
if (flowInfo.getInvoiceInfo() != null && !flowInfo.getInvoiceInfo().isEmpty()) {
// 只有付款成功(completed)才建立發票pending/failed 不開票避免對未成交的交易開立
if ("completed".equals(status)
&& flowInfo.getInvoiceInfo() != null && !flowInfo.getInvoiceInfo().isEmpty()) {
TransactionFinalizePayload.InvoicePayload invoicePayload =
new TransactionFinalizePayload.InvoicePayload();
invoicePayload.setStatus("pending");
@ -204,13 +284,18 @@ public class TransactionFinalizeBuilder {
// 支付類型flowType 直接對應 payment_type
order.setPayment_type(flowInfo.getFlowType());
order.setPayment_status(1); // 走到出貨流程代表支付已成功
// 生命週期狀態與支付狀態成功路徑沿用預設(completed/1)pending/failed setter 覆寫
order.setStatus(status);
order.setPayment_status(paymentStatus);
// 支付流程原始資料
order.setPayment_request(
flowInfo.getFlowSendInfo() != null ? flowInfo.getFlowSendInfo() : "");
order.setPayment_response(
flowInfo.getFlowRequestInfo() != null ? flowInfo.getFlowRequestInfo() : "");
String paymentResponse = flowInfo.getFlowRequestInfo() != null ? flowInfo.getFlowRequestInfo() : "";
if (resultCode != null && !resultCode.isEmpty()) {
paymentResponse = paymentResponse + " | code=" + resultCode;
}
order.setPayment_response(paymentResponse);
// 會員資訊
order.setMember_barcode(

View File

@ -428,6 +428,21 @@ public class BuyDialog extends DialogAbstract {
dialogCancel();
return;
}
// 進入付款先清掉上一筆殘留的 MQTT 交易流水號避免被本筆誤用flow_id 撞單
com.unibuy.smartdevice.structure.ReportFlowInfoStructure flowInfo =
MyApp.getInstance().getReportFlowInfoData();
flowInfo.setMqttFlowId("");
// basic 購物模式(本專案 general/demo) 且為實際付款方式(1/2/3/4/9/30/70)
// 為本筆生成新流水號並上報 pending進付款一律留底pending completed/failed 全程共用此 flow_id
// 通行碼(5)/來店禮(7) 非付款不在此處理employee_card(晟崴)/pickup_sheet(中國醫) 無付款不送後台資料不受影響
boolean realPayment = (flowType == 1 || flowType == 2 || flowType == 3 || flowType == 4
|| flowType == 9 || flowType == 30 || flowType == 70);
if ("basic".equals(MyApp.getInstance().getShoppingMode()) && realPayment) {
flowInfo.setMqttFlowId(com.unibuy.smartdevice.tools.FlowIdGenerator.next(MyApp.getInstance()));
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishPending();
}
switch (flowType) {
case 1:
changeDialog(CheckoutNfcCardDialog.class);

View File

@ -111,6 +111,8 @@ public class CheckoutEsunPayDialog extends DialogAbstract {
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
// [交易生命週期] 掃碼支付失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
isTransaction = false;
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();

View File

@ -112,6 +112,8 @@ public class CheckoutLinePayDialog extends DialogAbstract {
if (isTransaction) {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
// [交易生命週期] LINE Pay 失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
isTransaction = false;
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();

View File

@ -116,10 +116,14 @@ public class CheckoutNfcCardDialog extends DialogAbstract {
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回非 0000上報 failed(帶代碼)救回原本默默消失的失敗單flag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -118,10 +118,14 @@ public class CheckoutNfcPhoneDialog extends DialogAbstract {
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回非 0000上報 failed(帶代碼)救回原本默默消失的失敗單flag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -115,10 +115,14 @@ public class CheckoutNfcRechargeDialog extends DialogAbstract {
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回非 0000上報 failed(帶代碼)救回原本默默消失的失敗單flag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
} else {
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
binding.textStatus.setText(DevNFCPay.recordMessage(message));
// [交易生命週期] 刷卡機回空電文視為失敗上報 failedflag 開才送
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed(message);
}
// binding.textStatus.setText(DevNFCPay.recordMessage(message));

View File

@ -134,6 +134,8 @@ public class CheckoutTapPayDialog extends DialogAbstract {
isTransaction = false; // 🚨 馬上關閉
MyApp.getInstance().getReportFlowInfoData().setFlowStatusType(0);
// [MQTT Finalize 重構] 交易失敗不再經 B600直接提示並返回購買頁不進出貨
// [交易生命週期] 掃碼支付失敗上報 failedflag 開才送救回原本默默消失的失敗單
com.unibuy.smartdevice.tools.TransactionFinalizeBuilder.publishFailed("scan_failed");
start(getSrcClassName(), "@" + getContext().getString(R.string.transaction_failure));
changeBuy();
}