diff --git a/app/build.gradle b/app/build.gradle index 524a51c..26e2eda 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { // 避免不同尺寸機台間版本號產生衝突。 // ========================================================================= def verMinor = 2 -def verPatch = 18 +def verPatch = 19 diff --git a/app/src/main/java/com/unibuy/smartdevice/MyApp.java b/app/src/main/java/com/unibuy/smartdevice/MyApp.java index 9e0b16b..d9436ae 100644 --- a/app/src/main/java/com/unibuy/smartdevice/MyApp.java +++ b/app/src/main/java/com/unibuy/smartdevice/MyApp.java @@ -885,6 +885,78 @@ public class MyApp extends Application { throw new LogsEmptyException(logs, "Slot data not found"); } + /** + * 把「某商品要買 quantity 個」依可賣貨道(count>0 && !lock && !expired)拆成多筆 count=1 的 BuyStructure。 + * + * 用途:VMC 出貨狀態機不支援單筆 DispatchStructure quantity>1(出完第 1 個後 count allocateBuyStructures(SlotStructure representative, int quantity) { + List result = new ArrayList<>(); + if (representative == null || quantity <= 0) { + return result; + } + ProductStructure repProduct = representative.getProduct(); + if (repProduct == null || repProduct.getProductID() == null) { + // 退化情形:仍維持「一筆一個」不變式,避免漏單或重新塞回 quantity>1 + for (int i = 0; i < quantity; i++) { + result.add(new BuyStructure(representative.getField(), representative.getSlot(), repProduct, 1)); + } + return result; + } + + int field = representative.getField(); + String pid = repProduct.getProductID(); + int preferredSlot = representative.getSlot(); + + // 收集同商品可賣貨道:代表道優先,其餘依貨道號遞增 + SlotStructure preferred = null; + List others = new ArrayList<>(); + List slotList = getSlotList(field); + if (slotList != null) { + for (SlotStructure s : slotList) { + if (s == null || s.getProduct() == null) continue; + if (!pid.equals(s.getProduct().getProductID())) continue; + if (s.getCount() <= 0 || s.isLock() || s.isExpired()) continue; + if (s.getSlot() == preferredSlot) { + preferred = s; + } else { + others.add(s); + } + } + } + others.sort((a, b) -> Integer.compare(a.getSlot(), b.getSlot())); + + List candidates = new ArrayList<>(); + if (preferred != null) candidates.add(preferred); + candidates.addAll(others); + + // greedy 配貨:每個候選道按其真實庫存填,逐一拆成 count=1 + int need = quantity; + for (SlotStructure s : candidates) { + if (need <= 0) break; + int take = Math.min(need, s.getCount()); + for (int i = 0; i < take; i++) { + result.add(new BuyStructure(s.getField(), s.getSlot(), s.getProduct(), 1)); + } + need -= take; + } + + // 可賣庫存不足 quantity 的溢位:掛回代表貨道,交既有失敗→退款保護網 + for (int i = 0; i < need; i++) { + result.add(new BuyStructure(field, preferredSlot, repProduct, 1)); + } + return result; + } + public List getMarketingPlanList() { return marketingPlanList; } diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/PickupSheetSaleFlow.java b/app/src/main/java/com/unibuy/smartdevice/ui/PickupSheetSaleFlow.java index b1fdd15..0a02874 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/PickupSheetSaleFlow.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/PickupSheetSaleFlow.java @@ -386,13 +386,19 @@ public class PickupSheetSaleFlow extends SaleFlowHandler { MyApp.getInstance().getLogs().info("Skip invalid VMC slot for CMUH pickup: " + slotData.getSlot()); continue; } + // 鎖貨道/效期過期的藥道不得列入可用清單:否則庫存驗證與分配會把它算進去,出貨時才失敗。 + if (slotData.isLock() || slotData.isExpired()) { + MyApp.getInstance().getLogs().info("Skip locked/expired VMC slot for CMUH pickup: slot=" + 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; + return slotData != null && slotData.getCount() > 0 && slotData.getCount() <= 20 + && !slotData.isLock() && !slotData.isExpired(); } private int safeParseInt(String value, int defaultValue) { diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/VmcFragment.java b/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/VmcFragment.java index 16ae582..93a6c05 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/VmcFragment.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/devs/vmc/VmcFragment.java @@ -267,36 +267,48 @@ public class VmcFragment extends FragmentAbstract { new TypeToken>() {}.getType() ); if (rawSlots == null || rawSlots.isEmpty()) return; - // 2. 处理数据(确保不修改任何对象) - Map uniqueProductMap = new LinkedHashMap<>(); + // 2. 按 ProductID 分組(同一商品的多個貨道收成一組) + LinkedHashMap> groups = new LinkedHashMap<>(); for (SlotStructure slot : rawSlots) { ProductStructure product = slot.getProduct(); if (isInvalidProduct(product)) continue; + groups.computeIfAbsent(product.getProductID(), k -> new ArrayList<>()).add(slot); + } - String productId = product.getProductID(); - SlotStructure existingSlot = uniqueProductMap.get(productId); - - if (existingSlot == null) { - // 直接使用拷贝后的 slot(避免修改) - uniqueProductMap.put(productId, slot); - } else { - // 數量累加 - int newCount = existingSlot.getCount() + slot.getCount(); - - // 判斷代表 slot:如果 existingSlot 數量是 0,而這個 slot 有貨,就換掉 - if (existingSlot.getCount() == 0 && slot.getCount() > 0) { - SlotStructure newSlot = CloneUtils.deepClone(slot, SlotStructure.class); - newSlot.setCount(newCount); - uniqueProductMap.put(productId, newSlot); - } else { - // 否則保留原本的貨道,只更新數量 - SlotStructure newSlot = CloneUtils.deepClone(existingSlot, SlotStructure.class); - newSlot.setCount(newCount); - uniqueProductMap.put(productId, newSlot); + // 3. 每個商品產生一張卡:可售判定看「同商品任一可賣貨道(count>0 && !lock && !expired)」, + // 只有全部貨道都不可賣時才鎖整張卡。避免「鎖一個貨道/單一貨道過期」就把整個商品擋掉。 + Map uniqueProductMap = new LinkedHashMap<>(); + for (Map.Entry> entry : groups.entrySet()) { + List group = entry.getValue(); + int sellableCount = 0; // 可賣貨道庫存加總(顯示與可加購上限,排除鎖定/過期/0 庫存,防買超) + SlotStructure sellableRep = null; // 第一個可賣貨道 + SlotStructure lockedRep = null; // 第一個被鎖貨道(全不可賣時用來顯示「暫停販售」) + SlotStructure expiredRep = null; // 第一個過期貨道(全不可賣時用來顯示「暫不販售」) + for (SlotStructure s : group) { + if (s.getCount() > 0 && !s.isLock() && !s.isExpired()) { + sellableCount += s.getCount(); + if (sellableRep == null) sellableRep = s; + } else if (s.isLock()) { + if (lockedRep == null) lockedRep = s; + } else if (s.isExpired()) { + if (expiredRep == null) expiredRep = s; } } + + SlotStructure card; + if (sellableRep != null) { + // 有可賣貨道:用可賣道當代表(不鎖、不過期),庫存只算可賣道加總 + card = CloneUtils.deepClone(sellableRep, SlotStructure.class); + card.setCount(sellableCount); + } else { + // 全部不可賣才鎖整卡:依 鎖定 > 過期 > 售完 擇一代表道,讓遮罩顯示正確原因 + SlotStructure rep = lockedRep != null ? lockedRep + : (expiredRep != null ? expiredRep : group.get(0)); + card = CloneUtils.deepClone(rep, SlotStructure.class); + } + uniqueProductMap.put(entry.getKey(), card); } - // 3. 更新UI + // 4. 更新UI if (!uniqueProductMap.isEmpty()) { slotList.clear(); slotList.addAll(uniqueProductMap.values()); diff --git a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/DialogProductDetail.java b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/DialogProductDetail.java index e00263f..176f43f 100644 --- a/app/src/main/java/com/unibuy/smartdevice/ui/dialog/DialogProductDetail.java +++ b/app/src/main/java/com/unibuy/smartdevice/ui/dialog/DialogProductDetail.java @@ -8,7 +8,6 @@ import com.unibuy.smartdevice.MyApp; import com.unibuy.smartdevice.R; import com.unibuy.smartdevice.databinding.DialogProductDetailBinding; import com.unibuy.smartdevice.exception.LogsEmptyException; -import com.unibuy.smartdevice.structure.BuyStructure; import com.unibuy.smartdevice.structure.SlotStructure; import com.unibuy.smartdevice.tools.HandlerMain; import com.unibuy.smartdevice.tools.ToastHandlerMain; @@ -181,8 +180,9 @@ public class DialogProductDetail extends DialogAbstract { cancel(); } else { MyApp.getInstance().getBuyList().clear(); - MyApp.getInstance().getBuyList().add( - new BuyStructure(slot.getField(), slot.getSlot(), slot.getProduct(), quantity)); + // 依可賣貨道把數量拆成多筆 count=1,規避 VMC 出貨狀態機不支援單筆 quantity>1(會卡 40 秒看門狗)。 + MyApp.getInstance().getBuyList().addAll( + MyApp.getInstance().allocateBuyStructures(slot, quantity)); cancel(); requestBuy(); } diff --git a/app/src/main/java/com/unibuy/v2/vm/CartViewModel.kt b/app/src/main/java/com/unibuy/v2/vm/CartViewModel.kt index 272242f..1e567bd 100644 --- a/app/src/main/java/com/unibuy/v2/vm/CartViewModel.kt +++ b/app/src/main/java/com/unibuy/v2/vm/CartViewModel.kt @@ -30,11 +30,12 @@ class CartViewModel : ViewModel() { buyList.clear() // 1. 免費商品(來店禮;General 不做時恆空) buyList += freeGoods - // 2. 購物車商品 + // 2. 購物車商品:依可賣貨道把數量拆成多筆 count=1,規避 VMC 出貨狀態機不支援單筆 quantity>1 + // (出完第 1 個後不會自動觸發下一次出貨,會卡 40 秒看門狗)。拆道邏輯集中在 MyApp.allocateBuyStructures。 buyList += UnibuyHelper.getCartViewModel() .getCartItemsSnapshot() - .map { cartItem -> - BuyStructure(cartItem.field, cartItem.slot, cartItem.product, cartItem.cartSelectedCount) + .flatMap { cartItem -> + allocateBuyStructures(cartItem, cartItem.cartSelectedCount) } } }