[FIX] 整合中國醫功能優化、連續出貨修復與多國語系字串調整

1. 遞增 app/build.gradle 修補版本號為 1.30。
2. 優化 StarCloudAPI 登入追蹤日誌並調整日誌前綴。
3. 修正 DispatchDialog 出貨邏輯,移除單筆出貨防呆,恢復支援多筆連續出貨。
4. 新增與調整多國語系 strings.xml (繁中、英文、日文) 中國醫無庫存與未設定貨道的提示訊息。
This commit is contained in:
sky121113 2026-05-29 11:08:42 +08:00
parent 71a059183a
commit 47acd44e61
10 changed files with 58 additions and 31 deletions

View File

@ -9,7 +9,7 @@ plugins {
//
// =========================================================================
def verMinor = 1
def verPatch = 24
def verPatch = 30
android {

View File

@ -47,6 +47,7 @@ import javax.crypto.spec.SecretKeySpec;
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_EMPTY = "STOCK_EMPTY";
private static final String STOCK_NOT_ENOUGH = "STOCK_NOT_ENOUGH";
protected final Context context;
@ -270,7 +271,13 @@ public class SaleFlowHandler {
}
int totalStock = 0;
for (SlotStructure slotData : matchedSlots) {
totalStock += slotData.getCount();
if (isAvailableCmuhStock(slotData)) {
totalStock += slotData.getCount();
}
}
if (totalStock <= 0) {
failWithCode(STOCK_EMPTY, context.getString(R.string.cmuh_stock_empty, ordc));
return;
}
if (requestQty <= 0 || totalStock < requestQty) {
failWithCode(STOCK_NOT_ENOUGH, context.getString(R.string.cmuh_stock_not_enough, ordc, requestQty, totalStock));
@ -293,6 +300,7 @@ public class SaleFlowHandler {
MemoSnDrgno = "";
List<SlotStructure> matchedSlots = materialSlotMap.get(ordc);
for (SlotStructure matchedSlot : matchedSlots) {
if (!isAvailableCmuhStock(matchedSlot)) continue;
MedicineCount += matchedSlot.getCount();
MedicineSlot += matchedSlot.getSlot() + "-" + matchedSlot.getCount() + ",";
if (MedicineCount >= requestQty) {
@ -363,12 +371,19 @@ public class SaleFlowHandler {
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;
if (slotData.getSlot() <= 0) {
MyApp.getInstance().getLogs().info("Skip invalid VMC slot for CMUH pickup: " + slotData.getSlot());
continue;
}
materialSlotMap.computeIfAbsent(materialCode.trim(), k -> new ArrayList<>()).add(slotData);
}
return materialSlotMap;
}
private boolean isAvailableCmuhStock(SlotStructure slotData) {
return slotData != null && slotData.getCount() > 0 && slotData.getCount() <= 20;
}
private int safeParseInt(String value, int defaultValue) {
try {
return Integer.parseInt(value);
@ -378,17 +393,21 @@ public class SaleFlowHandler {
}
private void failWithCode(String code, String message) {
String finalMessage = "@" + code + " " + message;
MyApp.getInstance().getLogs().info("[" + code + "] " + message);
MyApp.getInstance().setCmuhPickupErrorMessage(finalMessage.substring(1));
MyApp.getInstance().setCmuhPickupErrorMessage(message);
haveValuestate = false;
openBarcode = false;
MyApp.getInstance().getBuyList().clear();
}
public void addMedicineToBuyList(String MedicinePidStr, String MedicineNameStr, int SlotNumber, String MedicineImg, String MemoSnDrgno, int count) {
if (SlotNumber <= 0) {
failWithCode(MATERIAL_NOT_FOUND, context.getString(R.string.cmuh_material_not_found, MedicinePidStr));
MyApp.getInstance().getLogs().info("CMUH pickup blocked invalid slot: slot=" + SlotNumber + ", pid=" + MedicinePidStr + ", name=" + MedicineNameStr);
return;
}
ProductStructure productStructure = new ProductStructure(MedicinePidStr, MedicineImg, MedicineNameStr, 0, 0, 0, 0, MemoSnDrgno, "", "", "");
MyApp.getInstance().getLogs().info("aaaaaa " + MedicinePidStr + " " + MemoSnDrgno + " " + MedicineNameStr);
MyApp.getInstance().getLogs().info("CMUH pickup add buy item: slot=" + SlotNumber + ", pid=" + MedicinePidStr + ", name=" + MedicineNameStr);
BuyStructure buyProduct = new BuyStructure(0, SlotNumber, productStructure, count);
MyApp.getInstance().getBuyList().add(buyProduct);
}

View File

@ -59,7 +59,7 @@ public class RecyclerDialogPickupCodeBuyListAdpter extends RecyclerView.Adapter<
logs.info("position:" + position);
BuyStructure buyProduct = buyList.get(position);
holder.binding.textPosition.setText(String.valueOf(position));
holder.binding.textPosition.setText(String.valueOf(position + 1));
String productID = buyProduct.getProduct().getProductID();
if (!productID.isEmpty() && !productID.equals("?")) {
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());

View File

@ -54,7 +54,7 @@ public class RecyclerDialogTakeMedicineListAdpter extends RecyclerView.Adapter<R
logs.info("position:" + position);
BuyStructure buyProduct = buyList.get(position);
holder.binding.textPosition.setText(String.valueOf(position));
holder.binding.textPosition.setText(String.valueOf(buyProduct.getSlot()));
String productID = buyProduct.getProduct().getProductID();
if (!productID.isEmpty() && !productID.equals("?")) {
holder.binding.textProductName.setText(buyProduct.getProduct().getProductName());

View File

@ -13,7 +13,6 @@ 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;
@ -64,7 +63,6 @@ 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;
@ -86,7 +84,7 @@ public class MainActivity extends AppCompatActivityAbstract {
if ("登入成功".equals(message)) {
if (pendingLoginTraceId != null) {
long now = System.currentTimeMillis();
Log.i(LOGIN_TRACE_TAG, "[" + pendingLoginTraceId + "] callback=success totalMs="
getLogs().info("[LoginTrace][" + pendingLoginTraceId + "] callback=success totalMs="
+ (now - pendingLoginStartMs) + " msg=" + message);
}
updateLoginStatus();
@ -103,7 +101,7 @@ public class MainActivity extends AppCompatActivityAbstract {
} 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="
getLogs().info("[LoginTrace][" + pendingLoginTraceId + "] callback=failure totalMs="
+ (now - pendingLoginStartMs) + " msg=" + message + " code=" + commandCode);
pendingLoginTraceId = null;
pendingLoginStartMs = 0L;
@ -586,10 +584,10 @@ public class MainActivity extends AppCompatActivityAbstract {
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());
getLogs().info("[LoginTrace][" + traceId + "] click_login accountLen=" + access.length());
if (access.isEmpty() || password.isEmpty()) {
Log.w(LOGIN_TRACE_TAG, "[" + traceId + "] blocked_empty_input");
getLogs().info("[LoginTrace][" + traceId + "] blocked_empty_input");
Toast.makeText(context, "請輸入帳號和密碼", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "正在驗證登入...", Toast.LENGTH_SHORT).show();
@ -599,15 +597,15 @@ public class MainActivity extends AppCompatActivityAbstract {
pendingAction = onSuccess;
pendingLoginTraceId = traceId;
pendingLoginStartMs = clickStartMs;
Log.i(LOGIN_TRACE_TAG, "[" + traceId + "] dispatch_starcloud_login");
getLogs().info("[LoginTrace][" + 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");
getLogs().info("[LoginTrace][" + traceId + "] remember_account=save");
} else {
prefs.edit().remove("saved_account").apply();
Log.i(LOGIN_TRACE_TAG, "[" + traceId + "] remember_account=clear");
getLogs().info("[LoginTrace][" + traceId + "] remember_account=clear");
}
// 🛠 關鍵優化不再此處立即 dismiss()改由 API 回報成功時關閉
}

View File

@ -55,6 +55,14 @@ public class StarCloudAPI {
this.srcHandlerMain = srcHandlerMain;
}
private void traceLogin(String message) {
if (handlerMain != null && handlerMain.getLogs() != null) {
handlerMain.getLogs().info("[LoginTrace]" + message);
} else {
Log.i("LoginTrace", message);
}
}
/**
* B000: 管理員登入驗證
*/
@ -66,7 +74,7 @@ public class StarCloudAPI {
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));
traceLogin("[" + traceId + "] b000_start fromUiMs=" + (System.currentTimeMillis() - startMs));
// 不依賴 FormPostMainScheduler (會靜默吞掉 IO exception)
// 直接用 Thread + HttpURLConnection 確保錯誤一定看得到
@ -103,11 +111,11 @@ public class StarCloudAPI {
long writeStartMs = System.currentTimeMillis();
conn.getOutputStream().write(bodyBytes);
conn.getOutputStream().flush();
Log.i("LoginTrace", "[" + traceId + "] b000_write_done tMs=" + (System.currentTimeMillis() - writeStartMs));
traceLogin("[" + traceId + "] b000_write_done tMs=" + (System.currentTimeMillis() - writeStartMs));
long responseStartMs = System.currentTimeMillis();
responseCode = conn.getResponseCode();
Log.i("LoginTrace", "[" + traceId + "] b000_response_code=" + responseCode
traceLogin("[" + traceId + "] b000_response_code=" + responseCode
+ " waitMs=" + (System.currentTimeMillis() - responseStartMs));
java.io.InputStream is = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream() : conn.getErrorStream();
@ -120,11 +128,11 @@ public class StarCloudAPI {
conn.disconnect();
Log.d(TAG, "B000 Response (" + responseCode + "): " + receive);
Log.i("LoginTrace", "[" + traceId + "] b000_network_done totalNetMs="
traceLogin("[" + 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="
traceLogin("[" + traceId + "] b000_network_error afterMs="
+ (System.currentTimeMillis() - threadStartMs) + " err=" + e.getMessage());
new android.os.Handler(android.os.Looper.getMainLooper()).post(new Runnable() {
@Override
@ -149,12 +157,12 @@ 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="
traceLogin("[" + 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="
traceLogin("[" + traceId + "] b000_business_fail code=401 parseMs="
+ (System.currentTimeMillis() - mainStartMs)
+ " totalMs=" + (System.currentTimeMillis() - startMs));
handlerMain.start("StarCloudAPI", 401,
@ -162,14 +170,14 @@ public class StarCloudAPI {
}
} catch (Exception e) {
Log.e(TAG, "B000 Parse Error: " + e.getMessage());
Log.e("LoginTrace", "[" + traceId + "] b000_parse_error parseMs="
traceLogin("[" + 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
traceLogin("[" + traceId + "] b000_http_fail code=" + finalCode
+ " totalMs=" + (System.currentTimeMillis() - startMs));
handlerMain.start("StarCloudAPI", finalCode,
"網路錯誤 (" + finalCode + "): " + finalReceive);

View File

@ -264,7 +264,7 @@ public class DispatchDialog extends DialogAbstract {
List<BuyStructure> buyList = MyApp.getInstance().getBuyList();
if (buyList.size() > 1) {
getLogs().info("⚠️ BuyList 有多筆,只處理第一筆,其餘略過");
getLogs().info("✅ BuyList 有多筆,將依序連續出貨");
}
for (BuyStructure buyStructure: buyList) {
@ -297,7 +297,6 @@ public class DispatchDialog extends DialogAbstract {
int pcount = buyStructure.getCount();
deliveryLogs.add("▲ 應出貨明細 - " + ptId + " / " + pName + " / 數量 " + pcount);
break; // 加入這行防呆只處理一筆後跳出
}
getLogs().info("DispatchList size:" + this.dispatchList.size());

View File

@ -394,7 +394,8 @@
<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>
<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_material_not_found">Item [%1$s] has no assigned slot. Please contact pharmacy staff.</string>
<string name="cmuh_stock_empty">Item [%1$s] is currently out of stock. Please contact pharmacy staff.</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>

View File

@ -396,7 +396,8 @@
<string name="syncing">同期中... </string>
<string name="sync_completed">商品データの同期が完了しました</string>
<string name="cmuh_ordc_empty">処方データに ORDC がありません。再度スキャンするか薬局窓口へお問い合わせください。</string>
<string name="cmuh_material_not_found">商品 [%1$s] は在庫切れ、またはスロット未設定です。設定を確認してください。</string>
<string name="cmuh_material_not_found">商品 [%1$s] はスロット未設定です。薬局窓口へお問い合わせください。</string>
<string name="cmuh_stock_empty">商品 [%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>

View File

@ -313,7 +313,8 @@
<string name="upload_failed">資料上傳失敗</string>
<string name="upload_to_backend_and_return">上傳後台並返回</string>
<string name="cmuh_ordc_empty">領藥單缺少 ORDC請重新掃碼或洽藥局窗口。</string>
<string name="cmuh_material_not_found">商品 [%1$s] 無庫存或未設定貨道,請確認。</string>
<string name="cmuh_material_not_found">商品 [%1$s] 未設定貨道,請洽藥局窗口。</string>
<string name="cmuh_stock_empty">商品 [%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>