[PROMOTE] 晉升 dev 變更至 demo 環境

1. [FIX] 修復中國醫多貨道庫存分配異常,引進動態多貨道輪替分配機制,徹底解決一筆單多商品時貨道重複分配的 Bug。
2. [RELEASE] 遞增修補版號 verPatch 至 66,完成發版前置準備。
3. [RELEASE] 修正多商品取貨 UI 倒數閃跳與 3 分鐘閒置崩潰問題,發佈版本 65。
4. [FIX] 修正 OTA 連線超時與首頁執行緒併發競態導致 UI 異常問題。
5. [FEAT] 優化 OTA 靜默安裝邏輯,支援降版本覆蓋安裝。
6. [STYLE] 優化 CMUH 專屬佈局元件比例與樣式排版。
7. [FEAT] 新增 CMUH 客製初始畫面跳轉與多處頁面重啟邏輯優化。
8. [DOCS] 修正 release-apk 工作流中寫死 S12XYU 的問題。
This commit is contained in:
sky121113 2026-06-02 17:52:47 +08:00
commit 3db92cb3fb
13 changed files with 671 additions and 220 deletions

View File

@ -157,6 +157,7 @@ if [ -n "$APK_SOURCE" ]; then
echo "* 版本名稱 (versionName)$VERSION_NAME"
echo "* 版本代碼 (versionCode)$VERSION_CODE"
echo "* APK 檔案名稱:$APK_NAME"
echo "* 更新說明:[請在此處附上口語化的一句話更新說明]"
echo "=================================================="
else
echo "❌ 找不到編譯產出的 APK 檔案,複製失敗!"

View File

@ -42,10 +42,16 @@ if [ "$TARGET_FLAVOR" != "General" ] && [ "$TARGET_FLAVOR" != "Chengwai" ] && [
exit 1
fi
echo "📦 [Release] 開始編譯正式版變體: ${TARGET_FLAVOR}S12XYURelease"
# 根據專案 Flavor 判定硬體 Flavor中國醫 Cmuh 使用 S9XYU其餘使用 S12XYU
HW_FLAVOR="S12XYU"
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
HW_FLAVOR="S9XYU"
fi
echo "📦 [Release] 開始編譯正式版變體: ${TARGET_FLAVOR}${HW_FLAVOR}Release"
# 使用 clean 確保完整重新編譯,避開 incremental 快取問題
./gradlew clean :app:assemble${TARGET_FLAVOR}S12XYURelease --no-daemon
./gradlew clean :app:assemble${TARGET_FLAVOR}${HW_FLAVOR}Release --no-daemon
```
## 步驟二:導出正式 APK 並產出後台 Keyin 派送參考資料
@ -62,8 +68,13 @@ if [ -n "$1" ]; then
TARGET_FLAVOR=$(echo "$1" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
fi
HW_FLAVOR="S12XYU"
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
HW_FLAVOR="S9XYU"
fi
# 動態尋找編譯產出的最新正式版 APK 檔案
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}S12XYU/release/ -name "app-S_12_XY_U_${TARGET_FLAVOR}-*-release.apk" | head -n 1)
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}${HW_FLAVOR}/release/ -name "app-S_*_${TARGET_FLAVOR}-*-release.apk" | head -n 1)
if [ -n "$APK_SOURCE" ]; then
APK_NAME=$(basename "$APK_SOURCE")
@ -74,12 +85,19 @@ if [ -n "$APK_SOURCE" ]; then
echo "=================================================="
# 提取與計算版本資訊
MAJOR=$(grep "def verMajor" app/build.gradle | awk '{print $4}')
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
# 根據硬體規格決定 Major (S9XYU 為 21 吋S12XYU 為 10 吋)
if [ "$HW_FLAVOR" = "S9XYU" ]; then
MAJOR=21
else
MAJOR=10
fi
VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R"
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
FLAVOR="${TARGET_FLAVOR}S12XYU"
FLAVOR="${TARGET_FLAVOR}${HW_FLAVOR}"
echo "🚀 實體販賣機 OTA 後台新增版本 Keyin 參考資料:"
echo "--------------------------------------------------"

View File

@ -9,7 +9,7 @@ plugins {
//
// =========================================================================
def verMinor = 1
def verPatch = 33
def verPatch = 66
android {

View File

@ -322,14 +322,36 @@ public class SaleFlowHandler {
haveValuestate = true;
if (requestQty > 1) {
int checkcount = 0;
int slotnum = 0;
class TempSlot {
int slotNo;
int stock;
TempSlot(int slotNo, int stock) {
this.slotNo = slotNo;
this.stock = stock;
}
}
String[] slotPairs = medicineList.get(i).getMedicineSlot().split(",");
java.util.List<TempSlot> tempSlots = new java.util.ArrayList<>();
for (String pair : slotPairs) {
if (pair.trim().isEmpty()) continue;
String[] parts = pair.split("-");
if (parts.length == 2) {
tempSlots.add(new TempSlot(Integer.parseInt(parts[0]), Integer.parseInt(parts[1])));
}
}
int slotIdx = 0;
for (int j = 0; j < requestQty; j++) {
checkcount += 1;
if (checkcount > Integer.valueOf(medicineList.get(i).getMedicineSlot().split(",")[0].split("-")[1])) {
slotnum = Integer.valueOf(medicineList.get(i).getMedicineSlot().split(",")[1].split("-")[0]);
while (slotIdx < tempSlots.size() && tempSlots.get(slotIdx).stock <= 0) {
slotIdx++;
}
int slotnum;
if (slotIdx < tempSlots.size()) {
TempSlot activeSlot = tempSlots.get(slotIdx);
slotnum = activeSlot.slotNo;
activeSlot.stock--;
} else {
slotnum = Integer.valueOf(medicineList.get(i).getMedicineSlot().split(",")[0].split("-")[0]);
slotnum = tempSlots.get(tempSlots.size() - 1).slotNo;
}
addMedicineToBuyList(
medicineList.get(i).getProductId(),

View File

@ -0,0 +1,271 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/light_gray"
>
<LinearLayout
android:id="@+id/linearLayout5"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@color/red"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/textVersion"
android:layout_width="80dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="20250108ver1"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textThreadCount"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="TC:0"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/btnswitchlanTW"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="中"
android:visibility="gone"
android:textSize="30sp"
android:textColor="@color/white"
android:textStyle="bold"
/>
<TextView
android:id="@+id/btnswitchlanEn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="En"
android:visibility="gone"
android:textSize="30sp"
android:textColor="@color/white"
android:textStyle="bold"
/>
<TextView
android:id="@+id/btnswitchlanJa"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="start"
android:text="日"
android:visibility="gone"
android:textSize="30sp"
android:textColor="@color/white"
android:textStyle="bold"
/>
<TextView
android:id="@+id/textTemperature"
android:layout_width="60dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:text="0 ℃"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textTime"
android:layout_width="120dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:text="1911/01/01 01:01:01"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/textNetwork"
android:layout_width="150dp"
android:layout_height="match_parent"
android:gravity="center_vertical|end"
android:text="@string/no_internet"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
</LinearLayout>
<RelativeLayout
android:id="@+id/relativeLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="60dp"
android:layout_marginBottom="2dp"
android:background="@color/black"
app:layout_constraintDimensionRatio="18:21"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.media3.ui.PlayerView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:show_subtitle_button="true"
app:use_artwork="true"
app:use_controller="false"
app:resize_mode="fit"
app:surface_type="texture_view" />
<ImageView
android:id="@+id/imageAd"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:visibility="gone"
/>
</RelativeLayout>
<FrameLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/relativeLayout" />
<LinearLayout
android:id="@+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone"
android:paddingStart="10dp"
android:paddingEnd="10dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonVmc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/drinksale"
app:cornerRadius="12dp"
android:backgroundTint="@android:color/transparent"
app:strokeColor="@color/gray"
android:textSize="38sp"
app:strokeWidth="2dp"
android:visibility="gone"
android:layout_margin="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonElectric"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/board_games"
app:cornerRadius="12dp"
android:backgroundTint="@android:color/transparent"
app:strokeColor="@color/gray"
android:textSize="38sp"
android:visibility="gone"
app:strokeWidth="2dp"
android:layout_margin="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonOther"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone"
android:text="@string/other"
app:cornerRadius="12dp"
android:backgroundTint="@android:color/transparent"
app:strokeColor="@color/gray"
android:textSize="38sp"
app:strokeWidth="2dp"
android:layout_margin="4dp"/>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/floatButtonPickupCode"
android:layout_width="50dp"
android:layout_height="150dp"
android:text="取\n貨\n碼"
android:textSize="28sp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="80dp"
android:backgroundTint="@color/gray"
android:textColor="@color/black"
android:alpha="0.8"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/floatButtonShoppingCart"
app:layout_constraintEnd_toEndOf="@+id/bottom_navigation"
android:textStyle="bold"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/floatButtonShoppingCart"
android:layout_width="58dp"
android:layout_height="51dp"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginEnd="24dp"
android:layout_marginBottom="80dp"
android:contentDescription="TODO"
android:visibility="gone"
app:backgroundTint="@color/gray"
app:layout_constraintBottom_toBottomOf="@+id/fragment_container"
app:layout_constraintEnd_toEndOf="@+id/bottom_navigation"
app:maxImageSize="60dp"
app:srcCompat="@drawable/baseline_add_shopping_cart_24" />
<Button
android:id="@+id/btnTopRight"
android:layout_width="200dp"
android:layout_height="100dp"
android:text="進入後台"
android:textSize="24sp"
android:backgroundTint="@color/red"
android:textColor="@android:color/white"
android:alpha="1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:visibility="gone"
android:layout_marginTop="60dp"
android:layout_marginEnd="8dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -18,23 +18,22 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="500dp"
android:orientation="horizontal"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
>
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/scanbarcodebtn"
android:layout_width="400dp"
android:layout_height="400dp"
android:layout_marginTop="-50dp"
android:padding="10dp"
android:text="掃描單據\n按鈕"
android:textSize="70sp"
android:textColor="@android:color/white"
app:strokeWidth="5dp"
android:textSize="70sp"
app:strokeColor="@color/gray"
android:layout_marginTop="-50dp"
/>
app:strokeWidth="5dp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,16 @@
package com.unibuy.smartdevice;
import com.unibuy.smartdevice.ui.FontendActivity;
public final class AppEntry {
private AppEntry() {
}
public static boolean isCmuh() {
return "Cmuh".equalsIgnoreCase(BuildConfig.FLAVOR_project);
}
public static Class<?> initialActivity() {
return isCmuh() ? FontendActivity.class : HomeActivity.class;
}
}

View File

@ -239,6 +239,14 @@ public class HomeActivity extends AppCompatActivityAbstract {
}
settingsDao.close();
if (AppEntry.isCmuh()) {
Intent intent = new Intent(this, FontendActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
return;
}
binding = ActivityHomeBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
@ -312,7 +320,7 @@ public class HomeActivity extends AppCompatActivityAbstract {
getLogs().info("🔄HomeActivity - Scheduled Hard App restarting via gracefulDisconnect...");
com.unibuy.smartdevice.external.mqtt.MqttManager.getInstance().gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);

View File

@ -649,7 +649,7 @@ public class MainActivity extends AppCompatActivityAbstract {
getLogs().info("🔄MainActivity - Manual App restarting via gracefulDisconnect...");
com.unibuy.smartdevice.external.mqtt.MqttManager.getInstance().gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);

View File

@ -15,7 +15,7 @@ import android.util.Log;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import com.unibuy.smartdevice.HomeActivity;
import com.unibuy.smartdevice.AppEntry;
import com.unibuy.smartdevice.R;
import com.unibuy.smartdevice.MyApp;
import com.unibuy.smartdevice.devices.SlotField;
@ -57,7 +57,7 @@ public class MqttService extends Service {
createNotificationChannel();
// 2. 啟動為前台服務確保不被系統殺掉
Intent notificationIntent = new Intent(this, HomeActivity.class);
Intent notificationIntent = new Intent(this, AppEntry.initialActivity());
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
@ -118,7 +118,7 @@ public class MqttService extends Service {
.postDelayed(() -> mqttManager.gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent restartIntent = new Intent(getApplicationContext(),
com.unibuy.smartdevice.HomeActivity.class);
AppEntry.initialActivity());
restartIntent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(restartIntent);
@ -160,12 +160,12 @@ public class MqttService extends Service {
mqttManager.publishCommandAck(cmdId, "success", "Machine unlocked, restarting...");
// 🚩 優雅重啟 gracefulDisconnect不觸發 LWT killProcess
// 解鎖後重啟到初始畫面 (HomeActivity)
// 解鎖後重啟到目前版本的初始畫面
new android.os.Handler(android.os.Looper.getMainLooper())
.postDelayed(() -> mqttManager.gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent restartIntent = new Intent(getApplicationContext(),
com.unibuy.smartdevice.HomeActivity.class);
AppEntry.initialActivity());
restartIntent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(restartIntent);

View File

@ -17,6 +17,8 @@ import java.net.URL;
public class OtaManager {
private static final String TAG = "OtaManager";
private static final Logs logs = new Logs(OtaManager.class);
private static final int DOWNLOAD_CONNECT_TIMEOUT_MS = 30000;
private static final int DOWNLOAD_READ_TIMEOUT_MS = 120000;
/**
* 啟動背景執行緒執行 APP OTA 更新
@ -67,8 +69,8 @@ public class OtaManager {
private static File downloadApk(Context context, String apkUrl) throws Exception {
URL url = new URL(apkUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
connection.setConnectTimeout(DOWNLOAD_CONNECT_TIMEOUT_MS);
connection.setReadTimeout(DOWNLOAD_READ_TIMEOUT_MS);
connection.connect();
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
@ -108,10 +110,10 @@ public class OtaManager {
Process process = null;
DataOutputStream os = null;
try {
logs.info("Executing silent install command via su: pm install -r " + apkPath);
logs.info("Executing silent install command via su: pm install -r -d " + apkPath);
process = Runtime.getRuntime().exec("su");
os = new DataOutputStream(process.getOutputStream());
os.writeBytes("pm install -r " + apkPath + "\n");
os.writeBytes("pm install -r -d " + apkPath + "\n");
os.writeBytes("exit\n");
os.flush();

View File

@ -33,8 +33,8 @@ import android.widget.Toast;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.unibuy.smartdevice.AppCompatActivityAbstract;
import com.unibuy.smartdevice.AppEntry;
import com.unibuy.smartdevice.FragmentAbstract;
import com.unibuy.smartdevice.HomeActivity;
import com.unibuy.smartdevice.MainActivity;
import com.unibuy.smartdevice.MyApp;
@ -118,11 +118,11 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
private Fragment activeFragment;
private Fragment vmcFragment;
private Fragment electricFragment;
private static ReportShotTimeByScheduler reportOnScheduler;
private ReportShotTimeByScheduler reportOnScheduler;
private ConnectivityManager connectivityManager;
private ConnectivityManager.NetworkCallback networkCallback;
private static final long IDLE_TIMEOUT = 5 * 60 * 1000; // 3 分鐘
private static final long IDLE_TIMEOUT = 5 * 60 * 1000; // 5 分鐘
private final long TIME_LIMIT = 200000;
private int clickCount = 0;
private long firstClickTime = 0;
@ -238,7 +238,11 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
ElectricFragment.Option.SHIPPED_PRODUCT.getOption(), "shipped product");
}
saveData();
setBottomNavigation();// 這一個方法是在消費完後它會呼叫本方法更新銷售平台的商品列長fragment
// 🚩 安全生命週期防禦如果 Activity 已在銷毀流程中絕不執行 Fragment 提交避免崩潰
if (!isFinishing() && !isDestroyed()) {
setBottomNavigation();// 這一個方法是在消費完後它會呼叫本方法更新銷售平台的商品列長fragment
}
break;
case REPORT_AND_SHOT_TIME:
String ver = "";
@ -263,7 +267,8 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
// 第一層定時重啟到了整點就啟動等待模式RestartAPP2 自己會等閒置 >= 90 秒才真重啟
if (timeByM.equals("04:00") || timeByM.equals("08:00") || timeByM.equals("12:00")
|| timeByM.equals("16:00") || timeByM.equals("20:00") || timeByM.equals("23:59")) {
android.content.SharedPreferences prefs = getSharedPreferences("AppRestartPrefs", Context.MODE_PRIVATE);
android.content.SharedPreferences prefs = getSharedPreferences("AppRestartPrefs",
Context.MODE_PRIVATE);
String today = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(new Date());
String currentSlot = today + " " + timeByM;
String lastRestartSlot = prefs.getString("last_restart_slot", "");
@ -280,11 +285,11 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
// 第二層閒置深度刷新任何時間閒置超過 30 分鐘就重啟 (暫時關閉改依賴定時重啟)
/*
if (MyApp.getInstance().getIdleSeconds() >= 30 * 60) {
getLogs().info("閒置超過 30 分鐘,執行深度排毒重啟...");
new RestartAPP2(this).start();
}
*/
* if (MyApp.getInstance().getIdleSeconds() >= 30 * 60) {
* getLogs().info("閒置超過 30 分鐘,執行深度排毒重啟...");
* new RestartAPP2(this).start();
* }
*/
// 這裡判斷每5分鐘去跑連線下位機的狀態
// String minutePart = timeByM.split(":")[1]; // 取得 "mm" 部分
@ -305,24 +310,24 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
// new CMUHAutoRun(this).start();
}
getLogs().update("ThreadCount", String.valueOf(Tools.threadCount()));
String tc = getLogs().getMessage();
getLogs().update("ViceoCount", String.valueOf(VideoEoxPlayer.getExoPlayerTotal()));
String vc = getLogs().getMessage();
binding.textThreadCount.setText("TC:" + tc + " VC:" + vc);
getLogs().clearMessage();
// 直接使用區域變數更新 UI避免多執行緒 Race Condition 導致 getLogs().getMessage() 被改寫或清空而使 UI 變空
String threadCountStr = String.valueOf(Tools.threadCount());
String videoCountStr = String.valueOf(VideoEoxPlayer.getExoPlayerTotal());
binding.textThreadCount.setText("TC:" + threadCountStr + " VC:" + videoCountStr);
getLogs().update("ThreadCount", threadCountStr);
getLogs().update("ViceoCount", videoCountStr);
Tools.threadInfos();
String networkDevice = getNetworkDevice();
binding.textNetwork.setText(getString(R.string.network) + "" + networkDevice);
getLogs().update("Network", networkDevice);
binding.textNetwork.setText(getString(R.string.network) + "" + getLogs().getMessage());
getLogs().clearMessage();
DevXinYuanController.setVersion(ver);
String tempStr = DevXinYuanController.getTemperature() + "";
binding.textTemperature.setText(getString(R.string.temp) + ":" + tempStr);
getLogs().update("Temperature", String.valueOf(DevXinYuanController.getTemperature()), "");
binding.textTemperature.setText(getString(R.string.temp) + ":" + getLogs().getMessage());
getLogs().clearMessage();
try {
// httpAPI.detectHost();
@ -436,7 +441,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
public void ReSetAPP() {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@ -449,7 +454,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
public void ReSetAPP2() {
Intent intent = new Intent(getApplicationContext(), HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
@ -467,7 +472,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
return saleFlowHandler;
}
public void startOnSwitchFragment() {
try {
stopOnSwitchFragment(); // 先安全釋放舊播放器
@ -487,7 +491,8 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
videoEoxPlayer = new VideoEoxPlayer(getCtx()); // 用新版建構子
getHandlerMain().getHandler().postDelayed(() -> {
if (isFinishing() || isDestroyed()) return;
if (isFinishing() || isDestroyed())
return;
if (videoEoxPlayer != null) {
// videoEoxPlayer.playMedia(binding.videoView, binding.imageAd); // 傳入 view
// 開始播放
@ -522,9 +527,17 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
resetIdleTimer();
return;
}
getLogs().info("⚠️ 無操作 3 分鐘,自動返回首頁(清空購物車)");
// 🚩 若當前機台正處於忙碌/出貨狀態則忽略閒置返回待機頁並重新計時防止出貨中 Activity 被銷毀
if (MyApp.getInstance().isMachineBusy()) {
getLogs().info(" 偵測到當前機台正處於忙碌/出貨狀態,忽略閒置返回待機頁。原因: " + MyApp.getInstance().getBusyReason());
resetIdleTimer();
return;
}
getLogs().info("⚠️ 無操作 3 分鐘,自動返回初始畫面(清空購物車)");
MyApp.getInstance().getBuyList().clear(); // 🚩 逾時返回首頁時務必清空購物車避免機台進入永久忙碌狀態
Intent intent = new Intent(FontendActivity.this, HomeActivity.class);
Intent intent = new Intent(FontendActivity.this, AppEntry.initialActivity());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
@ -700,12 +713,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
binding.floatButtonShoppingCart.setVisibility(View.GONE);
}
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
}
reportOnScheduler = new ReportShotTimeByScheduler(getHandlerMain());
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
startHeaderReportScheduler();
// binding.btnswitchlanTW.setOnClickListener(v -> switchLanguage("tw"));
//
@ -753,6 +761,20 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
startOnSwitchFragment();
}
private void startHeaderReportScheduler() {
stopHeaderReportScheduler();
reportOnScheduler = new ReportShotTimeByScheduler(getHandlerMain());
getHandlerMain().start(getClass().getSimpleName(), Option.REPORT_AND_SHOT_TIME.getOption(), "start");
reportOnScheduler.start(1L, 10L, TimeUnit.SECONDS);
}
private void stopHeaderReportScheduler() {
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
reportOnScheduler = null;
}
}
private void hiddenMultiLanguageButton() {
binding.btnswitchlanTW.setVisibility(View.GONE);
binding.btnswitchlanEn.setVisibility(View.GONE);
@ -840,7 +862,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
/**
* 現代化重啟不再使用 sleep 等待直接確認閒置秒數後執行真重啟
* - 只要閒置 >= 90 立刻啟動 HomeActivity killProcess
* - 只要閒置 >= 90 立刻啟動目前版本的初始畫面 killProcess
* - 若有人正在使用閒置 < 90 則等待 5 秒後再重新判斷最多等待 10 分鐘
*/
private class RestartAPP2 extends HandlerMainScheduler {
@ -864,7 +886,7 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
getLogs().info("RestartAPP2執行真重啟 (MQTT 優雅斷線模式)");
com.unibuy.smartdevice.external.mqtt.MqttManager.getInstance().gracefulDisconnect(() -> {
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
Intent intent = new Intent(getApplicationContext(), com.unibuy.smartdevice.HomeActivity.class);
Intent intent = new Intent(getApplicationContext(), AppEntry.initialActivity());
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
@ -1230,9 +1252,6 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
}
}
if (reportOnScheduler != null) {
reportOnScheduler.shutdown();
reportOnScheduler = null;
}
stopHeaderReportScheduler();
}
}
}

View File

@ -135,22 +135,24 @@ public class DispatchDialog extends DialogAbstract {
cancelOnThreadCountdown();
break;
case SET_COUNTDOWN_TEXT:
if (isFinalizing) break; // 結案倒數中忽略舊的倒數事件
if (isFinalizing)
break; // 結案倒數中忽略舊的倒數事件
setButtonOkText(Integer.parseInt(message));
if (Integer.parseInt(message) <= 15) {
setTextViewCountdown(Integer.parseInt(message));
}
// 💡 徹底閹割背景防護計時器對 TextView 倒數文字的修改防堵假倒數與 14 秒閃跳 Bug
break;
case NEXT_PRODUCT:
if (isFinalizing) break; // 結案倒數中不再重新啟動出貨
if (isFinalizing)
break; // 結案倒數中不再重新啟動出貨
currentlyProcessingGoods();
break;
case ENABLED_TRUE:
if (isFinalizing) break; // 結案倒數中不改變按鈕狀態
if (isFinalizing)
break; // 結案倒數中不改變按鈕狀態
openButtonOk();
break;
case ENABLED_FALSE:
if (isFinalizing) break; // 結案倒數中不改變按鈕狀態
if (isFinalizing)
break; // 結案倒數中不改變按鈕狀態
closeButtonOk();
break;
case REPORT_DISPATCH:
@ -162,15 +164,18 @@ public class DispatchDialog extends DialogAbstract {
setTextViewSmall(message);
break;
case CURRENTLY_PROCESSING_GOODS:
if (isFinalizing) break; // 結案倒數中不再處理
if (isFinalizing)
break; // 結案倒數中不再處理
currentlyProcessingGoods();
break;
case SHIPPED_PROCESSING_GOODS:
if (isFinalizing) break; // 結案倒數中不再處理
if (isFinalizing)
break; // 結案倒數中不再處理
shippedProcessingGoods();
break;
case NEXT_PROCESS_GOODS:
if (isFinalizing) break; // 結案倒數中不再處理
if (isFinalizing)
break; // 結案倒數中不再處理
nextProcessGoods();
break;
case ERROR_PROCESS_GOODS:
@ -191,7 +196,7 @@ public class DispatchDialog extends DialogAbstract {
startCountdown();
getLogs().info("✅ 出貨到一半失敗");
deliverylot(0);//出貨失敗寫入log
deliverylot(0);// 出貨失敗寫入log
getLogs().info("📦 出貨狀況總結:");
for (String logItem : deliveryLogs) {
getLogs().info(logItem);
@ -268,26 +273,30 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info("✅ BuyList 有多筆,將依序連續出貨。");
}
for (BuyStructure buyStructure: buyList) {
for (BuyStructure buyStructure : buyList) {
getLogs().info(buyStructure.toString());
if (!buyStructure.getProduct().getMaterialCode().isEmpty() && buyStructure.getProduct().getMaterialCode().contains(":")) {
if (!buyStructure.getProduct().getMaterialCode().isEmpty()
&& buyStructure.getProduct().getMaterialCode().contains(":")) {
String[] materialCodes = buyStructure.getProduct().getMaterialCode().split(":");
String productId = materialCodes[0];
int count = Integer.parseInt(materialCodes[1]);
try {
SlotStructure slotData = MyApp.getInstance().getSlotData(buyStructure.getField(), buyStructure.getSlot());
SlotStructure slotData = MyApp.getInstance().getSlotData(buyStructure.getField(),
buyStructure.getSlot());
if (slotData.getProduct().getProductID().equals(productId)) {
DispatchStructure dispatchData = new DispatchStructure(buyStructure.getField(), buyStructure.getSlot(), slotData.getProduct(), count*buyStructure.getCount());
DispatchStructure dispatchData = new DispatchStructure(buyStructure.getField(),
buyStructure.getSlot(), slotData.getProduct(), count * buyStructure.getCount());
dispatchData.setFreeGift(buyStructure.isFreeGift());
// 這裡的 buyStructure.getCount() 現在會是正確的 QTY
this.dispatchList.add(dispatchData);
}
} catch (LogsEmptyException e) {
throw new RuntimeException(e);
//很嚴重的錯誤
// 很嚴重的錯誤
}
} else {
DispatchStructure dispatchData = new DispatchStructure(buyStructure.getField(), buyStructure.getSlot(), buyStructure.getProduct(), buyStructure.getCount());
DispatchStructure dispatchData = new DispatchStructure(buyStructure.getField(), buyStructure.getSlot(),
buyStructure.getProduct(), buyStructure.getCount());
dispatchData.setFreeGift(buyStructure.isFreeGift());
// 這裡的 buyStructure.getCount() 現在會是正確的 QTY
this.dispatchList.add(dispatchData);
@ -313,10 +322,13 @@ public class DispatchDialog extends DialogAbstract {
if (decryptedStr != null && !decryptedStr.isEmpty()) {
JSONObject fullJson = new JSONObject(decryptedStr);
patName = fullJson.optString("NAME", "");
if (patName.isEmpty()) patName = fullJson.optString("PATNAME", "");
if (patName.isEmpty()) patName = fullJson.optString("patName", "");
if (patName.isEmpty()) patName = "未知患者";
if (patName.isEmpty())
patName = fullJson.optString("PATNAME", "");
if (patName.isEmpty())
patName = fullJson.optString("patName", "");
if (patName.isEmpty())
patName = "未知患者";
sn = fullJson.optString("SN", "");
drgNo = fullJson.optString("DRGNO", "");
}
@ -335,11 +347,12 @@ public class DispatchDialog extends DialogAbstract {
// 按照最新要求[患者姓名]([藥單號]) 用括號
String barcodeInfo = patName + "(" + drgNo + ")";
getLogs().info("✅ 中國醫 QR Code 取物單資訊打包上報:姓名 " + patName + ", 藥單號 " + drgNo + " -> barcodeInfo: " + barcodeInfo);
getLogs().info(
"✅ 中國醫 QR Code 取物單資訊打包上報:姓名 " + patName + ", 藥單號 " + drgNo + " -> barcodeInfo: " + barcodeInfo);
// 設定到 flowBarCode (會連動到後台 member_barcode 欄位)
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(barcodeInfo);
// CMUH: flow_id 保留 App 機台流水號order_no 使用取物單序號 SN 方便後台對藥單
if (sn != null && !sn.isEmpty()) {
finalizeBuilder.setOrderNo(sn);
@ -351,7 +364,8 @@ public class DispatchDialog extends DialogAbstract {
}
}
this.recyclerDialogDispatchListAdpter = new RecyclerDialogDispatchListAdpter(getCtx(), this.dispatchList, getHandlerMain());
this.recyclerDialogDispatchListAdpter = new RecyclerDialogDispatchListAdpter(getCtx(), this.dispatchList,
getHandlerMain());
this.binding.recyclerDialogDispatchList.setAdapter(recyclerDialogDispatchListAdpter);
this.binding.recyclerDialogDispatchList.setLayoutManager(new LinearLayoutManager(context));
@ -469,15 +483,19 @@ public class DispatchDialog extends DialogAbstract {
@Override
public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) {
if (isCheckSlotByVMC) return;
if (isCheckSlotByVMC)
return;
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA006");
isCheckSlotByVMC = true; //進來立即設定true可以判斷掉重複出貨
isCheckSlotByVMC = true; // 進來立即設定true可以判斷掉重複出貨
boolean isCMD = false;
boolean isStatus = false;
String errorDetail = "";
for (byte[] buffer: devController.getReadBufferByMax()) {
for (byte[] buffer : devController.getReadBufferByMax()) {
getLogs().info("🔍 checkSlot 收到原始 Buffer: " + PortTools.showHex(buffer));
deliveryLogs.add("🔍 checkSlot 收到: " + PortTools.showHex(buffer));
if (buffer[0] == 0x02) {
getLogs().info("XinYuan Read:" + PortTools.showHex(buffer));
@ -495,16 +513,20 @@ public class DispatchDialog extends DialogAbstract {
if (isCMD) {
if (isStatus) {
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA001");
setTextViewCountdown("商品出貨中",65);
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000);
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().getProduct(slot));
getLogs().info("-----------------商品出貨中-----------------");
setTextViewCountdown("商品出貨中", 65);
MyApp.getInstance().getDevXinYuanController()
.getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000);
MyApp.getInstance().getDevXinYuanController().addSendBuffer(
MyApp.getInstance().getDevXinYuanController().getDevXinYuan().getProduct(slot));
} else {
if (!errorDetail.isEmpty()) {
MqttManager.getInstance().publishError(slot, errorDetail);
}
buttonControlCountdown.shutdown();
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.check_whether_goods_have_been_taken_out_before_confirming_collection));//請檢查商品有無取出再確認取貨
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(),
getCtx().getString(
R.string.check_whether_goods_have_been_taken_out_before_confirming_collection));// 請檢查商品有無取出再確認取貨
}
} else {
boolean isCmuh = "Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project);
@ -517,19 +539,24 @@ public class DispatchDialog extends DialogAbstract {
if (!MyApp.checkAndReconnectDev()) {
getLogs().info("無法連線到下位機,出貨作業中止");
}
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), "領藥有問題 請等3秒後再重掃");
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(),
"領藥有問題 請等3秒後再重掃");
} else {
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.slave_computer_did_not_respond_wait_3_seconds_try_again));
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(),
getCtx().getString(R.string.slave_computer_did_not_respond_wait_3_seconds_try_again));
MyApp.getInstance().getDevXinYuanController().start();
}
checkSlotRunCount = 0;
} else {
getLogs().info("機台未回應,第 " + (checkSlotRunCount + 1) + " 次重試 checkSlot..." + (isCmuh ? " (溫和模式,不發送指令)" : ""));
getLogs().info("機台未回應,第 " + (checkSlotRunCount + 1) + " 次重試 checkSlot..."
+ (isCmuh ? " (溫和模式,不發送指令)" : ""));
isCheckSlotByVMC = false; // 重置旗標允許下次回調進入
checkSlotRunCount++;
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(handlerMain, slot));
MyApp.getInstance().getDevXinYuanController()
.getReadBufferByNow(new DevCheckSlotByVMC(handlerMain, slot));
if (!isCmuh) {
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
MyApp.getInstance().getDevXinYuanController().addSendBuffer(
MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
}
}
}
@ -538,6 +565,7 @@ public class DispatchDialog extends DialogAbstract {
private boolean isGetProductByVMC = false;
private int getProductRunCount = 0;
public class DevGetProductByVMC extends DevController.ReadBufferOnScheduler {
private int slot;
@ -559,24 +587,28 @@ public class DispatchDialog extends DialogAbstract {
@Override
public void readBufferOnScheduler(DevController devController, HandlerMain handlerMain) {
if (isGetProductByVMC) return;
if (isGetProductByVMC)
return;
boolean isCMD = false;
boolean inTheMiddle = false;
boolean isSuccess = false;
String errorDetail = "";
for (byte[] buffer: devController.getReadBufferByMax()) {
for (byte[] buffer : devController.getReadBufferByMax()) {
getLogs().info("🔍 getProduct 收到原始 Buffer: " + PortTools.showHex(buffer));
deliveryLogs.add("🔍 getProduct 收到: " + PortTools.showHex(buffer));
if (buffer[0] == 0x04) {
getLogs().info("XinYuan Read:" + PortTools.showHex(buffer));
isCMD = true;
// if (buffer[1] == 0x01) {
// inTheMiddle = true;
// isSuccess = true;
// break;
// }
// if (buffer[1] == 0x01) {
// inTheMiddle = true;
// isSuccess = true;
// break;
// }
if (buffer[1] == 0x01 || buffer[1] == 0x10 || buffer[1] == 0x11) {
inTheMiddle = true;
@ -624,23 +656,28 @@ public class DispatchDialog extends DialogAbstract {
}
}
getLogs().info("✅ 出貨成功,觸發 nextProcessGoods");
deliverylot(1);//出貨成功寫入出貨狀況的LOG
getSrcHandlerMain().send(getClass().getSimpleName(), Option.NEXT_PROCESS_GOODS.getOption(), "next process goods");
deliverylot(1);// 出貨成功寫入出貨狀況的LOG
getSrcHandlerMain().send(getClass().getSimpleName(), Option.NEXT_PROCESS_GOODS.getOption(),
"next process goods");
} else {
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA002");
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot),5000L);
MyApp.getInstance().getDevXinYuanController()
.getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000L);
}
} else {
if (!errorDetail.isEmpty()) {
MqttManager.getInstance().publishError(slot, errorDetail);
}
buttonControlCountdown.shutdown();
getSrcHandlerMain().send(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.product_delivery_issues_contact_the_counter_window));
getSrcHandlerMain().send(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(),
getCtx().getString(R.string.product_delivery_issues_contact_the_counter_window));
}
} else {
getLogs().info("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA003");
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot),5000L);
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
MyApp.getInstance().getDevXinYuanController()
.getReadBufferByNow(new DevGetProductByVMC(handlerMain, slot), 5000L);
MyApp.getInstance().getDevXinYuanController()
.addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
}
}
}
@ -650,13 +687,13 @@ public class DispatchDialog extends DialogAbstract {
slotsDao.deleteAll();
slotsDao.insertAll(MyApp.getInstance().getSlotMap());
slotsDao.close();
getLogs().info("save count:"+ MyApp.getInstance().getSlotList(SlotField.VMC.getField()).size());
getLogs().info("save count:" + MyApp.getInstance().getSlotList(SlotField.VMC.getField()).size());
}
public void currentlyProcessingGoods() { //出貨前動作UI與資料變更
public void currentlyProcessingGoods() { // 出貨前動作UI與資料變更
getLogs().info("----- currentlyProcessingGoods -----");
if (this.dispatchIndex == -1) { //最後一次確定完後沒有貨要出就關閉視窗
if (this.dispatchIndex == -1) { // 最後一次確定完後沒有貨要出就關閉視窗
dialogCancel();
}
@ -666,11 +703,11 @@ public class DispatchDialog extends DialogAbstract {
int slot = dispatch.getSlot();
int count = dispatch.getCount();
int quantity = dispatch.getQuantity();
boolean isShipped = dispatch.isShipped(); //表記是否是正在出貨
int increase = 1; //機台要出幾個貨
boolean isShipped = dispatch.isShipped(); // 表記是否是正在出貨
int increase = 1; // 機台要出幾個貨
try {
//出貨數量
// 出貨數量
if (SlotField.getSlotField(field).isCanOneShipped()) {
increase = 1;
} else {
@ -690,11 +727,11 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info("isShipped:" + isShipped);
getLogs().info("dispatchIndex:" + dispatchIndex + " dispatchList:" + dispatchList.size());
getLogs().info("count:" + count +" quantity:" + quantity);
getLogs().info("count:" + count + " quantity:" + quantity);
if (isShipped) { //正在要出貨的話
if (count < quantity) { //count 目前出了多少貨 Quantity 總貨物數量
//設定商品名稱和圖片
if (isShipped) { // 正在要出貨的話
if (count < quantity) { // count 目前出了多少貨 Quantity 總貨物數量
// 設定商品名稱和圖片
binding.textProductName.setText(product.getProductName());
try {
@ -710,14 +747,15 @@ public class DispatchDialog extends DialogAbstract {
getProductRunCount = 0;
isChangeStatus = true;
getHandlerMain().start(getClass().getSimpleName(), Option.SHIPPED_PROCESSING_GOODS.getOption(), "shipped processing goods");
getHandlerMain().start(getClass().getSimpleName(), Option.SHIPPED_PROCESSING_GOODS.getOption(),
"shipped processing goods");
}
}
}
}
//devShippedProduct(field, slot, increase);
//出貨中動作
// devShippedProduct(field, slot, increase);
// 出貨中動作
public void shippedProcessingGoods() {
getLogs().info("----- shippedProcessingGoods -----");
@ -727,11 +765,11 @@ public class DispatchDialog extends DialogAbstract {
int slot = dispatch.getSlot();
int count = dispatch.getCount();
int quantity = dispatch.getQuantity();
boolean isShipped = dispatch.isShipped(); //表記是否是正在出貨
int increase = 1; //機台要出幾個貨
boolean isShipped = dispatch.isShipped(); // 表記是否是正在出貨
int increase = 1; // 機台要出幾個貨
try {
//出貨數量
// 出貨數量
if (SlotField.getSlotField(field).isCanOneShipped()) {
increase = 1;
} else {
@ -745,10 +783,10 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info("isShipped:" + isShipped);
getLogs().info("dispatchIndex:" + dispatchIndex + " dispatchList:" + dispatchList.size());
getLogs().info("count:" + count +" quantity:" + quantity);
getLogs().info("count:" + count + " quantity:" + quantity);
if (isShipped) { //正在要出貨的話
//控制倒數計時先倒數再出貨
if (isShipped) { // 正在要出貨的話
// 控制倒數計時先倒數再出貨
try {
SlotField slotField = SlotField.getSlotField(field);
if (slotField.isAutoNextProduct()) {
@ -768,25 +806,26 @@ public class DispatchDialog extends DialogAbstract {
}
closeButtonOk();
devRunVMC(field, slot, increase);
// try {
// closeButtonOk();
// SlotField slotField = SlotField.getSlotField(field);
// switch (slotField) {
// case VMC:
// devRunVMC(field, slot, increase);
// break;
// case ELECTRIC:
// try {
// devRunElectric(field, slot, increase);
// } catch (LogsSecurityException | LogsUnsupportedOperationException | LogsIOException e) {
// getLogs().warning(e);
// }
//
// break;
// }
// } catch (LogsEmptyException e) {
// getLogs().warning(e);
// }
// try {
// closeButtonOk();
// SlotField slotField = SlotField.getSlotField(field);
// switch (slotField) {
// case VMC:
// devRunVMC(field, slot, increase);
// break;
// case ELECTRIC:
// try {
// devRunElectric(field, slot, increase);
// } catch (LogsSecurityException | LogsUnsupportedOperationException |
// LogsIOException e) {
// getLogs().warning(e);
// }
//
// break;
// }
// } catch (LogsEmptyException e) {
// getLogs().warning(e);
// }
}
} else {
dialogCancel();
@ -795,12 +834,14 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info("index:" + this.dispatchIndex);
}
private void nextProcessGoods() { //出貨後動作索引變更和上傳資料
private void nextProcessGoods() { // 出貨後動作索引變更和上傳資料
getLogs().info("----- nextProcessGoods -----");
// 在方法開頭檢查 dispatchIndex 的有效性
if (this.dispatchList == null || this.dispatchList.isEmpty() || this.dispatchIndex < 0 || this.dispatchIndex >= this.dispatchList.size()) {
getLogs().info("⚠ nextProcessGoods: dispatchList 為空或 dispatchIndex 越界直接返回。dispatchIndex=" + this.dispatchIndex);
if (this.dispatchList == null || this.dispatchList.isEmpty() || this.dispatchIndex < 0
|| this.dispatchIndex >= this.dispatchList.size()) {
getLogs().info(
"⚠ nextProcessGoods: dispatchList 為空或 dispatchIndex 越界直接返回。dispatchIndex=" + this.dispatchIndex);
isDispatchingNext = false; // 確保重置鎖
isGetProductByVMC = false; // 確保重置鎖
return;
@ -817,9 +858,9 @@ public class DispatchDialog extends DialogAbstract {
int slot = dispatch.getSlot();
int count = dispatch.getCount();
int quantity = dispatch.getQuantity();
boolean isShipped = dispatch.isShipped(); //表記是否是正在出貨
int increase = 1; //機台要出幾個貨
int remaining = 0;//計算貨道剩餘數量
boolean isShipped = dispatch.isShipped(); // 表記是否是正在出貨
int increase = 1; // 機台要出幾個貨
int remaining = 0;// 計算貨道剩餘數量
// 防呆如果這筆出貨已完成不應再進入出貨流程
if (count >= quantity) {
@ -831,7 +872,7 @@ public class DispatchDialog extends DialogAbstract {
// 🔽 保持原本 nextProcessGoods 原樣繼續執行...
try {
//出貨數量
// 出貨數量
if (SlotField.getSlotField(field).isCanOneShipped()) {
increase = 1;
} else {
@ -844,15 +885,15 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info("isShipped:" + isShipped);
getLogs().info("dispatchIndex:" + dispatchIndex + " dispatchList:" + dispatchList.size());
getLogs().info("count:" + count +" quantity:" + quantity);
getLogs().info("count:" + count + " quantity:" + quantity);
if (isShipped) { //正在要出貨的話出貨完成後
if (count < quantity) { //count 目前出了多少貨 Quantity 總貨物數量
if (isShipped) { // 正在要出貨的話出貨完成後
if (count < quantity) { // count 目前出了多少貨 Quantity 總貨物數量
try {
if (SlotField.getSlotField(field).isCanOneShipped()) { //一個出貨還是一次出貨
count += 1; //目前出貨量+將要出1個
if (SlotField.getSlotField(field).isCanOneShipped()) { // 一個出貨還是一次出貨
count += 1; // 目前出貨量+將要出1個
} else {
count = quantity; //要出全部
count = quantity; // 要出全部
}
} catch (LogsEmptyException e) {
count += 1;
@ -864,7 +905,7 @@ public class DispatchDialog extends DialogAbstract {
try {
int slotCount = MyApp.getInstance().getSlotData(field, slot).getCount();
remaining = slotCount-increase; //計算剩餘數量
remaining = slotCount - increase; // 計算剩餘數量
MyApp.getInstance().getSlotData(field, slot).setCount(remaining);
MyApp.getInstance().getReportDispatchInfoData().setRemaining(remaining);
} catch (LogsEmptyException e) {
@ -876,30 +917,36 @@ public class DispatchDialog extends DialogAbstract {
* 1)將原來的ProductId欄位由醫令代碼修改為原單的ProductId代碼
* 2)flowtype 直接固定寫入42代表QRCode取貨中國醫專用
* 3)dispatchStatusType欄位寫入0代表成功
* */
*/
int price = product.getRealPrice() * count;
// [MQTT Finalize 模式] 累積出貨成功記錄最後統一送出
int productIdInt = 0;
try { productIdInt = Integer.parseInt(product.getProductID()); } catch (Exception ignored) {}
try {
productIdInt = Integer.parseInt(product.getProductID());
} catch (Exception ignored) {
}
finalizeBuilder.addDispenseRecord(slot, productIdInt, price * increase, 0, true, remaining);
getLogs().info("[Finalize] 已累積出貨記錄: slot=" + slot + ", product=" + productIdInt + ", remaining=" + remaining);
getLogs().info(
"[Finalize] 已累積出貨記錄: slot=" + slot + ", product=" + productIdInt + ", remaining=" + remaining);
getLogs().info("count:" + count +" quantity:" + quantity);
getLogs().info("count:" + count + " quantity:" + quantity);
if (count == quantity) { //貨物是否已經出貨完畢
//偵測是否還有下個出貨商品
if (this.dispatchIndex >= this.dispatchList.size()-1) { //沒有貨
if (count == quantity) { // 貨物是否已經出貨完畢
// 偵測是否還有下個出貨商品
if (this.dispatchIndex >= this.dispatchList.size() - 1) { // 沒有貨
// 所有商品出貨完畢發送 MQTT Finalize 結案訊息
if (isFinalizing) return; // 防呆如果已經在結案倒數了直接跳出
if (isFinalizing)
return; // 防呆如果已經在結案倒數了直接跳出
isFinalizing = true;
getLogs().info("所有商品出貨完畢,發送 MQTT Finalize 結案訊息。");
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
insertDrgnoToMemo();
}
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(), "update data");
saveData();
getLogs().info("所有商品出貨完畢,發送 MQTT Finalize 結案訊息。");
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
insertDrgnoToMemo();
}
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(),
"update data");
saveData();
getLogs().info("📦 出貨狀況總結:");
for (String logItem : deliveryLogs) {
@ -926,12 +973,17 @@ public class DispatchDialog extends DialogAbstract {
// 顯示取貨倒數 15 秒後自動關閉對話框
isChangeStatus = true;
setTextViewCountdown(getCtx().getString(R.string.pick_up_goods_within_15_seconds_after_pick_up_port_opened), 65);
// 💡 統一都有括號消除初始與倒數的格式不一致閃爍
setTextViewCountdown(
getCtx().getString(R.string.pickup_port_open) + "(15)"
+ getCtx().getString(R.string.pick_up_within),
65);
countDownTimer = new CountDownTimer(15_000, 1_000) {
public void onTick(long millisUntilFinished) {
int seconds = (int) (millisUntilFinished / 1000);
setTextViewCountdown(seconds);
}
public void onFinish() {
dialogCancel();
}
@ -941,17 +993,50 @@ public class DispatchDialog extends DialogAbstract {
isDispatchingNext = false;
isGetProductByVMC = false;
return; // 結束方法
} else { //有貨要出了
this.dispatchIndex += 1; //表記下一個商品要出貨
} else { // 有下一個商品要出了
getLogs().info("📦 還有商品要出,啟動精準 15 秒取貨時間後再出下一個商品");
// 1. 停止原本在出貨中啟動的 40 秒防護計時器防堵背景干擾
closeDialogCountdown.shutdown();
if (!buttonControlCountdown.isShutdown()) {
buttonControlCountdown.shutdown();
}
if (countDownTimer != null) {
countDownTimer.cancel();
}
// 2. 表記下一個商品要出貨
this.dispatchIndex += 1;
// 3. 啟動精準的 15 秒取貨倒數計時時間到了之後再自動出下一個商品
isChangeStatus = true;
// 💡 統一都有括號消除初始與倒數的格式不一致閃爍
setTextViewCountdown(
getCtx().getString(R.string.pickup_port_open) + "(15)"
+ getCtx().getString(R.string.pick_up_within),
65);
countDownTimer = new CountDownTimer(15_000, 1_000) {
public void onTick(long millisUntilFinished) {
int seconds = (int) (millisUntilFinished / 1000);
setTextViewCountdown(seconds);
}
public void onFinish() {
getLogs().info("📢 15 秒取貨時間結束自動啟動下一個商品出貨。Index=" + dispatchIndex);
if (!isFinalizing) {
currentlyProcessingGoods();
}
}
}.start();
}
}
}
getLogs().info("確保重置出貨控制狀態 dispatchIndex="+dispatchIndex);
getLogs().info("確保重置出貨控制狀態 dispatchIndex=" + dispatchIndex);
// 在這裡確保重置出貨控制狀態
dispatch.setGetProductComplete(false); // 重置出貨旗標
isDispatchingNext = false; // 解鎖流程控制
isGetProductByVMC = false; // 解鎖出貨成功判定
isDispatchingNext = false; // 解鎖流程控制
isGetProductByVMC = false; // 解鎖出貨成功判定
}
private void finalizeFailedDispatch(String message) {
@ -975,7 +1060,8 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info("出貨失敗,發送 MQTT Finalize 結案訊息: " + message);
MqttManager.getInstance().publishTransactionFinalize(finalizeBuilder.build());
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(), "update data");
getSrcHandlerMain().send(getClass().getSimpleName(), FontendActivity.Option.RESET_DATA.getOption(),
"update data");
saveData();
}
@ -1028,15 +1114,16 @@ public class DispatchDialog extends DialogAbstract {
return false;
}
private void deliverylot(int msgflag){
private void deliverylot(int msgflag) {
String productId = dispatchList.get(dispatchIndex).getProduct().getProductID();
String productName = dispatchList.get(dispatchIndex).getProduct().getProductName();
if(msgflag == 1){
if (msgflag == 1) {
deliveryLogs.add("✔ 出貨成功 - " + productId + " / " + productName);
}else if(msgflag == 0) {
} else if (msgflag == 0) {
deliveryLogs.add("✖ 出貨失敗(貨道無實體商品或商品未取出等問題)- " + productId + " / " + productName);
}
}
private void devRunVMC(int field, int slot, int increase) {
if (buttonControlCountdown.isShutdown()) {
@ -1048,11 +1135,13 @@ public class DispatchDialog extends DialogAbstract {
getLogs().info("11111");
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(getHandlerMain(), slot));
getLogs().info("22222");
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
MyApp.getInstance().getDevXinYuanController()
.addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
getLogs().info("33333");
}
private void devRunElectric(int field, int slot, int increase) throws LogsEmptyException, LogsSecurityException, LogsUnsupportedOperationException, LogsIOException {
private void devRunElectric(int field, int slot, int increase)
throws LogsEmptyException, LogsSecurityException, LogsUnsupportedOperationException, LogsIOException {
if (buttonControlCountdown.isShutdown()) {
buttonControlCountdown.start(5);
} else {
@ -1063,7 +1152,7 @@ public class DispatchDialog extends DialogAbstract {
isChangeStatus = false;
SlotStructure slotData = MyApp.getInstance().getSlotData(field, slot);
// slotData.setLock(true);
// slotData.setLock(true);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.TAIWAN);
Date timeEpiredDate = new Date();
if (slotData.getOtherData().has("timeExpired")) {
@ -1095,18 +1184,18 @@ public class DispatchDialog extends DialogAbstract {
long diffInMillis = timeEpiredDate.getTime() - now.getTime();
long diffInSeconds = diffInMillis / 1000; // 總秒數
int hours = (int) (diffInSeconds / 3600); // 小時
int hours = (int) (diffInSeconds / 3600); // 小時
int minutes = (int) ((diffInSeconds % 3600) / 60); // 剩下的分鐘
int seconds = (int) (diffInSeconds % 60); // 剩下的秒數
int hoursByDay = (int) (diffInSeconds / 3600 % 24); // 小時
int day = (int) (diffInSeconds / 3600 / 24); // 小時
int seconds = (int) (diffInSeconds % 60); // 剩下的秒數
int hoursByDay = (int) (diffInSeconds / 3600 % 24); // 小時
int day = (int) (diffInSeconds / 3600 / 24); // 小時
getLogs().info("hours:" + hours + " minutes:" + minutes + " seconds:" + seconds);
DevElectricController devElectricController = new DevElectricController(getHandlerMain());
DevDigitalDisplayController devDigitalDisplayController = new DevDigitalDisplayController(getHandlerMain());
devElectricController.devSwitch(slot-1, hours, minutes+2, seconds);
devElectricController.devSwitch(slot - 1, hours, minutes + 2, seconds);
devDigitalDisplayController.devPut(slot, hours, minutes, seconds);
getHandlerMain().getHandler().postDelayed(new Runnable() {
@ -1127,7 +1216,7 @@ public class DispatchDialog extends DialogAbstract {
getHandlerMain().start(getClass().getSimpleName(), Option.NEXT_PROCESS_GOODS.getOption(), "next process goods");
}
public void ReSetAPPDispatch(Context context){
public void ReSetAPPDispatch(Context context) {
MyApp.getInstance().getBuyList().clear();
if (context == null) {
getLogs().info("ReSetAPP -- Context 為 null無法重啟");
@ -1135,7 +1224,8 @@ public class DispatchDialog extends DialogAbstract {
}
Intent intent = new Intent(context, FontendActivity.class);
// 清空所有 Task重新啟動
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.addFlags(
Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
context.startActivity(intent);
// 建議搭配 finishAffinity()確保所有舊的 Activity 都被清掉
@ -1148,23 +1238,26 @@ public class DispatchDialog extends DialogAbstract {
public void setButtonOkText(long countdown) {
if (countdown <= 0) {
binding.buttonOk.setText(getContext().getString(R.string.pickup_confirmation));//取貨確認
binding.buttonOk.setText(getContext().getString(R.string.pickup_confirmation));// 取貨確認
} else {
binding.buttonOk.setText(getContext().getString(R.string.pickup_confirmation)+"(" + countdown + ")");
binding.buttonOk.setText(getContext().getString(R.string.pickup_confirmation) + "(" + countdown + ")");
}
}
public void setTextViewCountdown(long countdown){
public void setTextViewCountdown(long countdown) {
if (countdown <= 0) {
setTextViewCountdown(getCtx().getString(R.string.pick_up_goods_within_15_seconds_after_pick_up_port_opened), 65);//取貨口開啟15秒內取貨
setTextViewCountdown(getCtx().getString(R.string.pick_up_goods_within_15_seconds_after_pick_up_port_opened),
65);// 取貨口開啟15秒內取貨
} else {
setTextViewCountdown(getCtx().getString(R.string.pickup_port_open)+"(" + countdown + ")"+getCtx().getString(R.string.pick_up_within), 65);//取貨口開啟,秒內取貨
setTextViewCountdown(getCtx().getString(R.string.pickup_port_open) + "(" + countdown + ")"
+ getCtx().getString(R.string.pick_up_within), 65);// 取貨口開啟,秒內取貨
}
}
public void setTextViewSmall(String message) {
setTextViewCountdown(message, 60);
}
public void setTextViewCountdown(String message, int size) {
if (isChangeStatus) {
binding.dispatchtips.setText(message);
@ -1189,7 +1282,8 @@ public class DispatchDialog extends DialogAbstract {
@Override
protected void execute(long countdown, HandlerMain handlerMain) {
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(), String.valueOf(countdown));
handlerMain.start(getClass().getSimpleName(), Option.SET_COUNTDOWN_TEXT.getOption(),
String.valueOf(countdown));
}
@Override
@ -1200,14 +1294,14 @@ public class DispatchDialog extends DialogAbstract {
public void insertDrgnoToMemo() {
try {
/*刷藥單解碼後進來的json資料*/
/* 刷藥單解碼後進來的json資料 */
JSONObject jsonObject = new JSONObject(dispatchList.get(0).getProduct().getMarketingPlan());
// **要新增的 ITEM 資料**
JSONObject newItem = new JSONObject();
newItem.put("SN", jsonObject.optString("SN")); // **領藥人藥單序號**
newItem.put("DRGNO", jsonObject.optString("DRGNO")); // **領藥人醫令**
newItem.put("SN", jsonObject.optString("SN")); // **領藥人藥單序號**
newItem.put("DRGNO", jsonObject.optString("DRGNO")); // **領藥人醫令**
/*從Memo的JSON資料中讀取SN及DRGNO的全部資料*/
/* 從Memo的JSON資料中讀取SN及DRGNO的全部資料 */
String jsonStr = MyApp.getInstance().getMemoMap().get("CMUH").get(0).getData().toString();
JSONObject jsonObject2 = new JSONObject(jsonStr);
JSONArray itemArray = jsonObject2.getJSONArray("ITEM");
@ -1216,9 +1310,10 @@ public class DispatchDialog extends DialogAbstract {
boolean exists = false;
for (int i = 0; i < itemArray.length(); i++) {
JSONObject item = itemArray.getJSONObject(i);
if (item.getString("SN").equals(newItem.getString("SN")) && item.getString("DRGNO").equals(newItem.getString("DRGNO"))) {
if (item.getString("SN").equals(newItem.getString("SN"))
&& item.getString("DRGNO").equals(newItem.getString("DRGNO"))) {
exists = true;
break; // **如果已存在就結束迴圈**
break; // **如果已存在就結束迴圈**
}
}