[FIX] 優化出貨流程 UI 體驗與修復雙重倒數 Bug

1. 修復出貨結案時,因 VMC 重複回傳訊號導致 15 秒倒數重置的 Bug。
2. 實作 isFinalizing 狀態鎖,確保結案流程不被舊有的定時器或重複訊號干擾。
3. 優化出貨流程 UI:初始狀態顯示「準備出貨」,取代原先錯誤的提示文字。
4. 調整「取貨確認」按鈕邏輯:出貨中隱藏按鈕,僅在 15 秒取貨倒數時顯示,並支援使用者提前確認取貨。
5. 修正「員工卡」錯字並同步多語系(英、日)字串資源。
6. 在 MqttService 與 FontendActivity 實作 isMachineBusy() 檢查,防止交易過程中觸發遠端重啟。
7. 優化 VMC 偵測邏輯,加入重複訊號過濾防護。
This commit is contained in:
sky121113 2026-05-13 09:33:32 +08:00
parent 93a9ad7e2d
commit 028cac10b5
9 changed files with 135 additions and 15 deletions

View File

@ -17,33 +17,35 @@ cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
```
## 步驟二:導出 APK 到 outputs 資料夾
將編譯出來的深層 APK 複製到專案最外層的 `outputs` 資料夾,並統一命名為 `TaiwanStar_debug.apk`
將編譯出來的 APK 複製到專案最外層的 `outputs` 資料夾。
```bash
// turbo
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
mkdir -p outputs
cp app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08_2_R-debug.apk outputs/TaiwanStar_debug.apk
echo "✅ APK 已經複製到專案根目錄的 outputs 資料夾中"
APK_SOURCE="app/build/outputs/apk/S_12_XY_U_Standard_/debug/app-S_12_XY_U_Standard_-10_08_2_R-debug.apk"
APK_DEST="outputs/TaiwanStar_debug.apk"
cp $APK_SOURCE $APK_DEST
echo "✅ APK 已經複製到 outputs 資料夾,路徑為:$APK_DEST"
```
## 步驟三:安裝並啟動 App (如果有連接設備)
利用 WSL 串接 Windows 端的 ADB偵測到設備後才執行安裝與啟動
## 步驟三:安裝並啟動 App (如有連接設備)
偵測到設備後才執行安裝。如果未偵測到,則保留 APK 檔案並結束工作流
```bash
// turbo
ADB_PATH="/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe"
APK_PATH="/home/mama/projects/TaiwanStar_general_size_s_Chengwai/outputs/TaiwanStar_debug.apk"
# 檢查是否有設備連接
DEVICE_COUNT=$($ADB_PATH devices | grep -v "List of devices" | grep "device" | wc -l)
if [ "$DEVICE_COUNT" -gt 0 ]; then
echo "📱 偵測到 $DEVICE_COUNT 個設備,開始安裝..."
$ADB_PATH install -r -d -t $APK_PATH
$ADB_PATH install -r -d -t "/home/mama/projects/TaiwanStar_general_size_s_Chengwai/outputs/TaiwanStar_debug.apk"
echo "🚀 安裝完成,正在啟動 App..."
$ADB_PATH shell am start -n com.unibuy.smartdevice/com.unibuy.smartdevice.HomeActivity
else
echo "⚠️ 未偵測到連接的手機或設備,跳過安裝與啟動步驟。"
echo "⚠️ 未偵測到連接的手機或設備,已保留 APK 檔案,跳過安裝步驟。"
fi
```

View File

@ -296,6 +296,11 @@ public class MqttService extends Service {
.publishCommandAck(new com.unibuy.smartdevice.structure.mqtt.CommandAckPayload(
cmdId, "success", "Inventory updated successfully",
String.valueOf(stock)));
// 通知前台銷售平台刷新商品列表輕量級不重建 Fragment
Intent refreshIntent = new Intent("com.unibuy.smartdevice.ACTION_SLOT_UPDATED");
refreshIntent.setPackage(getPackageName());
sendBroadcast(refreshIntent);
} else {
throw new Exception("Slot not found: " + slotNo);
}

View File

@ -5,8 +5,10 @@ import static android.view.View.VISIBLE;
import android.Manifest;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
@ -128,6 +130,17 @@ public class FontendActivity extends AppCompatActivityAbstract {
private Handler idleHandler = new Handler();
private Runnable idleRunnable;
// 遠端庫存更新監聽器輕量級只刷新 Adapter 不重建 Fragment
private final BroadcastReceiver slotUpdateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
getLogs().info("收到遠端庫存更新通知,刷新銷售平台商品列表...");
if (vmcFragment instanceof VmcFragment) {
((VmcFragment) vmcFragment).updateFlowerBuyList();
}
}
};
private static VideoEoxPlayer videoEoxPlayer;
@Override
@ -1043,6 +1056,13 @@ public class FontendActivity extends AppCompatActivityAbstract {
protected void onResume() {
super.onResume();
resetIdleTimer();
// 註冊庫存更新監聽
IntentFilter filter = new IntentFilter("com.unibuy.smartdevice.ACTION_SLOT_UPDATED");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(slotUpdateReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
} else {
registerReceiver(slotUpdateReceiver, filter);
}
}
@Override
@ -1051,6 +1071,12 @@ public class FontendActivity extends AppCompatActivityAbstract {
if (idleHandler != null) {
idleHandler.removeCallbacks(idleRunnable);
}
// 取消註冊庫存更新監聽
try {
unregisterReceiver(slotUpdateReceiver);
} catch (Exception e) {
getLogs().info("unregisterReceiver slotUpdateReceiver: " + e.getMessage());
}
}
@Override

View File

@ -150,6 +150,21 @@ public class RecyclerVmcBuyListAdpter extends RecyclerView.Adapter<RecyclerVmcBu
if (slotProduct.isLock()) return;
if (MyApp.getInstance().isShowDialog()) return;
// 即時庫存檢查防止 MQTT 更新後 UI 尚未刷新的時間差
try {
SlotStructure liveSlot = MyApp.getInstance().getSlotData(slotProduct.getField(), slotProduct.getSlot());
if (liveSlot != null && liveSlot.getCount() <= 0) {
logs.info("即時庫存檢查:商品已售罄 slot=" + slotProduct.getSlot());
android.widget.Toast.makeText(context, "商品已售罄", android.widget.Toast.LENGTH_SHORT).show();
// 觸發 UI 刷新讓商品顯示補貨中
fragment.getSrcHandlerMain().start(getClass().getSimpleName(),
VmcFragment.Option.SHIPPED_PRODUCT.getOption(), "refresh");
return;
}
} catch (LogsEmptyException e) {
// 查詢失敗時不阻擋讓原流程繼續
}
BuyStructure buyProduct = new BuyStructure(slotProduct.getField(), slotProduct.getSlot(), slotProduct.getProduct(), 1);
MyApp.getInstance().getBuyList().add(buyProduct);
fragment.getSrcHandlerMain().start(getClass().getSimpleName(), FontendActivity.Option.STOP_COUNTDOWN.getOption(), "stop countdown");

View File

@ -132,18 +132,22 @@ public class DispatchDialog extends DialogAbstract {
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
if (isFinalizing) break; // 結案倒數中忽略舊的倒數事件
setButtonOkText(Integer.parseInt(message));
if (Integer.parseInt(message) <= 15) {
setTextViewCountdown(Integer.parseInt(message));
}
break;
case NEXT_PRODUCT:
if (isFinalizing) break; // 結案倒數中不再重新啟動出貨
currentlyProcessingGoods();
break;
case ENABLED_TRUE:
if (isFinalizing) break; // 結案倒數中不改變按鈕狀態
openButtonOk();
break;
case ENABLED_FALSE:
if (isFinalizing) break; // 結案倒數中不改變按鈕狀態
closeButtonOk();
break;
case REPORT_DISPATCH:
@ -155,12 +159,15 @@ public class DispatchDialog extends DialogAbstract {
setTextViewSmall(message);
break;
case CURRENTLY_PROCESSING_GOODS:
if (isFinalizing) break; // 結案倒數中不再處理
currentlyProcessingGoods();
break;
case SHIPPED_PROCESSING_GOODS:
if (isFinalizing) break; // 結案倒數中不再處理
shippedProcessingGoods();
break;
case NEXT_PROCESS_GOODS:
if (isFinalizing) break; // 結案倒數中不再處理
nextProcessGoods();
break;
case ERROR_PROCESS_GOODS:
@ -214,6 +221,7 @@ public class DispatchDialog extends DialogAbstract {
private List<DispatchStructure> dispatchList;
private int dispatchIndex;
private boolean isReportDispatch;
private boolean isFinalizing = false; // 標記是否正在進行最後的取貨倒數結案
private boolean isChangeStatus;
private int countdownNumber = 40;
private CountDownTimer countDownTimer;
@ -293,9 +301,19 @@ public class DispatchDialog extends DialogAbstract {
this.binding.recyclerDialogDispatchList.setLayoutManager(new LinearLayoutManager(context));
this.binding.buttonOk.setOnClickListener(v -> {
currentlyProcessingGoods();
if (isFinalizing) {
// 結案倒數中按下 = 提前確認取貨關閉 dialog
if (countDownTimer != null) {
countDownTimer.cancel();
}
dialogCancel();
} else {
currentlyProcessingGoods();
}
});
// 出貨過程中隱藏按鈕等到 15 秒取貨倒數再顯示
binding.buttonOk.setVisibility(View.GONE);
setButtonOkText(0);
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
@ -440,10 +458,11 @@ public class DispatchDialog extends DialogAbstract {
checkSlotRunCount = 0;
MyApp.getInstance().getDevXinYuanController().start();
} else {
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA004");
// MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(handlerMain, slot));
// MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
getLogs().info("機台未回應,第 " + (checkSlotRunCount + 1) + " 次重試 checkSlot...");
isCheckSlotByVMC = false; // 重置旗標允許下次回調進入
checkSlotRunCount++;
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(handlerMain, slot));
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
}
}
}
@ -616,7 +635,7 @@ public class DispatchDialog extends DialogAbstract {
getLogs().warning(e);
}
setTextViewCountdown(0);
setTextViewCountdown(getCtx().getString(R.string.preparing_to_dispatch), 65);
isGetProductByVMC = false;
isCheckSlotByVMC = false;
checkSlotRunCount = 0;
@ -803,6 +822,9 @@ public class DispatchDialog extends DialogAbstract {
//偵測是否還有下個出貨商品
if (this.dispatchIndex >= this.dispatchList.size()-1) { //沒有貨
// 所有商品出貨完畢發送 MQTT Finalize 結案訊息
if (isFinalizing) return; // 防呆如果已經在結案倒數了直接跳出
isFinalizing = true;
getLogs().info("所有商品出貨完畢,發送 MQTT Finalize 結案訊息。");
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(), "update data");
@ -813,7 +835,40 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info(logItem);
}
dialogCancel(); // 結束對話框
// 停止所有計時器
closeDialogCountdown.shutdown();
if (!buttonControlCountdown.isShutdown()) {
buttonControlCountdown.shutdown();
}
if (countDownTimer != null) {
countDownTimer.cancel();
}
// 隱藏底部倒數文字
binding.tvCountdown.setVisibility(View.GONE);
// 顯示取貨確認按鈕讓使用者可以提前確認取貨
binding.buttonOk.setVisibility(View.VISIBLE);
binding.buttonOk.setEnabled(true);
binding.buttonOk.setTextColor(Color.BLACK);
setButtonOkText(0); // 顯示取貨確認不帶倒數
// 顯示取貨倒數 15 秒後自動關閉對話框
isChangeStatus = true;
setTextViewCountdown(getCtx().getString(R.string.pick_up_goods_within_15_seconds_after_pick_up_port_opened), 65);
countDownTimer = new CountDownTimer(15_000, 1_000) {
public void onTick(long millisUntilFinished) {
int seconds = (int) (millisUntilFinished / 1000);
setTextViewCountdown(seconds);
}
public void onFinish() {
dialogCancel();
}
}.start();
getLogs().info("📢 出貨完畢,取貨倒數 15 秒後關閉對話框");
isDispatchingNext = false;
isGetProductByVMC = false;
return; // 結束方法
} else { //有貨要出了
this.dispatchIndex += 1; //表記下一個商品要出貨

View File

@ -188,6 +188,20 @@ public class MemberCardPickupDialog extends DialogAbstract {
public void onSuccess(String codeId) {
getLogs().info("MemberCardPickupDialog - B680 Success, code_id: " + codeId);
// 最後一道防線出貨前即時檢查庫存
for (com.unibuy.smartdevice.structure.BuyStructure buy : MyApp.getInstance().getBuyList()) {
try {
com.unibuy.smartdevice.structure.SlotStructure liveSlot =
MyApp.getInstance().getSlotData(buy.getField(), buy.getSlot());
if (liveSlot != null && liveSlot.getCount() <= 0) {
getLogs().info("庫存已歸零 slot=" + buy.getSlot() + ",取消出貨,返回銷售平台。");
getHandlerMain().start(getClass().getSimpleName(), Option.TOAST.getOption(), "商品已售罄,請重新選擇");
dialogCancel(); // 內部已清空 BuyList + dismiss
return;
}
} catch (Exception ignored) {}
}
// 設定支付資訊
MyApp.getInstance().getReportFlowInfoData().setFlowType(41); // 員工卡
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(cardNo);

View File

@ -340,6 +340,7 @@
<string name="shopping_cart_merchant_payment_checkout">merchant payment checkout</string>
<string name="tappay_checkout">TapPay Checkout</string>
<string name="pick_up_goods_within_15_seconds_after_pick_up_port_opened">Pick up goods within 15 seconds after the pick-up port is opened</string>
<string name="preparing_to_dispatch">Preparing to dispatch</string>
<string name="enter_gift_or_scan_qrcode">Enter the gift or scan the QR Code</string>
<string name="select_combination">Select combination</string>
<string name="pickup">Pickup</string>

View File

@ -340,6 +340,7 @@
<string name="shopping_cart_merchant_payment_checkout">販売者支払いチェックアウト</string>
<string name="tappay_checkout">TapPayチェックアウト</string>
<string name="pick_up_goods_within_15_seconds_after_pick_up_port_opened">ピックアップポートが開いてから15秒以内に商品を受け取る</string>
<string name="preparing_to_dispatch">出荷準備中</string>
<string name="enter_gift_or_scan_qrcode">ギフトを入力するか、QRコードをスキャンしてください</string>
<string name="select_combination">組み合わせを選択</string>
<string name="pickup">選び出す</string>

View File

@ -250,6 +250,7 @@
<string name="shopping_cart_merchant_payment_checkout">商家支付結帳</string>
<string name="tappay_checkout">TapPay結帳</string>
<string name="pick_up_goods_within_15_seconds_after_pick_up_port_opened">取貨口開啟15秒內取貨</string>
<string name="preparing_to_dispatch">準備出貨</string>
<string name="enter_gift_or_scan_qrcode">輸入來店禮 或 掃描QRCode</string>
<string name="select_combination">選擇組合</string>
<string name="pickup">取貨</string>
@ -300,7 +301,7 @@
<string name="i_knew">知道了</string>
<string name="input_vehicle">輸入載具</string>
<string name="member_card_pickup_title">領取確認</string>
<string name="please_tap_member_card">請將員卡靠近讀卡機</string>
<string name="please_tap_member_card">請將員卡靠近讀卡機</string>
<string name="sync_product">下載商品資料</string>
<string name="sync_product_warning">確定要從雲端下載商品資料嗎?這將會更新所有商品的品名、價格與圖片,並覆蓋目前的資訊。</string>
<string name="downloading_product_data">正在下載商品資料...</string>