diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceQuoteRequest.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceQuoteRequest.java index a8fc661c..c72c15b3 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceQuoteRequest.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/TmsPriceQuoteRequest.java @@ -21,6 +21,11 @@ public class TmsPriceQuoteRequest implements Serializable @NotBlank(message = "productCode is required") private String productCode; + /** + * 打单套餐(LABEL)试算/计费用渠道ID;计费也可从订单扩展 productChannelId 解析。 + */ + private String channelId; + /** ISO2; countryName alternative */ private String countryCode; /** country display name; countryCode alternative */ diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductTermMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductTermMapper.java index ff947b1c..1190f6e7 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductTermMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PayProductTermMapper.java @@ -22,6 +22,10 @@ public interface PayProductTermMapper List selectByPriceSetIdAndVersion(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo); + /** 打单计费:套餐+产品+计费日命中 term(时间窗口优先) */ + Long selectTermIdBySetProductAt(@Param("priceSetId") Long priceSetId, @Param("productId") Long productId, + @Param("bizDate") Date bizDate); + /** 打单套餐:按 version_no 聚合版本(含已失效) */ List selectLabelVersionGroups(@Param("priceSetId") Long priceSetId); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java index ef6a93aa..3f8fab55 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPayProductService.java @@ -90,6 +90,11 @@ public interface IPayProductService /** 按订单时间取这一产品当时有效的价格版本 */ PayProduct selectPriceForDate(String channelId, Date bizDate); + /** + * 打单套餐计费取价:渠道 AR 映射 → 稳定 product_id,再按套餐+计费日命中 term。 + */ + PayProduct selectLabelPriceForDate(Long priceSetId, String channelId, Date bizDate); + /** 对照两个版本的重量段和附加费 */ PayPriceCompare compare(Long leftId, Long rightId); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java index d871ca94..2b7a9f84 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PayProductServiceImpl.java @@ -1537,6 +1537,36 @@ public class PayProductServiceImpl implements IPayProductService return version; } + @Override + public PayProduct selectLabelPriceForDate(Long priceSetId, String channelId, Date bizDate) + { + if (priceSetId == null) + { + throw new ServiceException("打单套餐不能为空"); + } + if (isBlank(channelId)) + { + throw new ServiceException("打单计费需要渠道"); + } + Long root = channelProductRoot(channelId.trim(), PAY_TYPE_AR); + if (root == null) + { + throw new ServiceException("渠道还没有绑定打单应收产品"); + } + Date day = bizDate == null ? today() : bizDate; + Long termId = payProductTermMapper.selectTermIdBySetProductAt(priceSetId, root, day); + if (termId == null) + { + throw new ServiceException("订单时间没有落在打单价卡有效期内"); + } + PayProduct product = selectPayProductDetailByTerm(termId); + if (product == null) + { + throw new ServiceException("打单价卡版本不存在"); + } + return product; + } + @Override public PayPriceCompare compare(Long leftId, Long rightId) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java index 33e1bade..553fcae5 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -28,7 +28,11 @@ import com.bms.customer.customerinfo.mapper.BmsCustomerMapper; import com.bms.price.domain.BmsProduct; import com.bms.price.domain.BmsProductCountry; import com.bms.price.domain.BmsRedlineProductCountry; +import com.bms.price.domain.PayCostLine; +import com.bms.price.domain.PayOrderCostResult; +import com.bms.price.domain.PayProduct; import com.bms.price.domain.PrcCustomerPriceSet; +import com.bms.price.domain.PrcOrderBillExt; import com.bms.price.domain.PrcPriceSet; import com.bms.price.domain.PrcPriceSetDetail; import com.bms.price.domain.PrcPriceSetProduct; @@ -37,6 +41,7 @@ import com.bms.price.domain.PrcQuote; import com.bms.price.domain.PrcQuoteFee; import com.bms.price.domain.PrcSurchargeRel; import com.bms.price.domain.PrcZone; +import com.bms.price.pay.PayCostCalculator; import com.bms.price.mapper.PrcZoneDetailMapper; import com.bms.price.domain.vo.TmsPriceBillRequest; import com.bms.price.domain.vo.TmsPriceQuoteFeeLine; @@ -48,6 +53,7 @@ import com.bms.price.mapper.BmsProductCountryMapper; import com.bms.price.mapper.BmsProductMapper; import com.bms.price.mapper.BmsRedlineProductCountryMapper; import com.bms.price.mapper.PrcCustomerPriceSetMapper; +import com.bms.price.mapper.PrcOrderBillExtMapper; import com.bms.price.mapper.PrcPriceSetDetailMapper; import com.bms.price.mapper.PrcPriceSetMapper; import com.bms.price.mapper.PrcPriceSetProductMapper; @@ -58,12 +64,14 @@ import com.bms.price.mapper.PrcSurchargeRelMapper; import com.bms.price.search.EsBizLogSupport; import com.bms.price.search.EsLogType; import com.bms.price.search.IBillSearchStore; +import com.bms.price.service.IPayProductService; import com.bms.price.service.ITmsPriceQuoteService; import com.bms.system.service.IBmsProductCodeMappingService; /** * 共用的试算/计费计算:按 bizDate 取客户绑定和套餐版本(版本 effective_time)。 * 绑定的 pr_begindate/pr_enddate 不作为计费时间门槛。 + * 先按 prc_price_set.set_category 分流:LABEL 走打单 pay_*;PACKAGE/EXCLUSIVE 走销售快照。 * 销售价只来自套餐快照(FIXED 明细 / 按目录底价 DISCOUNT)。 * 无绑定或无生效版本:试算和计费都直接失败(不用 PRODUCT_DEFAULT)。 * 账单同步时,产品/国家/重量段未命中为软失败(billing_fail_status 1/2/3,金额为 0)。 @@ -78,6 +86,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService private static final String TYPE_DISCOUNT = "DISCOUNT"; private static final String SRC_FIXED = "PACKAGE_FIXED"; private static final String SRC_DISCOUNT = "PACKAGE_DISCOUNT"; + private static final String SRC_LABEL = "LABEL_PAY"; private static final String FEE_BASIC = "BASIC_FREIGHT"; private static final String FEE_REG = "REGISTRATION_FEE"; @@ -128,6 +137,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService @Autowired private BmsCustomerMapper bmsCustomerMapper; + @Autowired + private IPayProductService payProductService; + + @Autowired + private PrcOrderBillExtMapper orderBillExtMapper; + @Override @Transactional(rollbackFor = Exception.class) public TmsPriceQuoteResult quote(TmsPriceQuoteRequest request) @@ -169,7 +184,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return toResult(cached, fees, true); } TmsPriceQuoteResult refreshed = doQuote(request, bizDate); - applySurchargesAndFees(refreshed); + applySalesSurchargesIfNeeded(refreshed); replaceQuoteFees(cached.getQuoteId(), refreshed); refreshed.setQuoteId(cached.getQuoteId()); refreshed.setFromCache(false); @@ -179,11 +194,21 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } TmsPriceQuoteResult result = doQuote(request, bizDate); - applySurchargesAndFees(result); + applySalesSurchargesIfNeeded(result); persistQuote(quoteKey, request, weight, bizDate, result); return result; } + /** LABEL 费用已由 PayCostCalculator 算完,不再叠加销售附加费。 */ + private void applySalesSurchargesIfNeeded(TmsPriceQuoteResult result) + { + if (result == null || SRC_LABEL.equals(result.getMatchSource())) + { + return; + } + applySurchargesAndFees(result); + } + /** * Cached quotes from pre-package-snapshot logic (PRODUCT_DEFAULT / missing package id) * must not be reused after billing rules changed. @@ -211,7 +236,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService TmsPriceQuoteResult result = doQuote(request, bizDate); if (!BillBillingFailStatus.isFail(result.getBillingFailStatus())) { - applySurchargesAndFees(result); + applySalesSurchargesIfNeeded(result); result.setBillingFailStatus(BillBillingFailStatus.SUCCESS.getCode()); } result.setCustomerActiveStatus(customerActiveStatus); @@ -442,6 +467,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService continue; } anyEnabledSet = true; + // 打单套餐:独立 pay_* 链路,不走销售版本明细 + if (PrcPriceSet.CATEGORY_LABEL.equals( + PrcPriceSetServiceImpl.normalizeSetCategory(set.getSetCategory()))) + { + return doLabelQuote(request, bind, set, productCode, productName, countryCode, countryName, + weight, bizDate); + } // Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate) PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf); if (version == null || version.getPsvId() == null) @@ -622,6 +654,171 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService throw new ServiceException(MessageUtils.message(missKey)); } + /** + * 打单套餐计费:渠道 AR → product_id,套餐+时间 → term,PayCostCalculator 算费。 + */ + private TmsPriceQuoteResult doLabelQuote(TmsPriceQuoteRequest request, PrcCustomerPriceSet bind, + PrcPriceSet set, String productCode, String productName, String countryCode, String countryName, + BigDecimal weight, Date bizDate) + { + String channelId = resolveLabelChannelId(request); + if (StringUtils.isEmpty(channelId)) + { + if (request instanceof TmsPriceBillRequest) + { + return buildBillingMissResult(request, productCode, productName, countryCode, countryName, + weight, bizDate, BillBillingFailStatus.PRICE_NOT_HIT.getCode()); + } + throw new ServiceException("打单计费需要渠道"); + } + PayProduct product; + try + { + product = payProductService.selectLabelPriceForDate(set.getPriceSetId(), channelId, bizDate); + } + catch (ServiceException ex) + { + if (request instanceof TmsPriceBillRequest) + { + return buildBillingMissResult(request, productCode, productName, countryCode, countryName, + weight, bizDate, BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode()); + } + throw ex; + } + BigDecimal length = null; + BigDecimal width = null; + BigDecimal height = null; + Integer orderStatus = null; + if (request instanceof TmsPriceBillRequest) + { + TmsPriceBillRequest billReq = (TmsPriceBillRequest) request; + length = billReq.getPkgLength(); + width = billReq.getPkgWidth(); + height = billReq.getPkgHeight(); + orderStatus = billReq.getOrderStatus(); + } + PayOrderCostResult cost = PayCostCalculator.calculate(product, weight, length, width, height, + request.getDestPostcode(), bizDate, orderStatus); + if (cost == null || cost.getFees() == null || cost.getFees().isEmpty()) + { + if (request instanceof TmsPriceBillRequest) + { + return buildBillingMissResult(request, productCode, productName, countryCode, countryName, + weight, bizDate, BillBillingFailStatus.PRICE_NOT_HIT.getCode()); + } + throw new ServiceException(cost != null && StringUtils.isNotEmpty(cost.getMessage()) + ? cost.getMessage() : "打单价卡未算出费用"); + } + TmsPriceQuoteResult r = mapLabelCostToQuote(cost, product, request, bind, set, + productCode, productName, countryCode, countryName, weight, bizDate); + stampRequestMeta(request, r); + return r; + } + + /** 试算用 request.channelId;计费优先订单扩展 productChannelId。 */ + private String resolveLabelChannelId(TmsPriceQuoteRequest request) + { + if (request != null && StringUtils.isNotEmpty(request.getChannelId())) + { + return request.getChannelId().trim(); + } + if (!(request instanceof TmsPriceBillRequest)) + { + return null; + } + TmsPriceBillRequest billReq = (TmsPriceBillRequest) request; + PrcOrderBillExt ext = null; + if (StringUtils.isNotEmpty(billReq.getOrderNo())) + { + ext = orderBillExtMapper.selectByOrderNo(billReq.getOrderNo().trim()); + } + if (ext == null) + { + return null; + } + if (StringUtils.isNotEmpty(ext.getProductChannelId())) + { + return ext.getProductChannelId().trim(); + } + if (StringUtils.isNotEmpty(ext.getProductFloorCode())) + { + return ext.getProductFloorCode().trim(); + } + return null; + } + + private TmsPriceQuoteResult mapLabelCostToQuote(PayOrderCostResult cost, PayProduct product, + TmsPriceQuoteRequest request, PrcCustomerPriceSet bind, PrcPriceSet set, + String productCode, String productName, String countryCode, String countryName, + BigDecimal weight, Date bizDate) + { + RoundingMode rm = RoundingMode.HALF_UP; + String currency = normalizeCurrency(cost.getCurrency()); + List lines = new ArrayList(); + BigDecimal freightAmount = BigDecimal.ZERO; + BigDecimal surchargeAmount = BigDecimal.ZERO; + BigDecimal registrationFee = BigDecimal.ZERO; + for (PayCostLine fee : cost.getFees()) + { + if (fee == null || fee.getAmount() == null) + { + continue; + } + String feeType = StringUtils.isEmpty(fee.getFeeType()) ? FEE_SUR : fee.getFeeType().trim(); + String feeName = StringUtils.isEmpty(fee.getFeeName()) ? feeNameOf(feeType) : fee.getFeeName(); + BigDecimal amount = fee.getAmount().setScale(2, rm); + Long scId = fee.getScId() == null ? 0L : fee.getScId(); + lines.add(feeLine(feeType, feeName, scId, amount, currency, "HALF_UP", fee.getRemark())); + if (FEE_BASIC.equals(feeType)) + { + freightAmount = freightAmount.add(amount); + } + else if (FEE_REG.equals(feeType)) + { + registrationFee = registrationFee.add(amount); + } + else + { + surchargeAmount = surchargeAmount.add(amount); + } + } + BigDecimal incomeTotal = nvl(cost.getPayTotal()); + if (incomeTotal.compareTo(BigDecimal.ZERO) <= 0) + { + incomeTotal = freightAmount.add(registrationFee).add(surchargeAmount).setScale(2, rm); + } + String labelProductName = StringUtils.isNotEmpty(product.getProductName()) + ? product.getProductName() : productName; + TmsPriceQuoteResult r = new TmsPriceQuoteResult(); + r.setMatchSource(SRC_LABEL); + r.setMatchMessage("hit label package pay card V" + + (product.getVersionNo() == null ? "?" : product.getVersionNo())); + fillPackage(r, bind, set); + r.setVersionNo(product.getVersionNo() == null ? null : ("V" + product.getVersionNo())); + r.setCustomerId(bind.getCustomerId()); + r.setProductCode(productCode); + r.setProductName(labelProductName); + r.setCountryCode(countryCode); + r.setCountryName(countryName); + r.setWeight(weight); + r.setChargeWeight(weight); + r.setBizDate(bizDate); + r.setEffectiveDate(product.getTermStart() != null ? product.getTermStart() : product.getEffectiveDate()); + r.setFreightRate(weight != null && weight.compareTo(BigDecimal.ZERO) > 0 + ? freightAmount.divide(weight, 2, rm) : freightAmount); + r.setFreightAmount(freightAmount.setScale(2, rm)); + r.setRegistrationFee(registrationFee.setScale(2, rm)); + r.setSurchargeAmount(surchargeAmount.setScale(2, rm)); + r.setPayableTotal(incomeTotal); + r.setIncomeTotal(incomeTotal); + r.setCurrency(currency); + r.setRoundMode("HALF_UP"); + r.setFromCache(false); + r.setFeeDetails(lines); + r.setPayFeeDetails(Collections.emptyList()); + return r; + } + /** * Diagnose FIXED miss: product rows absent -> 1; country/zone absent -> 2; else weight -> 3. */ diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PayProductTermMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PayProductTermMapper.xml index f882a41e..b955847b 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PayProductTermMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PayProductTermMapper.xml @@ -70,6 +70,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by case when t.status = '1' then 0 when t.status = '0' then 1 else 2 end, p.product_name, t.term_id desc + +