Enhance product import validation by adding checks for duplicate country and weight band entries. Update related error messages in properties file. Introduce new method to delete auto fees while retaining manual entries during rebill operations. Update front-end components to manage build mode changes and ensure proper handling of product lists during price set creation.

This commit is contained in:
wuweihua
2026-10-09 15:51:21 +08:00
parent 94e7a18e0b
commit 853187ad58
11 changed files with 519 additions and 54 deletions
@@ -47,4 +47,7 @@ public interface PrcIncomeBillFeeMapper
public int deleteByFeeId(String feeId);
public int deleteByBillId(String billId);
/** Rebill: delete auto fees only; keep manual_flag=1 rows. */
public int deleteAutoFeesByBillId(String billId);
}
@@ -309,6 +309,9 @@ public class BmsProductServiceImpl implements IBmsProductService
}
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
// Pre-check: same product cannot have duplicate country + weight band in Excel
assertImportProductsNoDupCountryWeight(workbook, products, regionByNameCn, euRegions,
zoneByCountryAndCode);
int successNum = 0;
int skipNum = 0;
@@ -460,6 +463,7 @@ public class BmsProductServiceImpl implements IBmsProductService
product.setTaxMode(resolveTaxMode(product.getProductName()));
}
List<BmsProductCountry> countries = stampSelectedCountry(block.rows, region, forcedEff);
assertNoDuplicateCountryWeightRows(product, countries);
int[] nums = persistImportedProduct(product, countries, operName);
transactionManager.commit(tx);
successNum += nums[0];
@@ -597,6 +601,7 @@ public class BmsProductServiceImpl implements IBmsProductService
{
continue;
}
assertNoDuplicateCountryWeightRows(product, rows);
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
out.put(code, rows);
}
@@ -632,6 +637,7 @@ public class BmsProductServiceImpl implements IBmsProductService
throw new ServiceException(MessageUtils.message("price.product.import.noCountry",
productSheet.getSheetName()));
}
assertNoDuplicateCountryWeightRows(product, countries);
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
Date newEff = requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
: product.getEffectiveDate());
@@ -1988,6 +1994,134 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
/**
* Workbook pre-check for full import: reject before any write when a product sheet
* has duplicate country + weight band (same zone) rows.
*/
private void assertImportProductsNoDupCountryWeight(Workbook workbook, List<BmsProduct> products,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
Map<String, PrcZone> zoneByCountryAndCode)
{
if (workbook == null || products == null || products.isEmpty())
{
return;
}
List<String> tips = new ArrayList<String>();
Set<String> unmatchedCountries = new HashSet<String>();
for (BmsProduct product : products)
{
if (product == null || StringUtils.isEmpty(product.getProductCode()))
{
continue;
}
Sheet productSheet = findProductSheet(workbook, product);
if (productSheet == null)
{
continue;
}
List<BmsProductCountry> rows = parseProductSheet(productSheet, product,
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
String tip = buildDuplicateCountryWeightTip(product, rows);
if (StringUtils.isNotEmpty(tip))
{
tips.add(tip);
}
}
if (!tips.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.dupCountry",
String.join("<br/>", tips)));
}
}
/** Fail when Excel rows for one product contain duplicate country + weight band. */
private void assertNoDuplicateCountryWeightRows(BmsProduct product, List<BmsProductCountry> rows)
{
String tip = buildDuplicateCountryWeightTip(product, rows);
if (StringUtils.isNotEmpty(tip))
{
throw new ServiceException(tip);
}
}
private String buildDuplicateCountryWeightTip(BmsProduct product, List<BmsProductCountry> rows)
{
List<String> dups = findDuplicateCountryWeightLabels(rows);
if (dups.isEmpty())
{
return null;
}
String code = product == null || StringUtils.isEmpty(product.getProductCode())
? "-" : product.getProductCode();
String name = product == null || StringUtils.isEmpty(product.getProductName())
? "-" : product.getProductName();
return MessageUtils.message("price.product.import.dupCountry.item",
code, name, String.join("\u3001", dups));
}
private List<String> findDuplicateCountryWeightLabels(List<BmsProductCountry> rows)
{
List<String> dups = new ArrayList<String>();
if (rows == null || rows.isEmpty())
{
return dups;
}
Map<String, Integer> counts = new LinkedHashMap<String, Integer>();
Map<String, String> labels = new LinkedHashMap<String, String>();
for (BmsProductCountry c : rows)
{
if (c == null)
{
continue;
}
String key = countryWeightDupKey(c);
Integer n = counts.get(key);
counts.put(key, n == null ? 1 : n.intValue() + 1);
if (!labels.containsKey(key))
{
labels.put(key, formatCountryWeightTip(c));
}
}
for (Map.Entry<String, Integer> e : counts.entrySet())
{
if (e.getValue() != null && e.getValue().intValue() > 1)
{
dups.add(labels.get(e.getKey()));
}
}
return dups;
}
/** Align with uk_product_country_zone_weight_eff (country + zone + weight min/max). */
private String countryWeightDupKey(BmsProductCountry c)
{
String country = StringUtils.isNotEmpty(c.getCountryCode())
? c.getCountryCode().trim().toUpperCase(Locale.ROOT)
: (c.getCountryName() == null ? "" : c.getCountryName().trim());
String zone = c.getZoneCode() == null ? "" : c.getZoneCode().trim().toUpperCase(Locale.ROOT);
return country + "|" + zone + "|" + decimalKey(c.getWeightMin()) + "|" + decimalKey(c.getWeightMax());
}
private String formatCountryWeightTip(BmsProductCountry c)
{
String name = StringUtils.isNotEmpty(c.getCountryName()) ? c.getCountryName()
: (StringUtils.isNotEmpty(c.getCountryCode()) ? c.getCountryCode() : "-");
if (StringUtils.isNotEmpty(c.getZoneCode()))
{
name = name + "(" + c.getZoneCode() + ")";
}
return name + " " + decimalKey(c.getWeightMin()) + "~" + decimalKey(c.getWeightMax());
}
private String decimalKey(BigDecimal v)
{
if (v == null)
{
return "";
}
return v.stripTrailingZeros().toPlainString();
}
/** Stamp product + all country detail rows with the same effective date. */
private void applyForcedEffectiveDate(BmsProduct product, List<BmsProductCountry> countries, Date newEff)
{
@@ -8,8 +8,10 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
@@ -1115,18 +1117,39 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
else
{
orderBillMapper.updatePrcOrderBill(bill);
incomeBillFeeMapper.deleteByBillId(bill.getBillId());
// Keep manually added fees (manual_flag=1) across rebill
incomeBillFeeMapper.deleteAutoFeesByBillId(bill.getBillId());
// Payable not in scope yet: skip prc_pay_bill_fee cleanup/write.
// payBillFeeMapper.deleteByBillId(bill.getBillId());
}
PrcIncomeBill incomeBill = persistIncomeBill(bill, result);
List<PrcIncomeBillFee> fees = toFeeRows(bill.getBillId(),
incomeBill == null ? null : incomeBill.getIncomeBillId(), result);
if (existed != null)
{
fees = filterAutoFeesAgainstManual(bill.getBillId(), fees);
}
if (!fees.isEmpty())
{
incomeBillFeeMapper.insertBatch(fees);
}
orderBillMapper.refreshArBillStatusByBillIds(Arrays.asList(bill.getBillId()));
if (existed != null)
{
// Quote totals were written above; recalc so retained manual fees join income/credit
TmsPriceQuoteResult refreshed = refreshBillIncomeTotals(bill);
if (refreshed != null)
{
result.setSurchargeAmount(refreshed.getSurchargeAmount());
result.setIncomeTotal(refreshed.getIncomeTotal());
result.setPayableTotal(refreshed.getPayableTotal());
result.setCreditArrearsStatus(refreshed.getCreditArrearsStatus());
result.setFeeDetails(refreshed.getFeeDetails());
}
}
else
{
orderBillMapper.refreshArBillStatusByBillIds(Arrays.asList(bill.getBillId()));
}
// Payable not in scope yet: do not write prc_pay_bill / prc_pay_bill_fee.
// PrcPayBill payBill = persistPayBill(bill, result);
// List<PrcPayBillFee> payFees = toPayFeeRows(bill.getBillId(),
@@ -1141,6 +1164,53 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
billSearchStore.save(EsLogType.BILL, EsBizLogSupport.billBizId(request), requestJson, toResponseJson(result));
}
/**
* Drop auto fee lines that collide with retained manual rows on uk_bill_fee_item
* (bill_id, fee_type, sc_id). Manual fees win on rebill.
*/
private List<PrcIncomeBillFee> filterAutoFeesAgainstManual(String billId, List<PrcIncomeBillFee> autoFees)
{
if (autoFees == null || autoFees.isEmpty() || StringUtils.isEmpty(billId))
{
return autoFees == null ? new ArrayList<PrcIncomeBillFee>() : autoFees;
}
List<PrcIncomeBillFee> existing = incomeBillFeeMapper.selectByBillId(billId);
if (existing == null || existing.isEmpty())
{
return autoFees;
}
Set<String> manualKeys = new HashSet<String>();
for (PrcIncomeBillFee row : existing)
{
if (row != null && "1".equals(row.getManualFlag()))
{
manualKeys.add(feeUniqueKey(row.getFeeType(), row.getScId()));
}
}
if (manualKeys.isEmpty())
{
return autoFees;
}
List<PrcIncomeBillFee> out = new ArrayList<PrcIncomeBillFee>();
for (PrcIncomeBillFee fee : autoFees)
{
if (fee == null)
{
continue;
}
if (!manualKeys.contains(feeUniqueKey(fee.getFeeType(), fee.getScId())))
{
out.add(fee);
}
}
return out;
}
private String feeUniqueKey(String feeType, Long scId)
{
return StringUtils.nvl(feeType, "") + "|" + (scId == null ? "0" : String.valueOf(scId));
}
/**
* Occupy/release credit by income total delta (bizType=ORDER).
* Missing/disabled customer: skip occupy, return NORMAL.
@@ -2733,6 +2733,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
version.setExpireTime(db.getExpireTime());
}
// Draft + build mode changed: wipe old product/detail/surcharge before mode switch
clearDraftVersionChildrenIfBuildModeChanged(db, version);
versionMapper.updatePrcPriceSetVersion(version);
}
set.setVersion(version);
@@ -2749,6 +2751,36 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
refreshVersionStatus(set.getPriceSetId());
}
/**
* Draft only: when priceBuildMode changes (PULL/IMPORT), delete this version's
* product / detail / surcharge rows. Same mode or non-draft: no-op.
*/
private void clearDraftVersionChildrenIfBuildModeChanged(PrcPriceSetVersion db, PrcPriceSetVersion incoming)
{
if (db == null || incoming == null || db.getPsvId() == null)
{
return;
}
if (!VER_DRAFT.equals(db.getStatus()))
{
return;
}
String newMode = StringUtils.trim(incoming.getPriceBuildMode());
if (StringUtils.isEmpty(newMode))
{
return;
}
String oldMode = StringUtils.trim(db.getPriceBuildMode());
if (newMode.equalsIgnoreCase(StringUtils.nvl(oldMode, "")))
{
return;
}
Long psvId = db.getPsvId();
versionSurchargeMapper.deleteByPsvId(psvId);
detailMapper.deletePrcPriceSetDetailByPsvId(psvId);
setProductMapper.deleteByPsvId(psvId);
}
/** Prefer same effective_time, else draft, else first version of the set. */
private PrcPriceSetVersion findReusableVersion(Long priceSetId, Date effectiveTime)
{
@@ -180,4 +180,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<delete id="deleteByBillId" parameterType="String">
delete from prc_income_bill_fee where bill_id = #{billId}
</delete>
<delete id="deleteAutoFeesByBillId" parameterType="String">
delete from prc_income_bill_fee
where bill_id = #{billId}
and (manual_flag is null or manual_flag &lt;&gt; '1')
</delete>
</mapper>