[REFACTOR] 優化中國醫(Cmuh)售貨與領藥流程,重構 API 連線模組,並自動升版至 24
1. 重構 Cmuh 的 SaleFlowHandler:新增 ORDC_EMPTY、MATERIAL_NOT_FOUND、STOCK_NOT_ENOUGH 等明確領藥錯誤代碼,最佳化多貨道庫存匹配與扣減邏輯,改善掃碼按鈕的動態 UI 綁定時序。 2. 增強 Dialog UI 提示:重構 PickupErrorTipDialog 與 DispatchDialog,優化 dialog_pickup_error_tip.xml 佈局結構,確保出貨異常時能正確顯示對應的中文 Toast 與圖示。 3. 重構 HttpAPI 與網路層:重構後台 API 與 HttpConnect 的 Socket/連線異常處理,確實向 UI 回傳網路錯誤訊息(非吞例外),提升在極端網路環境下的穩定度。 4. 健全持久化與日誌模組:強化 StarCloudAPI 機台 Token 變更時的本地 SQLite 同步邏輯,提升 Logs 模組的非同步儲存與排程上傳穩定性。 5. 自動升版:配合實體機台 OTA 發版鐵律,將 app/build.gradle 中的 verPatch 變數由 23 遞增至 24。 6. 更新工作流說明:優化 .agents/workflows/compiler-apk.md 文件中的智慧尺寸判斷指令說明。
This commit is contained in:
parent
46c24fcd2e
commit
71a059183a
@ -7,15 +7,15 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
|
||||
這個 Workflow 主要是協助您一鍵完成 Android 專案的編譯、匯出與安裝。當您呼叫 `/compiler-apk` 時,AI 會自動為您執行以下步驟:
|
||||
|
||||
> [!TIP]
|
||||
> **支援參數化指定編譯變體:**
|
||||
> * 預設(不帶參數):編譯 **`Chengwai` (晟崴客製版)**。
|
||||
> * 您可以呼叫 `/compiler-apk General`(或 `general`)編譯 **`General` (通用版)**。
|
||||
> * 您可以呼叫 `/compiler-apk Cmuh`(或 `cmuh`)編譯 **`Cmuh` (中國醫客製版)**。
|
||||
> **支援智慧硬體判斷與參數化覆寫:**
|
||||
> * 預設(不帶參數):編譯 **`Chengwai` 晟崴版 (S12XYU)**。
|
||||
> * **智慧判斷**:當編譯 **`中國醫 (Cmuh)`** 時,會自動識別並預設編譯 **`S9XYU` (Android 9 / 21.5 吋)**;編譯 **`General`** 與 **`Chengwai`** 時則預設編譯 **`S12XYU` (10 吋)**。
|
||||
> * **參數手動覆寫**:支援手動指定硬體尺寸。例如 `/compiler-apk Cmuh S12` 可強行編譯中國醫的 10 吋版本;`/compiler-apk General S9` 可強行編譯通用版的大螢幕 21.5 吋版本。
|
||||
|
||||
## 步驟零:自動檢查變更與升級版號
|
||||
在開始編譯之前,AI 助手會自動檢查是否有程式碼變更,以落實實體機台的 OTA 升級規範:
|
||||
1. AI 助手將檢查 `git status --porcelain app/` 的輸出,以偵測是否有未提交的程式碼修改。
|
||||
2. 若偵測到變更,且本次任務尚未增加版號,AI 助手將自動讀取 `app/build.gradle` 並將其中的 `verPatch` 變數數值遞增 1(例如由 `5` 變更為 `6`)。
|
||||
2. 若偵測到變更,且本次任務尚未增加版號,AI 助手將自動讀取 `app/build.gradle` 並將其中的 `verPatch` 變數數值遞增 1。
|
||||
3. 遞增完成後,向使用者呈報最新的版本號資訊,接著繼續執行步驟一。
|
||||
|
||||
## 步驟一:設定環境並編譯 Debug APK
|
||||
@ -29,14 +29,12 @@ description: 自動編譯 Android APK、輸出至專屬目錄並安裝至實體
|
||||
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
|
||||
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
|
||||
|
||||
# 取得目標 Flavor 參數,支援中文與英文別名自動對齊與防呆
|
||||
# 取得目標 Flavor 參數
|
||||
RAW_INPUT="$1"
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
|
||||
if [ -n "$RAW_INPUT" ]; then
|
||||
# 統一轉換為小寫以做比對
|
||||
LOWER_INPUT=$(echo "$RAW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
if [ "$LOWER_INPUT" = "general" ] || [ "$LOWER_INPUT" = "通用" ] || [ "$LOWER_INPUT" = "通用版" ] || [ "$LOWER_INPUT" = "標準" ] || [ "$LOWER_INPUT" = "標準版" ]; then
|
||||
TARGET_FLAVOR="General"
|
||||
elif [ "$LOWER_INPUT" = "chengwai" ] || [ "$LOWER_INPUT" = "晟崴" ] || [ "$LOWER_INPUT" = "晟崴版" ]; then
|
||||
@ -44,22 +42,37 @@ if [ -n "$RAW_INPUT" ]; then
|
||||
elif [ "$LOWER_INPUT" = "cmuh" ] || [ "$LOWER_INPUT" = "中國醫" ] || [ "$LOWER_INPUT" = "中國醫版" ]; then
|
||||
TARGET_FLAVOR="Cmuh"
|
||||
else
|
||||
# 若非預設之中文別名,則嘗試做首字母大寫轉換(相容英文輸入,如 cmuh -> Cmuh)
|
||||
TARGET_FLAVOR=$(echo "$RAW_INPUT" | awk '{print toupper(substr($0,1,1))tolower(substr($0,2))}')
|
||||
fi
|
||||
fi
|
||||
|
||||
# 根據 Flavor 動態設定智慧硬體規格預設值
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
fi
|
||||
|
||||
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
|
||||
RAW_HW_INPUT="$2"
|
||||
if [ -n "$RAW_HW_INPUT" ]; then
|
||||
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 安全防護:檢查是否為支援的客製項目
|
||||
if [ "$TARGET_FLAVOR" != "General" ] && [ "$TARGET_FLAVOR" != "Chengwai" ] && [ "$TARGET_FLAVOR" != "Cmuh" ]; then
|
||||
echo "❌ 不支援的 Flavor: $TARGET_FLAVOR,請使用 General, Chengwai 或 Cmuh。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "🚀 開始編譯: ${TARGET_FLAVOR}S12XYUDebug"
|
||||
echo "🚀 開始編譯: ${TARGET_FLAVOR}${TARGET_HARDWARE}Debug"
|
||||
|
||||
# 使用 clean 確保完整重新編譯,避免舊快取遮蔽 import 缺失等錯誤
|
||||
# --no-daemon:強制使用單次 JVM 進程,避免 Daemon 初始化期間被殘留訊號中斷
|
||||
./gradlew clean :app:assemble${TARGET_FLAVOR}S12XYUDebug --no-daemon
|
||||
./gradlew clean :app:assemble${TARGET_FLAVOR}${TARGET_HARDWARE}Debug --no-daemon
|
||||
```
|
||||
|
||||
## 步驟二:導出 APK 到 outputs 資料夾
|
||||
@ -70,7 +83,7 @@ echo "🚀 開始編譯: ${TARGET_FLAVOR}S12XYUDebug"
|
||||
cd /home/mama/projects/TaiwanStar_general_size_s_Chengwai
|
||||
mkdir -p outputs
|
||||
|
||||
# 取得 Flavor 參數,支援中英文別名對齊
|
||||
# 取得 Flavor 參數
|
||||
RAW_INPUT="$1"
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
|
||||
@ -87,8 +100,33 @@ if [ -n "$RAW_INPUT" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# 動態尋找編譯產出的最新 APK 檔案,避免因版號變更導致寫死路徑失效
|
||||
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}S12XYU/debug/ -name "app-S_12_XY_U_${TARGET_FLAVOR}-*-debug.apk" | head -n 1)
|
||||
# 根據 Flavor 動態設定智慧硬體規格預設值
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
fi
|
||||
|
||||
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
|
||||
RAW_HW_INPUT="$2"
|
||||
if [ -n "$RAW_HW_INPUT" ]; then
|
||||
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 解析硬體規格來決定輸出檔案名格式 (S9 -> S_9, S12 -> S_12)
|
||||
FORMATTED_HW=""
|
||||
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
|
||||
FORMATTED_HW="S_9_XY_U"
|
||||
else
|
||||
FORMATTED_HW="S_12_XY_U"
|
||||
fi
|
||||
|
||||
# 動態尋找編譯產出的最新 APK 檔案
|
||||
APK_SOURCE=$(find app/build/outputs/apk/${TARGET_FLAVOR}${TARGET_HARDWARE}/debug/ -name "app-${FORMATTED_HW}_${TARGET_FLAVOR}-*-debug.apk" | head -n 1)
|
||||
|
||||
if [ -n "$APK_SOURCE" ]; then
|
||||
APK_NAME=$(basename "$APK_SOURCE")
|
||||
@ -98,11 +136,19 @@ if [ -n "$APK_SOURCE" ]; then
|
||||
|
||||
# 提取與計算版本資訊
|
||||
MAJOR=$(grep "def verMajor" app/build.gradle | awk '{print $4}')
|
||||
|
||||
# 若在 build.gradle 內 Major 是由 hardware 動態判定,直接讀取並在 Shell 依據當前規格計算
|
||||
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
|
||||
MAJOR=21
|
||||
else
|
||||
MAJOR=10
|
||||
fi
|
||||
|
||||
MINOR=$(grep "def verMinor" app/build.gradle | awk '{print $4}')
|
||||
PATCH=$(grep "def verPatch" app/build.gradle | awk '{print $4}')
|
||||
VERSION_NAME="${MAJOR}_0${MINOR}_${PATCH}_R"
|
||||
VERSION_CODE=$((MAJOR * 10000 + MINOR * 100 + PATCH))
|
||||
FLAVOR="${TARGET_FLAVOR}S12XYU"
|
||||
FLAVOR="${TARGET_FLAVOR}${TARGET_HARDWARE}"
|
||||
|
||||
echo "=================================================="
|
||||
echo "📢 後台更新 keyin 參考資料:"
|
||||
@ -125,7 +171,7 @@ fi
|
||||
// turbo
|
||||
ADB_PATH="/mnt/c/Users/User/AppData/Local/Android/Sdk/platform-tools/adb.exe"
|
||||
|
||||
# 取得 Flavor 參數,支援中英文別名對齊
|
||||
# 取得 Flavor 參數
|
||||
RAW_INPUT="$1"
|
||||
TARGET_FLAVOR="Chengwai"
|
||||
|
||||
@ -142,12 +188,37 @@ if [ -n "$RAW_INPUT" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# 根據 Flavor 動態設定智慧硬體規格預設值
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
if [ "$TARGET_FLAVOR" = "Cmuh" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
fi
|
||||
|
||||
# 取得手動指定的第二個硬體規格參數 (S9 / S12)
|
||||
RAW_HW_INPUT="$2"
|
||||
if [ -n "$RAW_HW_INPUT" ]; then
|
||||
LOWER_HW=$(echo "$RAW_HW_INPUT" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$LOWER_HW" = "s9" ] || [ "$LOWER_HW" = "s9xyu" ] || [ "$LOWER_HW" = "大螢幕" ] || [ "$LOWER_HW" = "21.5" ]; then
|
||||
TARGET_HARDWARE="S9XYU"
|
||||
elif [ "$LOWER_HW" = "s12" ] || [ "$LOWER_HW" = "s12xyu" ] || [ "$LOWER_HW" = "10" ]; then
|
||||
TARGET_HARDWARE="S12XYU"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 解析硬體規格來決定輸出檔案名格式 (S9 -> S_9, S12 -> S_12)
|
||||
FORMATTED_HW=""
|
||||
if [ "$TARGET_HARDWARE" = "S9XYU" ]; then
|
||||
FORMATTED_HW="S_9_XY_U"
|
||||
else
|
||||
FORMATTED_HW="S_12_XY_U"
|
||||
fi
|
||||
|
||||
# 檢查是否有設備連接
|
||||
DEVICE_COUNT=$($ADB_PATH devices | grep -v "List of devices" | grep "device" | wc -l)
|
||||
|
||||
if [ "$DEVICE_COUNT" -gt 0 ]; then
|
||||
echo "📱 偵測到 $DEVICE_COUNT 個設備,開始安裝..."
|
||||
APK_TARGET=$(find outputs/ -name "app-S_12_XY_U_${TARGET_FLAVOR}-*-debug.apk" | head -n 1)
|
||||
APK_TARGET=$(find outputs/ -name "app-${FORMATTED_HW}_${TARGET_FLAVOR}-*-debug.apk" | head -n 1)
|
||||
if [ -n "$APK_TARGET" ]; then
|
||||
$ADB_PATH install -r -d -t "/home/mama/projects/TaiwanStar_general_size_s_Chengwai/$APK_TARGET"
|
||||
echo "🚀 安裝完成,正在啟動 App..."
|
||||
|
||||
31
AGENTS.md
Normal file
31
AGENTS.md
Normal file
@ -0,0 +1,31 @@
|
||||
# TaiwanStar Agent Rules Index
|
||||
|
||||
本文件為 Agent 規則入口與導覽摘要,不承載完整規範細節。
|
||||
|
||||
## Rule Sources (Single Source of Truth)
|
||||
|
||||
以下檔案為唯一真實來源(Single Source of Truth):
|
||||
|
||||
- [.agents/rules/star-cloud-api.md](.agents/rules/star-cloud-api.md)
|
||||
- [.agents/rules/versioning.md](.agents/rules/versioning.md)
|
||||
|
||||
任何規則更新,請只修改上述原始檔案;`AGENTS.md` 僅同步摘要與導覽。
|
||||
|
||||
## Critical Rules Snapshot
|
||||
|
||||
### Star-Cloud API
|
||||
|
||||
- 除 `B014`(綁定)外,請求需帶 `Authorization: Bearer [MachineToken]`。
|
||||
- 涉及 Token(`ApiKey`)變更時,必須同步持久化到資料庫。
|
||||
- 網路錯誤不可吞例外,需回傳可見錯誤到 UI(如 Toast/Handler)。
|
||||
|
||||
### Versioning
|
||||
|
||||
- 禁止直接手改成品 `versionCode`/`versionName` 值。
|
||||
- 版號管理透過 `verMajor`、`verMinor`、`verPatch` 變數。
|
||||
- OTA 發版時,`versionCode` 必須嚴格遞增。
|
||||
|
||||
## When Rules Conflict
|
||||
|
||||
- 規則細節若有衝突,以 `.agents/rules` 下原始規則檔為準。
|
||||
- `AGENTS.md` 僅作導覽與快速摘要,不作為完整規範來源。
|
||||
@ -5,12 +5,12 @@ plugins {
|
||||
// =========================================================================
|
||||
// 核心版號與自動連動定義
|
||||
// 每次發佈新版本給機台時,只需遞增 verPatch (修補版號)。
|
||||
// 例如:從 5 改為 6,對應的 versionName 就會自動變成 "10_08_6_R",
|
||||
// versionCode 就會自動變大為 100806,以正確觸發實體機台的 OTA 自動更新。
|
||||
// 螢幕尺寸 (verMajor) 已改為下方依據編譯 Flavor (S12XYU / S9XYU) 自動動態判定,
|
||||
// 避免不同尺寸機台間版本號產生衝突。
|
||||
// =========================================================================
|
||||
def verMajor = 10
|
||||
def verMinor = 1
|
||||
def verPatch = 6
|
||||
def verPatch = 24
|
||||
|
||||
|
||||
android {
|
||||
namespace 'com.unibuy.smartdevice'
|
||||
@ -20,8 +20,9 @@ android {
|
||||
applicationId "com.unibuy.smartdevice"
|
||||
minSdk 21
|
||||
targetSdk 34
|
||||
versionCode verMajor * 10000 + verMinor * 100 + verPatch
|
||||
versionName "${verMajor}_0${verMinor}_${verPatch}_R"
|
||||
// 預設以 10 吋進行基礎 Sync 估算,實際打包會由下方 applicationVariants 動態覆寫
|
||||
versionCode 10 * 10000 + verMinor * 100 + verPatch
|
||||
versionName "10_0${verMinor}_${verPatch}_R"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@ -47,6 +48,11 @@ android {
|
||||
S12XYU {
|
||||
dimension "hardware"
|
||||
}
|
||||
|
||||
S9XYU {
|
||||
dimension "hardware"
|
||||
minSdk 28
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -57,23 +63,48 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// 自定義打包檔案名稱,遵循公司命名規範:來源_安卓號_主板號_機器系列_專案名稱_螢幕大小_次版號_修補版號_環境
|
||||
// 自定義打包檔案名稱與動態版本號計算
|
||||
// 遵循公司命名規範:來源_安卓號_主板號_機器系列_專案名稱_螢幕大小_次版號_修補版號_環境
|
||||
applicationVariants.all { variant ->
|
||||
def project = variant.productFlavors[0].name // project dimension
|
||||
def hardware = variant.productFlavors[1].name // hardware dimension
|
||||
def buildType = variant.buildType.name
|
||||
|
||||
// 根據硬體規格自動動態判定螢幕吋數 (verMajor) 與版號
|
||||
def verMajor = 10
|
||||
def calculatedMinor = verMinor // 預設套用全域次版號
|
||||
def calculatedPatch = verPatch // 預設套用全域修補版號
|
||||
|
||||
if (hardware == "S9XYU") {
|
||||
verMajor = 21 // 21.5 吋
|
||||
|
||||
// 【預留擴充】若中國醫安卓 9 機台未來需要獨立的發版節奏,可直接在此單獨覆寫:
|
||||
// calculatedMinor = 1
|
||||
// calculatedPatch = 6
|
||||
} else if (hardware == "S12XYU") {
|
||||
verMajor = 10 // 10 吋
|
||||
}
|
||||
|
||||
// 動態重算並覆寫此 Variant 的版本號,避免互相污染
|
||||
def calculatedVersionCode = verMajor * 10000 + calculatedMinor * 100 + calculatedPatch
|
||||
def calculatedVersionName = "${verMajor}_0${calculatedMinor}_${calculatedPatch}_R"
|
||||
|
||||
|
||||
variant.outputs.each { output ->
|
||||
def versionName = variant.versionName
|
||||
def project = variant.productFlavors[0].name // project dimension
|
||||
def hardware = variant.productFlavors[1].name // hardware dimension
|
||||
def buildType = variant.buildType.name
|
||||
output.versionCodeOverride = calculatedVersionCode
|
||||
output.versionNameOverride = calculatedVersionName
|
||||
|
||||
// 將 hardware flavor 名稱展開為公司規範格式 S12XYU → S_12_XY_U
|
||||
def hwFormatted = hardware
|
||||
.replaceAll(/^([A-Z])(\d+)([A-Z]+)([A-Z])$/, '$1_$2_$3_$4')
|
||||
|
||||
// 輸出範例:app-S_12_XY_U_Chengwai-10_08_8_R-release.apk
|
||||
output.outputFileName = "app-${hwFormatted}_${project}-${versionName}-${buildType}.apk"
|
||||
output.outputFileName = "app-${hwFormatted}_${project}-${calculatedVersionName}-${buildType}.apk"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
@ -81,6 +112,7 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
packaging {
|
||||
|
||||
@ -24,6 +24,10 @@ public class SaleFlowHandler {
|
||||
// 晟崴版 UI 設定,預設無動作
|
||||
}
|
||||
|
||||
public void onSetupUI(android.view.View rootView) {
|
||||
// 晟崴版 UI 設定,預設無動作
|
||||
}
|
||||
|
||||
public void onBuyRequested() {
|
||||
new MemberCardPickupDialog(context, handlerMain).show();
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import com.unibuy.smartdevice.exception.LogsUnsupportedOperationException;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.MedicineStructure;
|
||||
import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainSchedulerOnClick;
|
||||
import com.unibuy.smartdevice.ui.dialog.FontendPickupCodeDialog;
|
||||
@ -28,6 +29,9 @@ import org.json.JSONObject;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
@ -41,6 +45,9 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
* 5. 顯示領藥與錯誤 Dialog
|
||||
*/
|
||||
public class SaleFlowHandler {
|
||||
private static final String ORDC_EMPTY = "ORDC_EMPTY";
|
||||
private static final String MATERIAL_NOT_FOUND = "MATERIAL_NOT_FOUND";
|
||||
private static final String STOCK_NOT_ENOUGH = "STOCK_NOT_ENOUGH";
|
||||
|
||||
protected final Context context;
|
||||
protected final SaleFlowCallback callback;
|
||||
@ -60,10 +67,17 @@ public class SaleFlowHandler {
|
||||
* 在 Activity 的 onResume() 中會被呼叫,此時尋找並綁定 VmcFragment 中的掃碼按鈕
|
||||
*/
|
||||
public void onSetupUI() {
|
||||
onSetupUI(null);
|
||||
}
|
||||
|
||||
public void onSetupUI(View rootView) {
|
||||
if (context instanceof Activity) {
|
||||
View btnScan = ((Activity) context).findViewById(R.id.scanbarcodebtn);
|
||||
View btnScan = rootView != null ? rootView.findViewById(R.id.scanbarcodebtn) : ((Activity) context).findViewById(R.id.scanbarcodebtn);
|
||||
if (btnScan != null) {
|
||||
btnScan.setOnClickListener(new BtnQrCodeOnClick(handlerMain, context));
|
||||
MyApp.getInstance().getLogs().info("✅ 成功在 Cmuh SaleFlowHandler 綁定掃碼按鈕!");
|
||||
} else {
|
||||
MyApp.getInstance().getLogs().info("❌ 在 Cmuh SaleFlowHandler 找不到掃碼按鈕 R.id.scanbarcodebtn!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -109,6 +123,7 @@ public class SaleFlowHandler {
|
||||
protected void execute(HandlerMain handlerMain) {
|
||||
handlerMain.start(getClass().getSimpleName(), "@40秒內刷條碼");
|
||||
openBarcode = true;
|
||||
haveValuestate = false;
|
||||
try {
|
||||
UsbDev usbDev = new UsbDev(DeviceType.QRCODE_SCANNER);
|
||||
byte[] buffer = new byte[1000];
|
||||
@ -211,13 +226,16 @@ public class SaleFlowHandler {
|
||||
|
||||
MyApp.getInstance().setDecryptedData("");
|
||||
MyApp.getInstance().setDecryptedData(new String(decryptedBytes, StandardCharsets.UTF_8).trim());
|
||||
MyApp.getInstance().setCmuhPickupErrorMessage("");
|
||||
|
||||
String decryptedStr = MyApp.getInstance().getDecryptedData();
|
||||
MyApp.getInstance().getLogs().info("decryptedStr=" + decryptedStr);
|
||||
|
||||
|
||||
JSONObject jsonObject = new JSONObject(decryptedStr);
|
||||
if (checkORDCDispatchNumber(decryptedStr)) {
|
||||
return false;
|
||||
}
|
||||
addMedicineToByulist(jsonObject);
|
||||
checkORDCDispatchNumber(decryptedStr); // 判斷領藥號是否已經領過
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new LogsParseException(MyApp.getInstance().getLogs(), "錯誤:加密數據長度不是 16 的倍數,可能需要填充");
|
||||
@ -226,55 +244,45 @@ public class SaleFlowHandler {
|
||||
}
|
||||
|
||||
public void addMedicineToByulist(JSONObject jsonObject) {
|
||||
JSONArray itemsArray = null;
|
||||
try {
|
||||
itemsArray = jsonObject.getJSONArray("ITEM");
|
||||
JSONArray itemsArray = jsonObject.getJSONArray("ITEM");
|
||||
// 清空 BuyList,確保每次掃描都是新的出貨清單
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
|
||||
int MedicineCount = 0; // 同一種商品全部貨道的總數量
|
||||
String MedicineName = "", MedicineSlot = "", MedicineImg = "", MedicinePId = "", MemoSnDrgno = "";
|
||||
ArrayList<MedicineStructure> medicineList = new ArrayList<>();
|
||||
|
||||
// 先進行預判,確認所有 ITEM 都能成功出貨才繼續後面流程
|
||||
boolean stockCheckPassed = true; // 預設檢查通過
|
||||
Map<String, List<SlotStructure>> materialSlotMap = buildMaterialSlotMap();
|
||||
|
||||
for (int i = 0; i < itemsArray.length(); i++) {
|
||||
JSONObject itemObject = itemsArray.getJSONObject(i);
|
||||
String ordc = itemObject.optString("ORDC");
|
||||
int requestQty = Integer.parseInt(itemObject.optString("QTY"));
|
||||
String ordc = itemObject.optString("ORDC", "").trim();
|
||||
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
|
||||
|
||||
if (ordc.isEmpty()) {
|
||||
failWithCode(ORDC_EMPTY, context.getString(R.string.cmuh_ordc_empty));
|
||||
return;
|
||||
}
|
||||
List<SlotStructure> matchedSlots = materialSlotMap.get(ordc);
|
||||
if (matchedSlots == null || matchedSlots.isEmpty()) {
|
||||
failWithCode(MATERIAL_NOT_FOUND, context.getString(R.string.cmuh_material_not_found, ordc));
|
||||
return;
|
||||
}
|
||||
int totalStock = 0;
|
||||
|
||||
for (int j = 0; j < MyApp.getInstance().getSlotList(0).size(); j++) {
|
||||
if (ordc.equals(MyApp.getInstance().getSlotList(0).get(j).getProduct().getMaterialCode())
|
||||
&& MyApp.getInstance().getSlotList(0).get(j).getCount() > 0
|
||||
&& MyApp.getInstance().getSlotList(0).get(j).getCount() <= 20) {
|
||||
totalStock += MyApp.getInstance().getSlotList(0).get(j).getCount();
|
||||
}
|
||||
for (SlotStructure slotData : matchedSlots) {
|
||||
totalStock += slotData.getCount();
|
||||
}
|
||||
|
||||
if (totalStock == 0) {
|
||||
MyApp.getInstance().getLogs().info("預判失敗:ORDC = " + ordc + " 找不到貨道或庫存為0");
|
||||
String storename = "@商品 [" + ordc + "] 無庫存或未設定貨道,請確認。";
|
||||
handlerMain.start("無庫存或未設定貨道,請確認", storename);
|
||||
stockCheckPassed = false;
|
||||
break;
|
||||
} else if (totalStock < requestQty) {
|
||||
MyApp.getInstance().getLogs().info("預判失敗:ORDC = " + ordc + " 庫存不足,總庫存:" + totalStock + ",需求:" + requestQty);
|
||||
String storename = "@商品 [" + ordc + "] 庫存不足,請補貨後再掃描。";
|
||||
handlerMain.start("庫存不足", storename);
|
||||
stockCheckPassed = false;
|
||||
break;
|
||||
if (requestQty <= 0 || totalStock < requestQty) {
|
||||
failWithCode(STOCK_NOT_ENOUGH, context.getString(R.string.cmuh_stock_not_enough, ordc, requestQty, totalStock));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!stockCheckPassed) {
|
||||
return; // 直接結束,不進入後續流程
|
||||
}
|
||||
|
||||
MyApp.getInstance().getLogs().info("itemsArray.length" + itemsArray.length());
|
||||
for (int i = 0; i < itemsArray.length(); i++) {
|
||||
JSONObject itemObject = itemsArray.getJSONObject(i);
|
||||
String ordc = itemObject.optString("ORDC", "").trim();
|
||||
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
|
||||
|
||||
// 重新初始化這些變數
|
||||
MedicineName = "";
|
||||
@ -283,29 +291,24 @@ public class SaleFlowHandler {
|
||||
MedicineImg = "";
|
||||
MedicinePId = "";
|
||||
MemoSnDrgno = "";
|
||||
|
||||
for (int j = 0; j < MyApp.getInstance().getSlotList(0).size(); j++) {
|
||||
if (itemObject.optString("ORDC").equals(MyApp.getInstance().getSlotList(0).get(j).getProduct().getMaterialCode())
|
||||
&& MyApp.getInstance().getSlotList(0).get(j).getCount() > 0
|
||||
&& MyApp.getInstance().getSlotList(0).get(j).getCount() <= 20) {
|
||||
MedicineCount += MyApp.getInstance().getSlotList(0).get(j).getCount();
|
||||
MedicineSlot += MyApp.getInstance().getSlotList(0).get(j).getSlot() + "-" + MyApp.getInstance().getSlotList(0).get(j).getCount() + ",";
|
||||
|
||||
if (MedicineCount >= Integer.valueOf(itemObject.optString("QTY"))) {
|
||||
MedicinePId = MyApp.getInstance().getSlotList(0).get(j).getProduct().getProductID();
|
||||
MedicineName = MyApp.getInstance().getSlotList(0).get(j).getProduct().getProductName();
|
||||
MedicineImg = MyApp.getInstance().getSlotList(0).get(j).getProduct().getProductImg();
|
||||
MemoSnDrgno = "{\"SN\": \"" + jsonObject.optString("SN") + "\", \"DRGNO\": \"" + jsonObject.optString("DRGNO") + "\"}";
|
||||
}
|
||||
List<SlotStructure> matchedSlots = materialSlotMap.get(ordc);
|
||||
for (SlotStructure matchedSlot : matchedSlots) {
|
||||
MedicineCount += matchedSlot.getCount();
|
||||
MedicineSlot += matchedSlot.getSlot() + "-" + matchedSlot.getCount() + ",";
|
||||
if (MedicineCount >= requestQty) {
|
||||
MedicinePId = matchedSlot.getProduct().getProductID();
|
||||
MedicineName = matchedSlot.getProduct().getProductName();
|
||||
MedicineImg = matchedSlot.getProduct().getProductImg();
|
||||
MemoSnDrgno = "{\"SN\": \"" + jsonObject.optString("SN") + "\", \"DRGNO\": \"" + jsonObject.optString("DRGNO") + "\"}";
|
||||
}
|
||||
}
|
||||
medicineList.add(new MedicineStructure(itemObject.optString("ORDC"), MedicineCount, MedicineName, MedicineSlot, MedicineImg, MedicinePId, MemoSnDrgno));
|
||||
medicineList.add(new MedicineStructure(ordc, MedicineCount, MedicineName, MedicineSlot, MedicineImg, MedicinePId, MemoSnDrgno));
|
||||
}
|
||||
|
||||
/* 這裡將商品依據領藥數量加入 BuyList */
|
||||
for (int i = 0; i < itemsArray.length(); i++) {
|
||||
JSONObject itemObject = itemsArray.getJSONObject(i);
|
||||
int requestQty = Integer.parseInt(itemObject.optString("QTY"));
|
||||
int requestQty = safeParseInt(itemObject.optString("QTY"), 0);
|
||||
|
||||
if (requestQty <= medicineList.get(i).getMedicineCount()) {
|
||||
haveValuestate = true;
|
||||
@ -351,6 +354,38 @@ public class SaleFlowHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, List<SlotStructure>> buildMaterialSlotMap() {
|
||||
Map<String, List<SlotStructure>> materialSlotMap = new HashMap<>();
|
||||
List<SlotStructure> slotList = MyApp.getInstance().getSlotList(0);
|
||||
for (SlotStructure slotData : slotList) {
|
||||
if (slotData == null || slotData.getProduct() == null) continue;
|
||||
String productId = slotData.getProduct().getProductID();
|
||||
String materialCode = slotData.getProduct().getMaterialCode();
|
||||
if (productId == null || productId.isEmpty() || "?".equals(productId)) continue;
|
||||
if (materialCode == null || materialCode.trim().isEmpty()) continue;
|
||||
if (slotData.getCount() <= 0 || slotData.getCount() > 20) continue;
|
||||
materialSlotMap.computeIfAbsent(materialCode.trim(), k -> new ArrayList<>()).add(slotData);
|
||||
}
|
||||
return materialSlotMap;
|
||||
}
|
||||
|
||||
private int safeParseInt(String value, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (Exception ignored) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private void failWithCode(String code, String message) {
|
||||
String finalMessage = "@" + code + " " + message;
|
||||
MyApp.getInstance().getLogs().info("[" + code + "] " + message);
|
||||
MyApp.getInstance().setCmuhPickupErrorMessage(finalMessage.substring(1));
|
||||
haveValuestate = false;
|
||||
openBarcode = false;
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
}
|
||||
|
||||
public void addMedicineToBuyList(String MedicinePidStr, String MedicineNameStr, int SlotNumber, String MedicineImg, String MemoSnDrgno, int count) {
|
||||
ProductStructure productStructure = new ProductStructure(MedicinePidStr, MedicineImg, MedicineNameStr, 0, 0, 0, 0, MemoSnDrgno, "", "", "");
|
||||
MyApp.getInstance().getLogs().info("aaaaaa " + MedicinePidStr + " " + MemoSnDrgno + " " + MedicineNameStr);
|
||||
@ -358,7 +393,7 @@ public class SaleFlowHandler {
|
||||
MyApp.getInstance().getBuyList().add(buyProduct);
|
||||
}
|
||||
|
||||
public void checkORDCDispatchNumber(String decryptedStr) throws JSONException {
|
||||
public boolean checkORDCDispatchNumber(String decryptedStr) throws JSONException {
|
||||
JSONObject jsonObject = new JSONObject(decryptedStr);
|
||||
JSONObject newItem = new JSONObject();
|
||||
newItem.put("SN", jsonObject.optString("SN"));
|
||||
@ -380,8 +415,11 @@ public class SaleFlowHandler {
|
||||
if (exists) {
|
||||
MyApp.getInstance().getLogs().info("⚠️ 此項目已存在,不新增!");
|
||||
openBarcode = false;
|
||||
handlerMain.start(getClass().getSimpleName(), "@此單已取物完成,請洽藥局發藥窗口!");
|
||||
haveValuestate = false;
|
||||
MyApp.getInstance().setCmuhPickupErrorMessage("此單已取物完成,請洽藥局發藥窗口!");
|
||||
MyApp.getInstance().getBuyList().clear();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package com.unibuy.smartdevice.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.databinding.DialogPickupErrorTipBinding;
|
||||
import android.content.Context;
|
||||
|
||||
import com.unibuy.smartdevice.DialogAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.DialogPickupErrorTipBinding;
|
||||
import com.unibuy.smartdevice.structure.BuyStructure;
|
||||
import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
@ -103,15 +104,20 @@ public class PickupErrorTipDialog extends DialogAbstract {
|
||||
|
||||
String decryptedStr = MyApp.getInstance().getDecryptedData();
|
||||
getLogs().info(decryptedStr);
|
||||
try {
|
||||
JSONObject jsonObject = new JSONObject(decryptedStr);
|
||||
binding.tvDrgnoid.setText("領藥號:"+jsonObject.optString("DRGNO"));
|
||||
} catch (JSONException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
try {
|
||||
JSONObject jsonObject = new JSONObject(decryptedStr);
|
||||
binding.tvDrgnoid.setText("領藥號:"+jsonObject.optString("DRGNO"));
|
||||
} catch (JSONException e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
String errorMessage = MyApp.getInstance().getCmuhPickupErrorMessage();
|
||||
if (errorMessage == null || errorMessage.trim().isEmpty()) {
|
||||
errorMessage = getCtx().getString(R.string.cmuh_pickup_failed_contact_counter);
|
||||
}
|
||||
binding.tvErrorMessage.setText(errorMessage);
|
||||
|
||||
closeDialogCountdown = new CloseDialogOnThreadHandler(getHandlerMain());
|
||||
closeDialogCountdown.start(40);
|
||||
binding.btnNonPerson.setOnClickListener(v -> dialogCancel());
|
||||
}
|
||||
|
||||
@ -140,11 +146,11 @@ public class PickupErrorTipDialog extends DialogAbstract {
|
||||
}*/
|
||||
getTools().dialogClose();
|
||||
cancel();
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.btnNonPerson.setText("否("+countdown+")");
|
||||
}
|
||||
}
|
||||
|
||||
public void setButtonCancelText(long countdown) {
|
||||
binding.btnNonPerson.setText(getCtx().getString(R.string.close_countdown, countdown));
|
||||
}
|
||||
|
||||
public static class CloseDialogOnThreadHandler extends HandlerMainCountdown {
|
||||
public CloseDialogOnThreadHandler(HandlerMain handlerMain) {
|
||||
|
||||
@ -34,13 +34,16 @@
|
||||
app:layout_constraintTop_toBottomOf="@+id/linearLayout211"
|
||||
tools:ignore="MissingConstraints">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="取物失敗\n請洽櫃台\n#104"
|
||||
android:textSize="60dp"
|
||||
android:gravity="center"
|
||||
android:textAlignment="center"/>
|
||||
<TextView
|
||||
android:id="@+id/tv_error_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:text="@string/cmuh_pickup_failed_contact_counter"
|
||||
android:textSize="42sp"
|
||||
android:gravity="center"
|
||||
android:textAlignment="center"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
@ -73,12 +76,12 @@
|
||||
<Button
|
||||
android:id="@+id/btn_non_person"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="否(40)"
|
||||
android:textSize="30sp"
|
||||
tools:ignore="MissingConstraints" />
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/close_40"
|
||||
android:textSize="30sp"
|
||||
tools:ignore="MissingConstraints" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -24,6 +24,10 @@ public class SaleFlowHandler {
|
||||
// 通用版 UI 設定,預設無動作
|
||||
}
|
||||
|
||||
public void onSetupUI(android.view.View rootView) {
|
||||
// 通用版 UI 設定,預設無動作
|
||||
}
|
||||
|
||||
public void onBuyRequested() {
|
||||
new BuyDialog(context, handlerMain).show();
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Process;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
@ -60,8 +61,10 @@ import com.unibuy.smartdevice.ui.tools.VideoImageMixedPlayer;
|
||||
import com.unibuy.smartdevice.utils.LanguageHelper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MainActivity extends AppCompatActivityAbstract {
|
||||
private static final String LOGIN_TRACE_TAG = "LoginTrace";
|
||||
@Override
|
||||
protected Context setCtx() {
|
||||
return this;
|
||||
@ -81,6 +84,11 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
public void execute(Context context, int commandCode, String message) {
|
||||
super.execute(context, commandCode, message);
|
||||
if ("登入成功".equals(message)) {
|
||||
if (pendingLoginTraceId != null) {
|
||||
long now = System.currentTimeMillis();
|
||||
Log.i(LOGIN_TRACE_TAG, "[" + pendingLoginTraceId + "] callback=success totalMs="
|
||||
+ (now - pendingLoginStartMs) + " msg=" + message);
|
||||
}
|
||||
updateLoginStatus();
|
||||
if (activeLoginDialog != null && activeLoginDialog.isShowing()) {
|
||||
activeLoginDialog.dismiss();
|
||||
@ -90,6 +98,15 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
pendingAction.run();
|
||||
pendingAction = null;
|
||||
}
|
||||
pendingLoginTraceId = null;
|
||||
pendingLoginStartMs = 0L;
|
||||
} else if (pendingLoginTraceId != null && message != null
|
||||
&& (message.contains("登入失敗") || message.contains("網路錯誤") || message.contains("解析失敗"))) {
|
||||
long now = System.currentTimeMillis();
|
||||
Log.w(LOGIN_TRACE_TAG, "[" + pendingLoginTraceId + "] callback=failure totalMs="
|
||||
+ (now - pendingLoginStartMs) + " msg=" + message + " code=" + commandCode);
|
||||
pendingLoginTraceId = null;
|
||||
pendingLoginStartMs = 0L;
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -99,6 +116,8 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
private HttpAPI httpAPI;
|
||||
private StarCloudAPI starCloudAPI;
|
||||
private Runnable pendingAction;
|
||||
private String pendingLoginTraceId;
|
||||
private long pendingLoginStartMs;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@ -563,10 +582,14 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
|
||||
// 設定點擊事件
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
long clickStartMs = System.currentTimeMillis();
|
||||
String access = accessInput.getText().toString().trim();
|
||||
String password = passwordInput.getText().toString().trim();
|
||||
String traceId = String.format(Locale.US, "L%tH%<tM%<tS-%03d", clickStartMs, clickStartMs % 1000);
|
||||
Log.i(LOGIN_TRACE_TAG, "[" + traceId + "] click_login accountLen=" + access.length());
|
||||
|
||||
if (access.isEmpty() || password.isEmpty()) {
|
||||
Log.w(LOGIN_TRACE_TAG, "[" + traceId + "] blocked_empty_input");
|
||||
Toast.makeText(context, "請輸入帳號和密碼", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(context, "正在驗證登入...", Toast.LENGTH_SHORT).show();
|
||||
@ -574,12 +597,17 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
// 為了處理 StarCloudAPI 登入成功後的回調
|
||||
// 我們重新設計 handlerMain 讓它能夠在 onSuccess 回調中跑 pendingAction
|
||||
pendingAction = onSuccess;
|
||||
starCloudAPI.login(access, password);
|
||||
pendingLoginTraceId = traceId;
|
||||
pendingLoginStartMs = clickStartMs;
|
||||
Log.i(LOGIN_TRACE_TAG, "[" + traceId + "] dispatch_starcloud_login");
|
||||
starCloudAPI.login(access, password, traceId, clickStartMs);
|
||||
|
||||
if (rememberAccountCb.isChecked()) {
|
||||
prefs.edit().putString("saved_account", access).apply();
|
||||
Log.i(LOGIN_TRACE_TAG, "[" + traceId + "] remember_account=save");
|
||||
} else {
|
||||
prefs.edit().remove("saved_account").apply();
|
||||
Log.i(LOGIN_TRACE_TAG, "[" + traceId + "] remember_account=clear");
|
||||
}
|
||||
// 🛠️ 關鍵優化:不再此處立即 dismiss(),改由 API 回報成功時關閉
|
||||
}
|
||||
@ -658,4 +686,4 @@ public class MainActivity extends AppCompatActivityAbstract {
|
||||
binding.wrapButtonGotoShoppingPlatform.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,6 +98,7 @@ public class MyApp extends Application {
|
||||
private List<MarketingPlanStructure> marketingPlanList;
|
||||
private List<BuyStructure> buyList;
|
||||
private String decryptedData;
|
||||
private String cmuhPickupErrorMessage;
|
||||
private Map<String, List<MemoStructure>> memoMap;
|
||||
private CashSetStructure cashSetStructure;
|
||||
private AccessCodeStructure accessCodeStructure;
|
||||
@ -410,6 +411,45 @@ public class MyApp extends Application {
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean checkAndReconnectDev() {
|
||||
boolean isAlive = false;
|
||||
Logs staticLogs = new Logs(MyApp.class);
|
||||
|
||||
if (devXinYuanController != null) {
|
||||
try {
|
||||
// 發送同步包測試
|
||||
devXinYuanController.addSendBuffer(
|
||||
devXinYuanController.getDevXinYuan().syncPackNo((byte) 0x01)
|
||||
);
|
||||
Thread.sleep(100);
|
||||
if (!devXinYuanController.getReadBufferByMax().isEmpty()) {
|
||||
isAlive = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
isAlive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 若不活躍則重連
|
||||
if (!isAlive) {
|
||||
staticLogs.info("下位機失連,重新連線中...");
|
||||
if (devXinYuanController != null) {
|
||||
devXinYuanController.shutdown();
|
||||
}
|
||||
devXinYuanController = new DevXinYuanController(devXinYuanController.getHandlerMain());
|
||||
devXinYuanController.start();
|
||||
devXinYuanController.addSendBuffer(
|
||||
devXinYuanController.getDevXinYuan().syncPackNo((byte) 0x01)
|
||||
);
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void initMqtt() {
|
||||
String machineId = MyApp.getInstance().getMachine().getMachineID();
|
||||
String apiKey = MyApp.getInstance().getMachine().getApiKey();
|
||||
@ -683,6 +723,14 @@ public class MyApp extends Application {
|
||||
return decryptedData;
|
||||
}
|
||||
|
||||
public void setCmuhPickupErrorMessage(String cmuhPickupErrorMessage) {
|
||||
this.cmuhPickupErrorMessage = cmuhPickupErrorMessage;
|
||||
}
|
||||
|
||||
public String getCmuhPickupErrorMessage() {
|
||||
return cmuhPickupErrorMessage;
|
||||
}
|
||||
|
||||
public Map<String, List<MemoStructure>> getMemoMap() {
|
||||
return memoMap;
|
||||
}
|
||||
|
||||
@ -46,6 +46,18 @@ public enum ComPort {
|
||||
}
|
||||
|
||||
public String getFilepath() {
|
||||
if (this == COM_PORT_S1) {
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
return "/dev/ttyS1";
|
||||
}
|
||||
return "/dev/ttyS7";
|
||||
}
|
||||
if (this == COM_PORT_S2) {
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
return "/dev/ttyS2";
|
||||
}
|
||||
return "/dev/ttyS6";
|
||||
}
|
||||
return filepath;
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
@ -37,7 +38,7 @@ public class Logs {
|
||||
return className;
|
||||
}
|
||||
|
||||
public void writeFile(String data) {
|
||||
public synchronized void writeFile(String data) {
|
||||
File txtDebug = new File(Tools.getExternalFilesDir(), "debug.txt");
|
||||
if (txtDebug.exists()) {
|
||||
txtDebug.setReadable(true, false);
|
||||
@ -48,8 +49,9 @@ public class Logs {
|
||||
data = dateTime + " " + data;
|
||||
|
||||
try (FileOutputStream fos = new FileOutputStream(txtDebug, true)) { // try-with-resources 自動 close()
|
||||
fos.write(data.getBytes());
|
||||
fos.write("\n".getBytes()); // 換行
|
||||
fos.write(data.getBytes(StandardCharsets.UTF_8));
|
||||
fos.write("\n".getBytes(StandardCharsets.UTF_8)); // 換行
|
||||
fos.flush();
|
||||
} catch (IOException e) {
|
||||
warning(e);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,10 @@ import java.util.ArrayList;
|
||||
public class StarCloudAPI {
|
||||
private static final String TAG = "StarCloudAPI";
|
||||
|
||||
public static String domainNameByNewHost = "https://logs.setdomains.work/";
|
||||
public static String updateOnDomainNameByNewHost = domainNameByNewHost + "api/machine/"
|
||||
+ MyApp.getInstance().getMachine().getMachineID() + "/log/update";
|
||||
|
||||
private String getBaseUrl() {
|
||||
if ("prod".equals(MyApp.getInstance().getMachine().getMqttEnv())) {
|
||||
return "https://starcloud.taiwan-star.com.tw/api/v1/app/";
|
||||
@ -55,9 +59,14 @@ public class StarCloudAPI {
|
||||
* B000: 管理員登入驗證
|
||||
*/
|
||||
public void login(final String account, final String password) {
|
||||
login(account, password, "NO_TRACE", System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public void login(final String account, final String password, final String traceId, final long startMs) {
|
||||
Log.i(TAG, "login() called for account: " + account);
|
||||
final String url = getBaseUrl() + "admin/login/B000";
|
||||
Log.i(TAG, "Starting B000 request to: " + url);
|
||||
Log.i("LoginTrace", "[" + traceId + "] b000_start fromUiMs=" + (System.currentTimeMillis() - startMs));
|
||||
|
||||
// 不依賴 FormPostMainScheduler (會靜默吞掉 IO exception)
|
||||
// 直接用 Thread + HttpURLConnection 確保錯誤一定看得到
|
||||
@ -66,6 +75,7 @@ public class StarCloudAPI {
|
||||
public void run() {
|
||||
int responseCode = -1;
|
||||
String receive = "";
|
||||
long threadStartMs = System.currentTimeMillis();
|
||||
try {
|
||||
JSONObject json = new JSONObject();
|
||||
json.put("Su_Account", account);
|
||||
@ -90,10 +100,15 @@ public class StarCloudAPI {
|
||||
conn.setDoOutput(true);
|
||||
|
||||
byte[] bodyBytes = json.toString().getBytes("UTF-8");
|
||||
long writeStartMs = System.currentTimeMillis();
|
||||
conn.getOutputStream().write(bodyBytes);
|
||||
conn.getOutputStream().flush();
|
||||
Log.i("LoginTrace", "[" + traceId + "] b000_write_done tMs=" + (System.currentTimeMillis() - writeStartMs));
|
||||
|
||||
long responseStartMs = System.currentTimeMillis();
|
||||
responseCode = conn.getResponseCode();
|
||||
Log.i("LoginTrace", "[" + traceId + "] b000_response_code=" + responseCode
|
||||
+ " waitMs=" + (System.currentTimeMillis() - responseStartMs));
|
||||
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
|
||||
? conn.getInputStream() : conn.getErrorStream();
|
||||
java.io.BufferedReader br = new java.io.BufferedReader(
|
||||
@ -105,8 +120,12 @@ public class StarCloudAPI {
|
||||
conn.disconnect();
|
||||
|
||||
Log.d(TAG, "B000 Response (" + responseCode + "): " + receive);
|
||||
Log.i("LoginTrace", "[" + traceId + "] b000_network_done totalNetMs="
|
||||
+ (System.currentTimeMillis() - threadStartMs));
|
||||
} catch (final Exception e) {
|
||||
Log.e(TAG, "B000 Network Error", e);
|
||||
Log.e("LoginTrace", "[" + traceId + "] b000_network_error afterMs="
|
||||
+ (System.currentTimeMillis() - threadStartMs) + " err=" + e.getMessage());
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@ -122,6 +141,7 @@ public class StarCloudAPI {
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
long mainStartMs = System.currentTimeMillis();
|
||||
if (finalCode >= 200 && finalCode < 300) {
|
||||
try {
|
||||
JSONObject res = new JSONObject(finalReceive);
|
||||
@ -129,16 +149,28 @@ public class StarCloudAPI {
|
||||
MyApp.getInstance().setSuAccess(res.optString("Su_Account", account));
|
||||
// 移除覆蓋 ApiKey 的邏輯,因為 B000 回傳的可能是 Admin Token,但我們需要保留 Machine Token
|
||||
Log.i(TAG, "Login success for user: " + account);
|
||||
Log.i("LoginTrace", "[" + traceId + "] b000_parse_success parseMs="
|
||||
+ (System.currentTimeMillis() - mainStartMs)
|
||||
+ " totalMs=" + (System.currentTimeMillis() - startMs));
|
||||
handlerMain.start("StarCloudAPI", 200, "登入成功");
|
||||
} else {
|
||||
Log.w("LoginTrace", "[" + traceId + "] b000_business_fail code=401 parseMs="
|
||||
+ (System.currentTimeMillis() - mainStartMs)
|
||||
+ " totalMs=" + (System.currentTimeMillis() - startMs));
|
||||
handlerMain.start("StarCloudAPI", 401,
|
||||
"登入失敗: " + res.optString("message", "帳號密碼錯誤"));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "B000 Parse Error: " + e.getMessage());
|
||||
Log.e("LoginTrace", "[" + traceId + "] b000_parse_error parseMs="
|
||||
+ (System.currentTimeMillis() - mainStartMs)
|
||||
+ " totalMs=" + (System.currentTimeMillis() - startMs)
|
||||
+ " err=" + e.getMessage());
|
||||
handlerMain.start("StarCloudAPI", 500, "登入解析失敗: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
Log.w("LoginTrace", "[" + traceId + "] b000_http_fail code=" + finalCode
|
||||
+ " totalMs=" + (System.currentTimeMillis() - startMs));
|
||||
handlerMain.start("StarCloudAPI", finalCode,
|
||||
"網路錯誤 (" + finalCode + "): " + finalReceive);
|
||||
}
|
||||
|
||||
@ -361,10 +361,11 @@ public class HttpConnect {
|
||||
return outputFile;
|
||||
}
|
||||
|
||||
public void uploadFile(String uploadUrl, File logFile) {
|
||||
public boolean uploadFile(String uploadUrl, File logFile) {
|
||||
HttpURLConnection connection = null;
|
||||
DataOutputStream outputStream = null;
|
||||
FileInputStream fileInputStream = null;
|
||||
boolean uploadSuccess = false;
|
||||
|
||||
String boundary = "*****"; // 分隔符
|
||||
String lineEnd = "\r\n";
|
||||
@ -373,7 +374,7 @@ public class HttpConnect {
|
||||
try {
|
||||
if (!logFile.exists()) {
|
||||
logs.debug("檔案不存在:" + logFile.getAbsolutePath());
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
logs.debug("開始上傳:" + logFile.getAbsolutePath());
|
||||
@ -413,6 +414,7 @@ public class HttpConnect {
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
logs.info("Log 上傳成功!");
|
||||
logFile.delete();
|
||||
uploadSuccess = true;
|
||||
} else {
|
||||
logs.debug("Log 上傳失敗,HTTP 狀態碼:" + responseCode);
|
||||
|
||||
@ -444,6 +446,8 @@ public class HttpConnect {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return uploadSuccess;
|
||||
}
|
||||
|
||||
public File getFileFromUrl(String urlString) throws LogsFileNullException, LogsEmptyException {
|
||||
|
||||
@ -34,7 +34,7 @@ public class TransactionFinalizeBuilder {
|
||||
private final String orderMachineTime;
|
||||
|
||||
/** 機台本地流水號(格式:YYYYMMDDXXXX,例如 202605110001) */
|
||||
private final String flowId;
|
||||
private String flowId;
|
||||
|
||||
/** 累積的出貨結果清單 */
|
||||
private final List<TransactionFinalizePayload.DispenseRecord> dispenseRecords = new ArrayList<>();
|
||||
@ -48,6 +48,10 @@ public class TransactionFinalizeBuilder {
|
||||
Log.i(TAG, "TransactionFinalizeBuilder created, flow_id=" + this.flowId);
|
||||
}
|
||||
|
||||
public void setFlowId(String flowId) {
|
||||
this.flowId = flowId;
|
||||
}
|
||||
|
||||
public void setCodeId(String codeId) {
|
||||
this.codeId = codeId;
|
||||
}
|
||||
|
||||
@ -463,6 +463,11 @@ public class FontendActivity extends AppCompatActivityAbstract implements SaleFl
|
||||
Process.killProcess(Process.myPid());
|
||||
}
|
||||
|
||||
public SaleFlowHandler getSaleFlowHandler() {
|
||||
return saleFlowHandler;
|
||||
}
|
||||
|
||||
|
||||
public void startOnSwitchFragment() {
|
||||
try {
|
||||
stopOnSwitchFragment(); // ✅ 先安全釋放舊播放器
|
||||
|
||||
@ -24,6 +24,9 @@ import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.ui.FontendActivity;
|
||||
import com.unibuy.smartdevice.ui.SaleFlowHandler;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@ -231,7 +234,16 @@ public class VmcFragment extends FragmentAbstract {
|
||||
setUseStartOnSwitchFragment(false);
|
||||
}
|
||||
|
||||
// 解決生命週期時序競爭:VmcFragment 視圖加載完成後重新綁定大掃碼按鈕
|
||||
if (getActivity() instanceof FontendActivity) {
|
||||
com.unibuy.smartdevice.ui.SaleFlowHandler handler = ((FontendActivity) getActivity()).getSaleFlowHandler();
|
||||
if (handler != null) {
|
||||
handler.onSetupUI(root);
|
||||
}
|
||||
}
|
||||
|
||||
return root;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
|
||||
import com.unibuy.smartdevice.AppCompatActivityAbstract;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.R;
|
||||
import com.unibuy.smartdevice.databinding.ActivityVmcSlotListBinding;
|
||||
import com.unibuy.smartdevice.devices.SlotField;
|
||||
import com.unibuy.smartdevice.exception.ErrorCode;
|
||||
@ -28,6 +29,9 @@ import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
import com.unibuy.smartdevice.ui.devs.electric.ElectricSlotListActivity;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class VmcSlotListActivity extends AppCompatActivityAbstract {
|
||||
private ActivityVmcSlotListBinding binding;
|
||||
private ActivityResultLauncher<Intent> resultLauncher;
|
||||
@ -146,6 +150,11 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
|
||||
if (currentFocus != null) {
|
||||
currentFocus.clearFocus();
|
||||
}
|
||||
String validationError = validateMaterialCodeConsistency();
|
||||
if (!validationError.isEmpty()) {
|
||||
getHandlerMain().start(getClass().getSimpleName(), "@" + validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 本地儲存
|
||||
saveData();
|
||||
@ -270,4 +279,18 @@ public class VmcSlotListActivity extends AppCompatActivityAbstract {
|
||||
getLogs().error(ErrorCode.NETWORK_UNKNOWN_ERROR, "同步啟動失敗: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String validateMaterialCodeConsistency() {
|
||||
for (SlotStructure slot : MyApp.getInstance().getSlotList(SlotField.VMC.getField())) {
|
||||
if (slot == null || slot.getProduct() == null) continue;
|
||||
ProductStructure product = slot.getProduct();
|
||||
String productId = product.getProductID() == null ? "" : product.getProductID().trim();
|
||||
if (productId.isEmpty() || "?".equals(productId)) continue;
|
||||
String materialCode = product.getMaterialCode() == null ? "" : product.getMaterialCode().trim();
|
||||
if (materialCode.isEmpty()) {
|
||||
return getString(R.string.vmc_material_code_empty, slot.getSlot(), productId);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import com.unibuy.smartdevice.controller.DevController;
|
||||
import com.unibuy.smartdevice.controller.DevDigitalDisplayController;
|
||||
import com.unibuy.smartdevice.controller.DevElectricController;
|
||||
import com.unibuy.smartdevice.database.SlotsDao;
|
||||
import com.unibuy.smartdevice.database.MemoDao;
|
||||
|
||||
import com.unibuy.smartdevice.databinding.DialogDispatchBinding;
|
||||
import com.unibuy.smartdevice.devices.ComPort;
|
||||
@ -43,6 +44,7 @@ import com.unibuy.smartdevice.structure.ProductStructure;
|
||||
import com.unibuy.smartdevice.structure.ReportDispatchInfoStructure;
|
||||
import com.unibuy.smartdevice.structure.ReportFreeGiftCodeStructure;
|
||||
import com.unibuy.smartdevice.structure.SlotStructure;
|
||||
import com.unibuy.smartdevice.structure.MemoStructure;
|
||||
import com.unibuy.smartdevice.tools.HandlerMainCountdown;
|
||||
import com.unibuy.smartdevice.tools.HandlerMain;
|
||||
import com.unibuy.smartdevice.tools.ToastHandlerMain;
|
||||
@ -53,6 +55,7 @@ import com.unibuy.smartdevice.ui.tools.ImageGlide;
|
||||
import com.unibuy.smartdevice.ui.tools.StringRefactoring;
|
||||
import com.unibuy.smartdevice.worker.UploadLogOneTimeWorker;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@ -249,6 +252,9 @@ public class DispatchDialog extends DialogAbstract {
|
||||
httpAPI = new HttpAPI(getHandlerMain());
|
||||
// 初始化 Finalize Builder(在支付完成、進入出貨流程時建立)
|
||||
finalizeBuilder = new TransactionFinalizeBuilder();
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowType(42);
|
||||
}
|
||||
|
||||
MyApp.getInstance().setReportFreeGiftCodeStructure(new ReportFreeGiftCodeStructure("", 0, "", 0, 0, ""));
|
||||
|
||||
@ -296,6 +302,56 @@ public class DispatchDialog extends DialogAbstract {
|
||||
|
||||
getLogs().info("DispatchList size:" + this.dispatchList.size());
|
||||
|
||||
if ("Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project)) {
|
||||
try {
|
||||
String patName = "未知患者";
|
||||
String sn = "";
|
||||
String drgNo = "";
|
||||
|
||||
// 優先從全域解密資料讀取(最完整,包含姓名與領藥號)
|
||||
String decryptedStr = MyApp.getInstance().getDecryptedData();
|
||||
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 = "未知患者";
|
||||
|
||||
sn = fullJson.optString("SN", "");
|
||||
drgNo = fullJson.optString("DRGNO", "");
|
||||
}
|
||||
|
||||
// 如果全域資料讀不到,嘗試從商品行銷計劃 (marketingPlan) 讀取 (備用防呆)
|
||||
if (sn.isEmpty() && this.dispatchList != null && !this.dispatchList.isEmpty()) {
|
||||
String marketingPlan = this.dispatchList.get(0).getProduct().getMarketingPlan();
|
||||
if (marketingPlan != null && !marketingPlan.isEmpty()) {
|
||||
JSONObject rxJson = new JSONObject(marketingPlan);
|
||||
sn = rxJson.optString("SN", "");
|
||||
if (drgNo.isEmpty()) {
|
||||
drgNo = rxJson.optString("DRGNO", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按照最新要求:[患者姓名]([藥單號]) 用括號
|
||||
String barcodeInfo = patName + "(" + drgNo + ")";
|
||||
getLogs().info("✅ 中國醫 QR Code 取物單資訊打包上報:姓名 " + patName + ", 藥單號 " + drgNo + " -> barcodeInfo: " + barcodeInfo);
|
||||
|
||||
// 設定到 flowBarCode (會連動到後台 member_barcode 欄位)
|
||||
MyApp.getInstance().getReportFlowInfoData().setFlowBarCode(barcodeInfo);
|
||||
|
||||
// 序號是 flow_id,將交易流水號與本地 OrderId / ReportFlowId 覆寫為純藥單序號 SN
|
||||
if (sn != null && !sn.isEmpty()) {
|
||||
finalizeBuilder.setFlowId(sn);
|
||||
MyApp.getInstance().getReportFlowInfoData().setOrderId(sn);
|
||||
MyApp.getInstance().getReportFlowInfoData().setReportFlowId(sn);
|
||||
getLogs().info("✅ 交易流水號已成功覆寫為純藥單序號 (SN):" + sn);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogs().warning(e);
|
||||
}
|
||||
}
|
||||
|
||||
this.recyclerDialogDispatchListAdpter = new RecyclerDialogDispatchListAdpter(getCtx(), this.dispatchList, getHandlerMain());
|
||||
this.binding.recyclerDialogDispatchList.setAdapter(recyclerDialogDispatchListAdpter);
|
||||
this.binding.recyclerDialogDispatchList.setLayoutManager(new LinearLayoutManager(context));
|
||||
@ -452,17 +508,31 @@ public class DispatchDialog extends DialogAbstract {
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.check_whether_goods_have_been_taken_out_before_confirming_collection));//請檢查商品有無取出再確認取貨
|
||||
}
|
||||
} else {
|
||||
if (checkSlotRunCount >= 3) {
|
||||
boolean isCmuh = "Cmuh".equalsIgnoreCase(com.unibuy.smartdevice.BuildConfig.FLAVOR_project);
|
||||
int maxRetries = isCmuh ? 10 : 3;
|
||||
if (checkSlotRunCount >= maxRetries) {
|
||||
buttonControlCountdown.shutdown();
|
||||
getSrcHandlerMain().start(getClass().getSimpleName(), Option.ERROR_PROCESS_GOODS.getOption(), getCtx().getString(R.string.slave_computer_did_not_respond_wait_3_seconds_try_again));//下位機無回應 請等3秒後再重試
|
||||
if (isCmuh) {
|
||||
getLogs().info("機台未回應,已達 Cmuh 最大重試次數 10 次,嘗試重新連線下位機");
|
||||
insertDrgnoToMemo(); // 領藥失敗,新增醫令至 Memo 防止雙重領取
|
||||
MyApp.getInstance().getDevXinYuanController().start();
|
||||
if (!MyApp.checkAndReconnectDev()) {
|
||||
getLogs().info("無法連線到下位機,出貨作業中止");
|
||||
}
|
||||
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));
|
||||
MyApp.getInstance().getDevXinYuanController().start();
|
||||
}
|
||||
checkSlotRunCount = 0;
|
||||
MyApp.getInstance().getDevXinYuanController().start();
|
||||
} else {
|
||||
getLogs().info("機台未回應,第 " + (checkSlotRunCount + 1) + " 次重試 checkSlot...");
|
||||
getLogs().info("機台未回應,第 " + (checkSlotRunCount + 1) + " 次重試 checkSlot..." + (isCmuh ? " (溫和模式,不發送指令)" : ""));
|
||||
isCheckSlotByVMC = false; // 重置旗標,允許下次回調進入
|
||||
checkSlotRunCount++;
|
||||
MyApp.getInstance().getDevXinYuanController().getReadBufferByNow(new DevCheckSlotByVMC(handlerMain, slot));
|
||||
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
|
||||
if (!isCmuh) {
|
||||
MyApp.getInstance().getDevXinYuanController().addSendBuffer(MyApp.getInstance().getDevXinYuanController().getDevXinYuan().checkSlot(slot));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -825,10 +895,13 @@ public class DispatchDialog extends DialogAbstract {
|
||||
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");
|
||||
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) {
|
||||
@ -1054,4 +1127,48 @@ public class DispatchDialog extends DialogAbstract {
|
||||
}
|
||||
}
|
||||
|
||||
public void insertDrgnoToMemo() {
|
||||
try {
|
||||
/*刷藥單解碼後進來的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")); // **領藥人醫令**
|
||||
|
||||
/*從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");
|
||||
|
||||
// **判斷是否已存在**
|
||||
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"))) {
|
||||
exists = true;
|
||||
break; // **如果已存在,就結束迴圈**
|
||||
}
|
||||
}
|
||||
|
||||
// **如果不存在,才加入**
|
||||
if (!exists) {
|
||||
itemArray.put(newItem);
|
||||
getLogs().info("✅ 領藥人醫令新增到Memo成功:" + newItem.toString());
|
||||
} else {
|
||||
getLogs().info("⚠️ 領藥人醫令已存在,不新增!");
|
||||
}
|
||||
|
||||
// **輸出結果**並新增到Memo 的 SQLite資料庫 data欄位中
|
||||
MyApp.getInstance().getMemoMap().get("CMUH").get(0).setData(jsonObject2);
|
||||
// ✅ 寫入 SQLite
|
||||
MemoStructure updatedMemo = MyApp.getInstance().getMemoMap().get("CMUH").get(0);
|
||||
MemoDao memoDao = new MemoDao(getCtx());
|
||||
memoDao.insertOnlyOne(updatedMemo);
|
||||
memoDao.close();
|
||||
} catch (JSONException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import androidx.work.WorkerParameters;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.exception.Logs;
|
||||
import com.unibuy.smartdevice.external.HttpAPI;
|
||||
import com.unibuy.smartdevice.external.StarCloudAPI;
|
||||
import com.unibuy.smartdevice.tools.HttpConnect;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
|
||||
@ -32,31 +33,60 @@ public class UploadLogOneTimeWorker extends Worker {
|
||||
|
||||
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
|
||||
logs.debug("背景工作執行失敗:沒有機器序號,使用暫時序號 00000000");
|
||||
HttpAPI.updateOnDomainNameByNewHost = HttpAPI.domainNameByNewHost + "api/machine/00000000/log/update";
|
||||
StarCloudAPI.updateOnDomainNameByNewHost = StarCloudAPI.domainNameByNewHost + "api/machine/00000000/log/update";
|
||||
} else {
|
||||
HttpAPI.updateOnDomainNameByNewHost = HttpAPI.domainNameByNewHost + "api/machine/" + MyApp.getInstance().getMachine().getMachineID() + "/log/update";
|
||||
StarCloudAPI.updateOnDomainNameByNewHost = StarCloudAPI.domainNameByNewHost + "api/machine/" + MyApp.getInstance().getMachine().getMachineID() + "/log/update";
|
||||
}
|
||||
|
||||
logs.debug("url:" + HttpAPI.updateOnDomainNameByNewHost);
|
||||
logs.debug("url:" + StarCloudAPI.updateOnDomainNameByNewHost);
|
||||
|
||||
File txtDebug = new File(Tools.getExternalFilesDir(), "debug.txt");
|
||||
|
||||
String yyyyMMddHHmmByNow = Tools.getStringByNow("yyyyMMddHHmm");
|
||||
if (yyyyMMddHHmmByNow.length() == 12) {
|
||||
yyyyMMddHHmmByNow = yyyyMMddHHmmByNow.substring(0, 11) + "0";
|
||||
}
|
||||
File newFile = new File(Tools.getExternalFilesDir(), yyyyMMddHHmmByNow + ".txt");
|
||||
// 1. 旋轉目前的 debug.txt
|
||||
if (txtDebug.exists() && txtDebug.length() > 0) {
|
||||
String yyyyMMddHHmmByNow = Tools.getStringByNow("yyyyMMddHHmm");
|
||||
if (yyyyMMddHHmmByNow.length() == 12) {
|
||||
yyyyMMddHHmmByNow = yyyyMMddHHmmByNow.substring(0, 11) + "0";
|
||||
}
|
||||
// 加上毫秒時間戳記,確保檔名在任何情況下都唯一,防止 renameTo 失敗
|
||||
String uniqueFileName = yyyyMMddHHmmByNow + "_" + System.currentTimeMillis() + ".txt";
|
||||
File newFile = new File(Tools.getExternalFilesDir(), uniqueFileName);
|
||||
|
||||
if (txtDebug.exists()) {
|
||||
boolean renamed = txtDebug.renameTo(newFile);
|
||||
logs.debug("newFile:" + newFile.getAbsolutePath() + " renamed:" + renamed);
|
||||
|
||||
if (renamed) {
|
||||
HttpConnect httpConnect = new HttpConnect();
|
||||
httpConnect.uploadFile(HttpAPI.updateOnDomainNameByNewHost, newFile);
|
||||
if (!renamed) {
|
||||
logs.debug("重新命名日誌檔失敗,將重試。");
|
||||
return Result.retry();
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success();// 工作成功
|
||||
// 2. 掃描 ExternalFilesDir 批次補上傳非 active 的所有 txt 日誌檔
|
||||
File dir = Tools.getExternalFilesDir();
|
||||
boolean anyUploadFailed = false;
|
||||
if (dir != null && dir.exists()) {
|
||||
File[] files = dir.listFiles((d, name) -> name.endsWith(".txt")
|
||||
&& !name.equals("debug.txt")
|
||||
&& !name.equals("crash_pending.txt"));
|
||||
|
||||
if (files != null && files.length > 0) {
|
||||
logs.debug("發現待上傳歷史日誌共 " + files.length + " 個,開始批次上傳。");
|
||||
HttpConnect httpConnect = new HttpConnect();
|
||||
for (File file : files) {
|
||||
boolean success = httpConnect.uploadFile(StarCloudAPI.updateOnDomainNameByNewHost, file);
|
||||
if (!success) {
|
||||
logs.debug("檔案上傳失敗,保留至下次重試:" + file.getName());
|
||||
anyUploadFailed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyUploadFailed) {
|
||||
logs.debug("部分歷史日誌上傳失敗,排程重試中。");
|
||||
return Result.retry();
|
||||
}
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@ import androidx.work.WorkerParameters;
|
||||
import com.unibuy.smartdevice.MyApp;
|
||||
import com.unibuy.smartdevice.exception.Logs;
|
||||
import com.unibuy.smartdevice.external.HttpAPI;
|
||||
import com.unibuy.smartdevice.external.StarCloudAPI;
|
||||
import com.unibuy.smartdevice.tools.HttpConnect;
|
||||
import com.unibuy.smartdevice.tools.Tools;
|
||||
|
||||
@ -47,33 +48,60 @@ public class UploadLogPeriodicWorker extends Worker {
|
||||
|
||||
if (MyApp.getInstance().getMachine().getMachineID().isEmpty()) {
|
||||
logs.debug("背景工作執行失敗:沒有機器序號,使用暫時序號 00000000");
|
||||
HttpAPI.updateOnDomainNameByNewHost = HttpAPI.domainNameByNewHost + "api/machine/00000000/log/update";
|
||||
StarCloudAPI.updateOnDomainNameByNewHost = StarCloudAPI.domainNameByNewHost + "api/machine/00000000/log/update";
|
||||
} else {
|
||||
HttpAPI.updateOnDomainNameByNewHost = HttpAPI.domainNameByNewHost + "api/machine/" + MyApp.getInstance().getMachine().getMachineID() + "/log/update";
|
||||
StarCloudAPI.updateOnDomainNameByNewHost = StarCloudAPI.domainNameByNewHost + "api/machine/" + MyApp.getInstance().getMachine().getMachineID() + "/log/update";
|
||||
}
|
||||
|
||||
logs.debug("url:" + HttpAPI.updateOnDomainNameByNewHost);
|
||||
logs.debug("url:" + StarCloudAPI.updateOnDomainNameByNewHost);
|
||||
|
||||
File txtDebug = new File(Tools.getExternalFilesDir(), "debug.txt");
|
||||
|
||||
String yyyyMMddHHmmByNow = Tools.getStringByNow("yyyyMMddHHmm");
|
||||
if (yyyyMMddHHmmByNow.length() == 12) {
|
||||
yyyyMMddHHmmByNow = yyyyMMddHHmmByNow.substring(0, 11) + "0";
|
||||
}
|
||||
File newFile = new File(Tools.getExternalFilesDir(), yyyyMMddHHmmByNow + ".txt");
|
||||
// 1. 旋轉目前的 debug.txt
|
||||
if (txtDebug.exists() && txtDebug.length() > 0) {
|
||||
String yyyyMMddHHmmByNow = Tools.getStringByNow("yyyyMMddHHmm");
|
||||
if (yyyyMMddHHmmByNow.length() == 12) {
|
||||
yyyyMMddHHmmByNow = yyyyMMddHHmmByNow.substring(0, 11) + "0";
|
||||
}
|
||||
// 加上毫秒時間戳記,確保檔名在任何情況下都唯一,防止 renameTo 失敗
|
||||
String uniqueFileName = yyyyMMddHHmmByNow + "_" + System.currentTimeMillis() + ".txt";
|
||||
File newFile = new File(Tools.getExternalFilesDir(), uniqueFileName);
|
||||
|
||||
if (txtDebug.exists()) {
|
||||
boolean renamed = txtDebug.renameTo(newFile);
|
||||
logs.debug("newFile:" + newFile.getAbsolutePath() + " renamed:" + renamed);
|
||||
|
||||
if (renamed) {
|
||||
HttpConnect httpConnect = new HttpConnect();
|
||||
httpConnect.uploadFile(HttpAPI.updateOnDomainNameByNewHost, newFile);
|
||||
} else {
|
||||
if (!renamed) {
|
||||
logs.debug("重新命名日誌檔失敗,將重試。");
|
||||
return Result.retry();
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 掃描 ExternalFilesDir 批次補上傳非 active 的所有 txt 日誌檔
|
||||
File dir = Tools.getExternalFilesDir();
|
||||
boolean anyUploadFailed = false;
|
||||
if (dir != null && dir.exists()) {
|
||||
File[] files = dir.listFiles((d, name) -> name.endsWith(".txt")
|
||||
&& !name.equals("debug.txt")
|
||||
&& !name.equals("crash_pending.txt"));
|
||||
|
||||
if (files != null && files.length > 0) {
|
||||
logs.debug("發現待上傳歷史日誌共 " + files.length + " 個,開始批次上傳。");
|
||||
HttpConnect httpConnect = new HttpConnect();
|
||||
for (File file : files) {
|
||||
boolean success = httpConnect.uploadFile(StarCloudAPI.updateOnDomainNameByNewHost, file);
|
||||
if (!success) {
|
||||
logs.debug("檔案上傳失敗,保留至下次重試:" + file.getName());
|
||||
anyUploadFailed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (anyUploadFailed) {
|
||||
logs.debug("部分歷史日誌上傳失敗,排程重試中。");
|
||||
return Result.retry();
|
||||
}
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@ -107,8 +107,8 @@
|
||||
<TextView
|
||||
android:id="@+id/textNetwork"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:text="@string/no_internet"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
|
||||
@ -101,4 +101,4 @@
|
||||
android:textStyle="bold"/>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@ -393,4 +393,11 @@
|
||||
<string name="sync_product">Reload Products</string>
|
||||
<string name="sync_product_warning">This will download all products from the cloud and overwrite any unsaved local inventory changes. Are you sure you want to continue?</string>
|
||||
<string name="downloading_product_data">Downloading product data...</string>
|
||||
</resources>
|
||||
<string name="cmuh_ordc_empty">Missing ORDC in prescription data. Please scan again or contact pharmacy staff.</string>
|
||||
<string name="cmuh_material_not_found">Item [%1$s] has no stock or no assigned slot. Please verify configuration.</string>
|
||||
<string name="cmuh_stock_not_enough">Item [%1$s] has insufficient stock. Required: %2$d, Available: %3$d</string>
|
||||
<string name="vmc_material_code_empty">MATERIAL_CODE_EMPTY Slot %1$d product %2$s is missing material code. Please fix product master data first.</string>
|
||||
<string name="cmuh_pickup_failed_contact_counter">Pickup failed\nPlease contact the counter</string>
|
||||
<string name="close_40">Close(40)</string>
|
||||
<string name="close_countdown">Close(%1$d)</string>
|
||||
</resources>
|
||||
|
||||
@ -395,4 +395,11 @@
|
||||
<string name="downloading_product_data">商品データを同期中...</string>
|
||||
<string name="syncing">同期中... </string>
|
||||
<string name="sync_completed">商品データの同期が完了しました</string>
|
||||
</resources>
|
||||
<string name="cmuh_ordc_empty">処方データに ORDC がありません。再度スキャンするか薬局窓口へお問い合わせください。</string>
|
||||
<string name="cmuh_material_not_found">商品 [%1$s] は在庫切れ、またはスロット未設定です。設定を確認してください。</string>
|
||||
<string name="cmuh_stock_not_enough">商品 [%1$s] の在庫が不足しています。必要数: %2$d、在庫: %3$d</string>
|
||||
<string name="vmc_material_code_empty">MATERIAL_CODE_EMPTY スロット %1$d の商品 %2$s に物料コードがありません。商品マスタを修正してください。</string>
|
||||
<string name="cmuh_pickup_failed_contact_counter">受け取り失敗\nカウンターへお問い合わせください</string>
|
||||
<string name="close_40">閉じる(40)</string>
|
||||
<string name="close_countdown">閉じる(%1$d)</string>
|
||||
</resources>
|
||||
|
||||
@ -312,4 +312,11 @@
|
||||
<string name="upload_success">資料上傳成功</string>
|
||||
<string name="upload_failed">資料上傳失敗</string>
|
||||
<string name="upload_to_backend_and_return">上傳後台並返回</string>
|
||||
</resources>
|
||||
<string name="cmuh_ordc_empty">領藥單缺少 ORDC,請重新掃碼或洽藥局窗口。</string>
|
||||
<string name="cmuh_material_not_found">商品 [%1$s] 無庫存或未設定貨道,請確認。</string>
|
||||
<string name="cmuh_stock_not_enough">商品 [%1$s] 庫存不足,需求:%2$d,可用:%3$d</string>
|
||||
<string name="vmc_material_code_empty">MATERIAL_CODE_EMPTY 貨道 %1$d 商品 %2$s 缺少物料編號,請先修正商品主檔。</string>
|
||||
<string name="cmuh_pickup_failed_contact_counter">取物失敗\n請洽櫃台</string>
|
||||
<string name="close_40">關閉(40)</string>
|
||||
<string name="close_countdown">關閉(%1$d)</string>
|
||||
</resources>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user