diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java index 07074984..1bbcdb69 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java @@ -78,7 +78,8 @@ public class TmsPriceController extends BaseController /** * Sync order and persist prc_order_bill / prc_income_bill_fee. - * Same price match as quote. Same orderNo upserts. + * Same price match as quote (bound set + active version by bizDate, else default). + * Same orderNo upserts. */ @PostMapping("/bill") public AjaxResult bill( diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcPriceSetDetail.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcPriceSetDetail.java index b6347a69..15ef17f5 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcPriceSetDetail.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcPriceSetDetail.java @@ -53,4 +53,10 @@ public class PrcPriceSetDetail implements Serializable private Date updateTime; /** query only: product/country/zone/weight keyword */ private String keyword; + /** query only: product code/name like */ + private String productKeyword; + /** query only: country code/name like */ + private String countryKeyword; + /** query only: zone code/name like */ + private String zoneKeyword; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java index ab5ad279..d3b4b1dc 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java @@ -25,6 +25,12 @@ public class TmsPriceQuoteResult implements Serializable private String setType; private Integer bindPriority; private Long customerPricesetId; + /** Hit package version id (prc_price_set_version.psv_id) */ + private Long psvId; + /** Hit package version no (e.g. V1) */ + private String versionNo; + /** Hit package detail id when package version detail matched */ + private Long psdId; private String productCode; private String productName; diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcCustomerPriceSetMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcCustomerPriceSetMapper.java index 2056cb42..e4f1f229 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcCustomerPriceSetMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcCustomerPriceSetMapper.java @@ -40,7 +40,7 @@ public interface PrcCustomerPriceSetMapper @Param("endDate") java.util.Date endDate); /** - * 客户在业务日有效、且套餐状态为生效(E) 的绑定,按优先级升序 + * Customer binds valid on bizDate; package not stopped. Version match is done in quote service. */ public java.util.List selectEffectiveByCustomer( @Param("customerId") Long customerId, diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcPriceSetDetailMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcPriceSetDetailMapper.java index 404bb492..33528a98 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcPriceSetDetailMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcPriceSetDetailMapper.java @@ -53,9 +53,10 @@ public interface PrcPriceSetDetailMapper @Param("priceSetId") Long priceSetId, @Param("psvId") Long psvId); - /** FIXED: product + country + zone + productCountryId / weight */ + /** FIXED: product + country + zone + productCountryId / weight; optional psvId scopes to version */ public PrcPriceSetDetail selectFixedMatch( @Param("priceSetId") Long priceSetId, + @Param("psvId") Long psvId, @Param("productCode") String productCode, @Param("countryCode") String countryCode, @Param("countryName") String countryName, @@ -63,8 +64,9 @@ public interface PrcPriceSetDetailMapper @Param("productCountryId") Long productCountryId, @Param("weightLabel") String weightLabel); - /** DISCOUNT:按产品命中明细折扣(可空) */ + /** DISCOUNT by product; optional psvId scopes to version */ public PrcPriceSetDetail selectDiscountMatch( @Param("priceSetId") Long priceSetId, + @Param("psvId") Long psvId, @Param("productCode") String productCode); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcPriceSetVersionMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcPriceSetVersionMapper.java index 24003232..28fda9ef 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcPriceSetVersionMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcPriceSetVersionMapper.java @@ -1,5 +1,6 @@ package com.ruoyi.price.mapper; +import java.util.Date; import java.util.List; import org.apache.ibatis.annotations.Param; import com.ruoyi.price.domain.PrcPriceSetVersion; @@ -24,4 +25,12 @@ public interface PrcPriceSetVersionMapper public int deletePrcPriceSetVersionByPriceSetId(Long priceSetId); public List selectPriceSetIds(); + + /** + * Version effective at asOf: effective_time <= asOf and (expire_time null or > asOf). + * Prefer latest effective_time. + */ + public PrcPriceSetVersion selectActiveByPriceSetAt( + @Param("priceSetId") Long priceSetId, + @Param("asOf") Date asOf); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java index d8486d1a..caf1e639 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java @@ -6,11 +6,14 @@ import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; import java.util.List; /** - * TMS freight quote service. + * TMS freight quote / bill calc service (shared by console trial and upstream bill). * - * Match order: effective customer package (bind_priority ASC) FIXED override - * then DISCOUNT, else bms_product_country default freight/registration. - * After calc, persist fee lines by bms_fee_type; same trial params return stored result. + * Match order: + * 1) Customer binds (not stopped), bind_priority ASC + * 2) Active package version at bill-time end-of-day (effective_time / expire_time) + * 3) FIXED detail override, else DISCOUNT on that version + * 4) Else bms_product_country default + * Package hit uses version surcharge snapshot when present; else product library. * * @author ruoyi */ diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java index 48fe1f85..15b3376f 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java @@ -824,37 +824,101 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService reportSaveProgress(set, 90, "saving customers"); if (set.getCustomers() != null) { - customerMapper.deletePrcCustomerPriceSetByPriceSetId(set.getPriceSetId()); - Set seenCustomers = new HashSet(); - for (PrcCustomerPriceSet c : filterCustomers(set.getCustomers())) + upsertCustomers(set); + } + reportSaveProgress(set, 96, "finishing"); + } + + /** Upsert customer binds by customerPricesetId / customerCode; delete removed ones. */ + private void upsertCustomers(PrcPriceSet set) + { + PrcCustomerPriceSet cquery = new PrcCustomerPriceSet(); + cquery.setPriceSetId(set.getPriceSetId()); + List existing = customerMapper.selectPrcCustomerPriceSetList(cquery); + Map byId = new HashMap(); + Map byCode = new HashMap(); + if (existing != null) + { + for (PrcCustomerPriceSet e : existing) { - if (StringUtils.isNotEmpty(c.getCustomerCode()) && !seenCustomers.add(c.getCustomerCode())) + if (e == null) { - throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound")); + continue; } - validateCustomer(c, set); - c.setCustomerPricesetId(null); - c.setPriceSetId(set.getPriceSetId()); - c.setCreateBy(set.getCreateBy()); - if (c.getBindPriority() == null) + if (e.getCustomerPricesetId() != null) { - c.setBindPriority(1); + byId.put(e.getCustomerPricesetId(), e); } - try + if (StringUtils.isNotEmpty(e.getCustomerCode())) { - customerMapper.insertPrcCustomerPriceSet(c); - } - catch (Exception e) - { - if (isCustomerUniqueViolation(e)) - { - throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound")); - } - throw e; + byCode.put(e.getCustomerCode(), e); } } } - reportSaveProgress(set, 96, "finishing"); + Set keepIds = new HashSet(); + Set seenCustomers = new HashSet(); + for (PrcCustomerPriceSet c : filterCustomers(set.getCustomers())) + { + if (StringUtils.isNotEmpty(c.getCustomerCode()) && !seenCustomers.add(c.getCustomerCode())) + { + throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound")); + } + validateCustomer(c, set); + c.setPriceSetId(set.getPriceSetId()); + if (c.getBindPriority() == null) + { + c.setBindPriority(1); + } + PrcCustomerPriceSet hit = null; + if (c.getCustomerPricesetId() != null) + { + hit = byId.get(c.getCustomerPricesetId()); + } + if (hit == null && StringUtils.isNotEmpty(c.getCustomerCode())) + { + hit = byCode.get(c.getCustomerCode()); + } + try + { + if (hit != null) + { + c.setCustomerPricesetId(hit.getCustomerPricesetId()); + c.setUpdateBy(set.getUpdateBy()); + customerMapper.updatePrcCustomerPriceSet(c); + keepIds.add(hit.getCustomerPricesetId()); + } + else + { + c.setCustomerPricesetId(null); + c.setCreateBy(set.getCreateBy()); + customerMapper.insertPrcCustomerPriceSet(c); + if (c.getCustomerPricesetId() != null) + { + keepIds.add(c.getCustomerPricesetId()); + } + } + } + catch (Exception e) + { + if (isCustomerUniqueViolation(e)) + { + throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound")); + } + throw e; + } + } + List orphanIds = new ArrayList(); + for (Long id : byId.keySet()) + { + if (!keepIds.contains(id)) + { + orphanIds.add(id); + } + } + if (!orphanIds.isEmpty()) + { + customerMapper.deletePrcCustomerPriceSetByIds(orphanIds.toArray(new Long[0])); + } } private void reportSaveProgress(PrcPriceSet set, int percent, String message) @@ -1466,7 +1530,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { PrcPriceSetVersion version = resolveSaveVersion(set); boolean pullLibrary = "Y".equalsIgnoreCase(set.getPullFromLibrary()); + // null details = leave existing rows alone; empty list = clear / replace with nothing + boolean detailsProvided = set.getDetails() != null; List details = filterDetails(set.getDetails()); + if (version == null && !detailsProvided && !pullLibrary) + { + return; + } if (version == null && details.isEmpty() && !pullLibrary) { return; @@ -1476,7 +1546,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService version = new PrcPriceSetVersion(); version.setEffectiveTime(set.getEffectiveDate()); } - // Draft versions may omit effective_time; set on activate (启动) + // Draft versions may omit effective_time; set on activate version.setPriceSetId(set.getPriceSetId()); version.setCreateBy(set.getCreateBy()); version.setUpdateBy(set.getUpdateBy()); @@ -1522,11 +1592,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { replaceVersionDetailsFromLibrary(set, version); } + else if (detailsProvided) + { + upsertVersionDetails(set, version, details); + reportSaveProgress(set, 85, "saving surcharges"); + resnapshotSurchargesIfNeeded(version, details); + } else { - replaceVersionDetails(set, version, details); - reportSaveProgress(set, 85, "saving surcharges"); - snapshotVersionSurcharges(version, details); + reportSaveProgress(set, 80, "details unchanged"); } refreshVersionStatus(set.getPriceSetId()); } @@ -1615,33 +1689,103 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService fillVersionChildren(version); } - private void replaceVersionDetails(PrcPriceSet set, PrcPriceSetVersion version, List details) + /** + * Upsert details by psdId. Keeps existing rows when id matches; inserts new; deletes orphans + * not present in the payload. Avoids full delete+reinsert. + */ + private void upsertVersionDetails(PrcPriceSet set, PrcPriceSetVersion version, List details) { - versionSurchargeMapper.deleteByPsvId(version.getPsvId()); - detailMapper.deletePrcPriceSetDetailByPsvId(version.getPsvId()); - int total = details == null ? 0 : details.size(); - int index = 0; - for (PrcPriceSetDetail d : details) + PrcPriceSetDetail query = new PrcPriceSetDetail(); + query.setPsvId(version.getPsvId()); + List existing = detailMapper.selectPrcPriceSetDetailList(query); + Map byId = new HashMap(); + if (existing != null) { - validateDetail(set, d); - d.setPsdId(null); - d.setPriceSetId(set.getPriceSetId()); - d.setPsvId(version.getPsvId()); - detailMapper.insertPrcPriceSetDetail(d); - index++; - if (total > 0 && (index == total || index % 50 == 0 || index == 1)) + for (PrcPriceSetDetail e : existing) { - int pct = 25 + (int) Math.floor(index * 55.0 / total); - if (pct > 80) + if (e != null && e.getPsdId() != null) { - pct = 80; + byId.put(e.getPsdId(), e); } - reportSaveProgress(set, pct, "saving details " + index + "/" + total); } } + Set keepIds = new HashSet(); + int total = details == null ? 0 : details.size(); + int index = 0; + if (details != null) + { + for (PrcPriceSetDetail d : details) + { + validateDetail(set, d); + d.setPriceSetId(set.getPriceSetId()); + d.setPsvId(version.getPsvId()); + if (d.getPsdId() != null && byId.containsKey(d.getPsdId())) + { + detailMapper.updatePrcPriceSetDetail(d); + keepIds.add(d.getPsdId()); + } + else + { + d.setPsdId(null); + detailMapper.insertPrcPriceSetDetail(d); + if (d.getPsdId() != null) + { + keepIds.add(d.getPsdId()); + } + } + index++; + if (total > 0 && (index == total || index % 50 == 0 || index == 1)) + { + int pct = 25 + (int) Math.floor(index * 55.0 / total); + if (pct > 80) + { + pct = 80; + } + reportSaveProgress(set, pct, "saving details " + index + "/" + total); + } + } + } + List orphanIds = new ArrayList(); + for (Long id : byId.keySet()) + { + if (!keepIds.contains(id)) + { + orphanIds.add(id); + } + } + if (!orphanIds.isEmpty()) + { + for (Long id : orphanIds) + { + versionSurchargeMapper.deleteByPsdId(id); + } + detailMapper.deletePrcPriceSetDetailByIds(orphanIds.toArray(new Long[0])); + } version.setDetails(details); } + /** Only rewrite surcharge snapshots for rows that explicitly sent surcharges. */ + private void resnapshotSurchargesIfNeeded(PrcPriceSetVersion version, List details) + { + if (details == null || details.isEmpty()) + { + return; + } + List withSc = new ArrayList(); + for (PrcPriceSetDetail d : details) + { + if (d != null && d.getPsdId() != null && d.getSurcharges() != null) + { + versionSurchargeMapper.deleteByPsdId(d.getPsdId()); + withSc.add(d); + } + } + if (!withSc.isEmpty()) + { + snapshotVersionSurcharges(version, withSc); + } + } + private void snapshotVersionSurcharges(PrcPriceSetVersion version, List details) { if (details == null || details.isEmpty()) diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java index f33591df..ce0e0750 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -5,6 +5,7 @@ import java.math.RoundingMode; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; @@ -25,6 +26,8 @@ import com.ruoyi.price.domain.BmsProductCountry; import com.ruoyi.price.domain.PrcCustomerPriceSet; import com.ruoyi.price.domain.PrcPriceSet; import com.ruoyi.price.domain.PrcPriceSetDetail; +import com.ruoyi.price.domain.PrcPriceSetVersion; +import com.ruoyi.price.domain.PrcPriceSetVersionSurcharge; import com.ruoyi.price.domain.PrcQuote; import com.ruoyi.price.domain.PrcQuoteFee; import com.ruoyi.price.domain.PrcSurchargeRel; @@ -38,6 +41,8 @@ import com.ruoyi.price.mapper.BmsProductMapper; import com.ruoyi.price.mapper.PrcCustomerPriceSetMapper; import com.ruoyi.price.mapper.PrcPriceSetDetailMapper; import com.ruoyi.price.mapper.PrcPriceSetMapper; +import com.ruoyi.price.mapper.PrcPriceSetVersionMapper; +import com.ruoyi.price.mapper.PrcPriceSetVersionSurchargeMapper; import com.ruoyi.price.mapper.PrcQuoteFeeMapper; import com.ruoyi.price.mapper.PrcQuoteMapper; import com.ruoyi.price.mapper.PrcSurchargeRelMapper; @@ -47,8 +52,8 @@ import com.ruoyi.price.search.IBillSearchStore; import com.ruoyi.price.service.ITmsPriceQuoteService; /** - * TMS freight quote: customer package first, then product default. - * After calc: persist fee lines by bms_fee_type; same trial params return stored rows. + * Shared quote/bill calc: bound set + active version by bizDate, else product default. + * Quote persists prc_quote; bill path calls calculate() then persists prc_order_bill. * * @author ruoyi */ @@ -71,6 +76,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService @Autowired private PrcPriceSetMapper priceSetMapper; + @Autowired + private PrcPriceSetVersionMapper versionMapper; + + @Autowired + private PrcPriceSetVersionSurchargeMapper versionSurchargeMapper; + @Autowired private PrcPriceSetDetailMapper detailMapper; @@ -249,17 +260,24 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService if (binds != null) { + Date asOf = endOfDay(bizDate); for (PrcCustomerPriceSet bind : binds) { PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId()); - if (set == null || !"E".equals(set.getStatus())) + if (set == null || "S".equals(set.getStatus())) { continue; } + PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf); + if (version == null || version.getPsvId() == null) + { + // Bound package but no version effective at bill time -> try next bind / default + continue; + } String setType = StringUtils.isEmpty(set.getSetType()) ? TYPE_FIXED : set.getSetType(); if (TYPE_FIXED.equals(setType)) { - TmsPriceQuoteResult hit = tryFixed(bind, set, base, productCode, productName, weight); + TmsPriceQuoteResult hit = tryFixed(bind, set, version, base, productCode, productName, weight); if (hit != null) { stampRequestMeta(request, hit); @@ -268,7 +286,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } else if (TYPE_DISCOUNT.equals(setType)) { - TmsPriceQuoteResult hit = tryDiscount(bind, set, base, productCode, productName, weight); + TmsPriceQuoteResult hit = tryDiscount(bind, set, version, base, productCode, productName, weight); if (hit != null) { stampRequestMeta(request, hit); @@ -334,23 +352,25 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return n; } - private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set, + private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set, PrcPriceSetVersion version, BmsProductCountry base, String productCode, String productName, BigDecimal weight) { - int detailCount = detailMapper.countByPriceSetId(set.getPriceSetId()); + Long psvId = version.getPsvId(); + int detailCount = detailMapper.countByPsvId(psvId); if (detailCount == 0) { TmsPriceQuoteResult r = buildFromRates(SRC_DEFAULT, base.getFreightPrice(), null, null, base.getRegistrationFee(), base, productCode, productName, weight, null); fillPackage(r, bind, set); - // 套餐固定价格没有明细,使用产品默认价格 + stampVersion(r, version, null); + // Package fixed has no version details; use product default rates r.setMatchMessage(MessageUtils.message("price.quote.fixedPackageNoDetails")); r.setCustomerId(bind.getCustomerId()); return r; } PrcPriceSetDetail detail = detailMapper.selectFixedMatch( - set.getPriceSetId(), productCode, + set.getPriceSetId(), psvId, productCode, base.getCountryCode(), base.getCountryName(), base.getZoneCode(), base.getDetailId(), formatWeightLabel(base.getWeightMin(), base.getWeightMax())); @@ -365,6 +385,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService TmsPriceQuoteResult r = buildFromRates(SRC_FIXED, detail.getFvValue(), null, null, detail.getFvRegistrationFee(), base, productCode, productName, weight, detail.getRoundMode()); fillPackage(r, bind, set); + stampVersion(r, version, detail.getPsdId()); if (StringUtils.isNotEmpty(detail.getProductName())) { r.setProductName(detail.getProductName()); @@ -377,25 +398,28 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService { r.setProductCountryId(detail.getProductCountryId()); } - // 命中客户套餐固定价格明细 - r.setMatchMessage(MessageUtils.message("price.quote.hitCustomerFixedPackageDetail")); + r.setMatchMessage(MessageUtils.message("price.quote.hitCustomerFixedPackageDetail") + + " [" + (version.getVersionNo() == null ? ("psv=" + psvId) : version.getVersionNo()) + "]"); r.setCustomerId(bind.getCustomerId()); return r; } - private TmsPriceQuoteResult tryDiscount(PrcCustomerPriceSet bind, PrcPriceSet set, + private TmsPriceQuoteResult tryDiscount(PrcCustomerPriceSet bind, PrcPriceSet set, PrcPriceSetVersion version, BmsProductCountry base, String productCode, String productName, BigDecimal weight) { - int detailCount = detailMapper.countByPriceSetId(set.getPriceSetId()); + Long psvId = version.getPsvId(); + int detailCount = detailMapper.countByPsvId(psvId); BigDecimal discount = set.getDefaultDiscount(); String roundMode = null; + Long psdId = null; if (detailCount > 0) { - PrcPriceSetDetail detail = detailMapper.selectDiscountMatch(set.getPriceSetId(), productCode); + PrcPriceSetDetail detail = detailMapper.selectDiscountMatch(set.getPriceSetId(), psvId, productCode); if (detail == null) { return null; } + psdId = detail.getPsdId(); if (detail.getDiscountValue() != null) { discount = detail.getDiscountValue(); @@ -419,11 +443,25 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService TmsPriceQuoteResult r = buildFromRates(SRC_DISCOUNT, rate, baseRate, discount, base.getRegistrationFee(), base, productCode, productName, weight, roundMode); fillPackage(r, bind, set); - r.setMatchMessage("hit customer DISCOUNT package; freight = default * discount%"); + stampVersion(r, version, psdId); + r.setMatchMessage("hit customer DISCOUNT package version " + + (version.getVersionNo() == null ? String.valueOf(psvId) : version.getVersionNo()) + + "; freight = default * discount%"); r.setCustomerId(bind.getCustomerId()); return r; } + private void stampVersion(TmsPriceQuoteResult result, PrcPriceSetVersion version, Long psdId) + { + if (result == null || version == null) + { + return; + } + result.setPsvId(version.getPsvId()); + result.setVersionNo(version.getVersionNo()); + result.setPsdId(psdId); + } + private TmsPriceQuoteResult buildDefault(Long customerId, BmsProductCountry base, String productCode, String productName, BigDecimal weight, String message) { @@ -538,6 +576,23 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService { return matched; } + if (result.getPsdId() != null) + { + List snaps = + versionSurchargeMapper.selectByPsdIds(Collections.singletonList(result.getPsdId())); + if (snaps != null && !snaps.isEmpty()) + { + for (PrcPriceSetVersionSurcharge snap : snaps) + { + PrcSurchargeRel rel = toRelFromVersionSnap(snap); + if (surchargeApplies(rel, result)) + { + matched.add(rel); + } + } + return matched; + } + } PrcSurchargeRel query = new PrcSurchargeRel(); query.setProductCode(result.getProductCode()); List rels = surchargeRelMapper.selectPrcSurchargeRelList(query); @@ -555,6 +610,27 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return matched; } + private PrcSurchargeRel toRelFromVersionSnap(PrcPriceSetVersionSurcharge snap) + { + PrcSurchargeRel rel = new PrcSurchargeRel(); + if (snap == null) + { + return rel; + } + rel.setScId(snap.getScId()); + rel.setProductCode(snap.getProductCode()); + rel.setScName(snap.getScName()); + rel.setFeeType(snap.getFeeType()); + rel.setChargeType(snap.getChargeType()); + rel.setChargeValue(snap.getChargeValue()); + rel.setCurrency(snap.getCurrency()); + rel.setRoundMode(snap.getRoundMode()); + rel.setCountryCode(snap.getCountryCode()); + rel.setScNote(snap.getScNote()); + rel.setSsCode("on"); + return rel; + } + private boolean surchargeApplies(PrcSurchargeRel rel, TmsPriceQuoteResult result) { if (rel == null) @@ -961,6 +1037,24 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } } + /** Use end of bill day so same-day version activate still matches that day's quote. */ + private static Date endOfDay(Date day) + { + if (day == null) + { + return new Date(); + } + try + { + String d = new SimpleDateFormat("yyyy-MM-dd").format(day); + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(d + " 23:59:59"); + } + catch (ParseException e) + { + return day; + } + } + private String resolveProductName(String productCode, Date bizDate) { BmsProduct p = productMapper.selectEffectiveBmsProductByCode(productCode, bizDate); diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcCustomerPriceSetMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcCustomerPriceSetMapper.xml index 31570ad7..f43939ff 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcCustomerPriceSetMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcCustomerPriceSetMapper.xml @@ -97,8 +97,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from prc_customer_price_set c inner join prc_price_set s on s.price_set_id = c.price_set_id where c.customer_id = #{customerId} - and s.status = 'E' - and c.pr_begindate <= #{bizDate} + and ifnull(s.status, 'D') <> 'S' + and (c.pr_begindate is null or c.pr_begindate <= #{bizDate}) and ifnull(c.pr_enddate, '9999-12-31') >= #{bizDate} order by c.bind_priority asc, c.pr_begindate desc, c.customer_priceset_id asc @@ -110,8 +110,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from prc_customer_price_set c inner join prc_price_set s on s.price_set_id = c.price_set_id where c.customer_code = #{customerCode} - and s.status = 'E' - and c.pr_begindate <= #{bizDate} + and ifnull(s.status, 'D') <> 'S' + and (c.pr_begindate is null or c.pr_begindate <= #{bizDate}) and ifnull(c.pr_enddate, '9999-12-31') >= #{bizDate} order by c.bind_priority asc, c.pr_begindate desc, c.customer_priceset_id asc diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml index 62dba9cc..d1dacf97 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml @@ -63,6 +63,24 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND zone_code = #{zoneCode} + + AND ( + product_code like concat('%', #{productKeyword}, '%') + or product_name like concat('%', #{productKeyword}, '%') + ) + + + AND ( + country_code like concat('%', #{countryKeyword}, '%') + or country_name like concat('%', #{countryKeyword}, '%') + ) + + + AND ( + zone_code like concat('%', #{zoneKeyword}, '%') + or zone_name like concat('%', #{zoneKeyword}, '%') + ) + AND ( product_code like concat('%', #{keyword}, '%') @@ -269,6 +287,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" where price_set_id = #{priceSetId} + + and psv_id = #{psvId} + and product_code = #{productCode} order by psd_id asc limit 1 diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcPriceSetVersionMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcPriceSetVersionMapper.xml index b713ea05..bfb5efe0 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcPriceSetVersionMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcPriceSetVersionMapper.xml @@ -100,4 +100,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select distinct price_set_id from prc_price_set_version + + diff --git a/bms-front/src/views/price/billing/components/BillDetail.vue b/bms-front/src/views/price/billing/components/BillDetail.vue index 37da27a5..ebf66f1a 100644 --- a/bms-front/src/views/price/billing/components/BillDetail.vue +++ b/bms-front/src/views/price/billing/components/BillDetail.vue @@ -65,6 +65,7 @@ {{ sourceLabel(detail.matchSource) }} {{ priceSetLabel(detail) }} + {{ detail.versionNo || (detail.psvId ? ('psv=' + detail.psvId) : '-') }} diff --git a/bms-front/src/views/price/billing/quote.vue b/bms-front/src/views/price/billing/quote.vue index d936097d..ac64af59 100644 --- a/bms-front/src/views/price/billing/quote.vue +++ b/bms-front/src/views/price/billing/quote.vue @@ -101,6 +101,7 @@ {{ result.priceSetCode || '-' }} {{ result.priceSetName || '-' }} {{ result.setType || '-' }} + {{ result.versionNo || (result.psvId ? ('psv=' + result.psvId) : '-') }} diff --git a/bms-front/src/views/price/set/components/create/PriceEditDrawer.vue b/bms-front/src/views/price/set/components/create/PriceEditDrawer.vue index 849f4374..7084c569 100644 --- a/bms-front/src/views/price/set/components/create/PriceEditDrawer.vue +++ b/bms-front/src/views/price/set/components/create/PriceEditDrawer.vue @@ -107,7 +107,7 @@ const editInputRefs = {} const title = computed(() => { const code = props.productCode || '' const name = props.productName || '' - return code + ' �� ' + name + ' �� 国家服务价格' + return code + ' · ' + name + ' · 国家服务价格' }) const hint = '黄色行=已改价;点编辑图标后修改,失焦即刻写入数据库。单位:运费 RMB/KG,挂号费 RMB/票。' const phSearch = '搜索国家/分区' diff --git a/bms-front/src/views/price/set/components/create/PriceEditPanel.vue b/bms-front/src/views/price/set/components/create/PriceEditPanel.vue new file mode 100644 index 00000000..7e06d453 --- /dev/null +++ b/bms-front/src/views/price/set/components/create/PriceEditPanel.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/bms-front/src/views/price/set/components/create/StepDetails.vue b/bms-front/src/views/price/set/components/create/StepDetails.vue index 3ed133ec..71b896fe 100644 --- a/bms-front/src/views/price/set/components/create/StepDetails.vue +++ b/bms-front/src/views/price/set/components/create/StepDetails.vue @@ -105,7 +105,8 @@ const props = defineProps({ setType: { type: String, default: 'FIXED' }, effectiveTime: { type: String, default: undefined }, versionNo: { type: String, default: '' }, - refData: { type: Object, required: true } + refData: { type: Object, required: true }, + headerTitle: { type: String, default: '第 2 步 · 编辑产品价格' } }) const { proxy } = getCurrentInstance() @@ -113,7 +114,6 @@ const { bms_tax_mode } = useDict('bms_tax_mode') const { hydrateRow } = useDetailCascade(props.refData) const loadingText = '正在全量拉取产品价格…' -const headerTitle = '第 2 步 · 编辑产品价格' const phKeyword = '产品代码/名称' const phTax = '全部税模式' const colCode = '产品代码' diff --git a/bms-front/src/views/price/set/components/detail/SetVersionDetailDrawer.vue b/bms-front/src/views/price/set/components/detail/SetVersionDetailDrawer.vue index f4bff076..cf6f8e03 100644 --- a/bms-front/src/views/price/set/components/detail/SetVersionDetailDrawer.vue +++ b/bms-front/src/views/price/set/components/detail/SetVersionDetailDrawer.vue @@ -9,10 +9,26 @@
+ + 查询 @@ -111,10 +127,14 @@ const total = ref(0) const query = reactive({ pageNum: 1, pageSize: 20, - keyword: '' + productKeyword: '', + countryKeyword: '', + zoneKeyword: '' }) -const phSearch = '产品/国家/分区/重量段' +const phProduct = '产品代码/名称' +const phCountry = '国家' +const phZone = '分区' const colProduct = '产品' const colCountry = '国家' const colZone = '分区' @@ -147,7 +167,9 @@ function search() { } function reset() { - query.keyword = '' + query.productKeyword = '' + query.countryKeyword = '' + query.zoneKeyword = '' query.pageNum = 1 load() } @@ -162,7 +184,9 @@ function load() { loading.value = true listSetDetail({ psvId, - keyword: query.keyword || undefined, + productKeyword: query.productKeyword || undefined, + countryKeyword: isFixed.value ? (query.countryKeyword || undefined) : undefined, + zoneKeyword: isFixed.value ? (query.zoneKeyword || undefined) : undefined, pageNum: query.pageNum, pageSize: query.pageSize }).then(res => { @@ -178,7 +202,9 @@ watch( ([open]) => { if (open) { query.pageNum = 1 - query.keyword = '' + query.productKeyword = '' + query.countryKeyword = '' + query.zoneKeyword = '' load() } } @@ -197,6 +223,7 @@ watch( gap: 8px; margin-bottom: 12px; flex-wrap: wrap; + align-items: center; } .pager { margin-top: 12px; diff --git a/bms-front/src/views/price/set/create.vue b/bms-front/src/views/price/set/create.vue index cee4259c..3422b478 100644 --- a/bms-front/src/views/price/set/create.vue +++ b/bms-front/src/views/price/set/create.vue @@ -24,7 +24,7 @@ 上一步 创建套餐价格 保存草稿 - 保存 + 完成
d && d.productCode) + // Price edits already persist via PUT /detail; product remove uses DELETE. + // Omit details when all rows already have psdId so save only updates header/version. + const hasUnsavedDetail = detailRows.some(d => d.psdId == null) + const payload = { priceSetId: form.priceSetId, priceSetName: basic.priceSetName, setType: basic.setType, @@ -199,11 +223,17 @@ function buildPayload() { version: { psvId: copyMode.value ? undefined : version.psvId, versionNo: version.versionNo - // no effectiveTime: set on activate - }, - details: form.details.filter(d => d.productCode).map(toDetailPayload), - customers: form.customers.filter(c => c.customerCode).map(toCustomerPayload) + } } + if (hasUnsavedDetail) { + payload.details = detailRows.map(toDetailPayload) + } + // Customer bind is managed on detail page APIs; only send when present to avoid wipe. + const customers = form.customers.filter(c => c && c.customerCode).map(toCustomerPayload) + if (customers.length) { + payload.customers = customers + } + return payload } function goBack() { diff --git a/bms-front/src/views/price/set/detail.vue b/bms-front/src/views/price/set/detail.vue index 98231587..ff8c777a 100644 --- a/bms-front/src/views/price/set/detail.vue +++ b/bms-front/src/views/price/set/detail.vue @@ -1,31 +1,41 @@ @@ -35,6 +45,7 @@ import SetBasicInfoCard from './components/detail/SetBasicInfoCard.vue' import SetCustomerTable from './components/detail/SetCustomerTable.vue' import SetVersionTable from './components/detail/SetVersionTable.vue' import SetVersionDetailDrawer from './components/detail/SetVersionDetailDrawer.vue' +import PriceEditPanel from './components/create/PriceEditPanel.vue' import { usePriceSetRefData } from './composables/usePriceSetRefData' import { getPriceSet, disablePriceSet, copyPriceSet, activatePriceSetVersion, copyPriceSetVersion, deletePriceSetVersion @@ -49,6 +60,8 @@ const set = ref({}) const currentPsvId = ref() const detailDrawerVisible = ref(false) const detailVersion = ref(null) +const editVisible = ref(false) +const editPsvId = ref(undefined) function onOpenVersionDetails(row) { detailVersion.value = row || null @@ -102,10 +115,8 @@ function onCopyVersion(row) { const newPsvId = res.data load() if (newPsvId != null) { - proxy.$router.push({ - path: '/customer/setCreate', - query: { priceSetId: set.value.priceSetId, psvId: newPsvId, step: '2' } - }) + editPsvId.value = newPsvId + editVisible.value = true } }).catch(() => {}) } @@ -119,9 +130,14 @@ function onRemoveVersion(row) { }).catch(() => {}) } function onEditVersion(row) { - const q = { priceSetId: set.value.priceSetId, step: '2' } - if (row && row.psvId != null) q.psvId = row.psvId - proxy.$router.push({ path: '/customer/setCreate', query: q }) + editPsvId.value = row && row.psvId != null ? row.psvId : undefined + editVisible.value = true +} + +/** 关闭内嵌编辑面板,刷新版本列表(明细数、状态可能变化) */ +function closeEdit() { + editVisible.value = false + load() } function goList() { const obj = { path: '/customer/set' } diff --git a/bms-front/src/views/price/set/priceSet.vue b/bms-front/src/views/price/set/priceSet.vue index e4f19a3b..1467be92 100644 --- a/bms-front/src/views/price/set/priceSet.vue +++ b/bms-front/src/views/price/set/priceSet.vue @@ -33,6 +33,16 @@ {{ statusLabel(row) }} + +