[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:
sky121113 2026-05-28 17:48:03 +08:00
parent 46c24fcd2e
commit 71a059183a
27 changed files with 1271 additions and 610 deletions

View File

@ -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
View 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` 僅作導覽與快速摘要,不作為完整規範來源。

View File

@ -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 {

View File

@ -24,6 +24,10 @@ public class SaleFlowHandler {
// 晟崴版 UI 設定預設無動作
}
public void onSetupUI(android.view.View rootView) {
// 晟崴版 UI 設定預設無動作
}
public void onBuyRequested() {
new MemberCardPickupDialog(context, handlerMain).show();
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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>

View File

@ -24,6 +24,10 @@ public class SaleFlowHandler {
// 通用版 UI 設定預設無動作
}
public void onSetupUI(android.view.View rootView) {
// 通用版 UI 設定預設無動作
}
public void onBuyRequested() {
new BuyDialog(context, handlerMain).show();
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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

View File

@ -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);
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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(); // 先安全釋放舊播放器

View File

@ -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;
}
/**

View File

@ -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 "";
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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"

View File

@ -101,4 +101,4 @@
android:textStyle="bold"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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>

View File

@ -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>

View File

@ -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>