[FIX] 鎖貨道/效期只鎖該貨道不再鎖整商品,同商品多貨道改逐道出貨
1. VmcFragment.updateFlowerBuyList():銷售卡合併同商品多貨道改為「任一可賣貨道(count>0且未鎖未過期)即可售」,庫存只計可賣貨道加總;全部不可賣才依 鎖定>過期>售完 顯示對應遮罩。修正鎖一個貨道或單一貨道過期就把整個商品擋掉的問題。 2. MyApp 新增 allocateBuyStructures():把購買數量依可賣貨道 greedy 拆成多筆 count=1 的 BuyStructure,規避 VMC 出貨狀態機不支援單筆 quantity>1(出完第一個會卡 40 秒看門狗);庫存不足才溢位回代表道,交既有「出貨失敗→後台依 dispense 退款」保護網。 3. CartViewModel.convertToBuyStructure():多品項購物車改用 allocateBuyStructures 拆道,避免同商品買多個時重複出同一貨道而卡頓/出貨失敗。 4. DialogProductDetail 直購:改走 allocateBuyStructures 拆道。 5. PickupSheetSaleFlow:中國醫領藥 buildMaterialSlotMap 與 isAvailableCmuhStock 排除鎖定/過期藥道,避免藥單驗證通過卻在出貨時失敗。 6. build.gradle:verPatch 18→19(versionName 依硬體規格產生)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8db3fab1c0
commit
90f86cf3c9
@ -10,7 +10,7 @@ plugins {
|
||||
// 避免不同尺寸機台間版本號產生衝突。
|
||||
// =========================================================================
|
||||
def verMinor = 2
|
||||
def verPatch = 18
|
||||
def verPatch = 19
|
||||
|
||||
|
||||
|
||||
|
||||
@ -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<quantity,
|
||||
* 狀態機不會主動再觸發下一次出貨,只能等 40 秒看門狗逾時被動補出,形同卡死)。因此多數量必須在
|
||||
* 進狀態機之前就拆成「一筆一個」,與中國醫領藥既有做法一致。
|
||||
*
|
||||
* greedy 分配:優先用代表貨道(representative),再依貨道號遞增溢位到下一個可賣道,盡量集中以維持
|
||||
* 貨道補貨整齊;單道不足才換道。可賣總庫存若仍不足 quantity(例如加入購物車後庫存又被消耗),
|
||||
* 剩餘數量掛回代表貨道,交由既有「出貨失敗→後台依 dispense 退款」保護網處理,不在此處吞掉訂單金額。
|
||||
*
|
||||
* 注意:representative 可能是銷售畫面合併後的「代表卡」(getCount() 為加總值),所以這裡一律以
|
||||
* getSlotList(field) 的真實逐道庫存為準,不信任 representative.getCount()。
|
||||
*/
|
||||
public List<BuyStructure> allocateBuyStructures(SlotStructure representative, int quantity) {
|
||||
List<BuyStructure> 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<SlotStructure> others = new ArrayList<>();
|
||||
List<SlotStructure> 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<SlotStructure> 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<MarketingPlanStructure> getMarketingPlanList() {
|
||||
return marketingPlanList;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -267,36 +267,48 @@ public class VmcFragment extends FragmentAbstract {
|
||||
new TypeToken<List<SlotStructure>>() {}.getType()
|
||||
);
|
||||
if (rawSlots == null || rawSlots.isEmpty()) return;
|
||||
// 2. 处理数据(确保不修改任何对象)
|
||||
Map<String, SlotStructure> uniqueProductMap = new LinkedHashMap<>();
|
||||
// 2. 按 ProductID 分組(同一商品的多個貨道收成一組)
|
||||
LinkedHashMap<String, List<SlotStructure>> 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<String, SlotStructure> uniqueProductMap = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<SlotStructure>> entry : groups.entrySet()) {
|
||||
List<SlotStructure> 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());
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user