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 new file mode 100644 index 00000000..3bb2ddfa --- /dev/null +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/TmsPriceController.java @@ -0,0 +1,76 @@ +package com.ruoyi.web.controller.api; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.config.properties.TmsApiProperties; +import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest; +import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; +import com.ruoyi.price.service.ITmsPriceQuoteService; + +/** + * TMS open API: freight quote. + * + * POST /api/tms/price/quote + * Header X-Api-Code must equal bms.tms.api-code + * + * @author ruoyi + */ +@Anonymous +@RestController +@RequestMapping("/api/tms/price") +public class TmsPriceController extends BaseController +{ + public static final String HEADER_API_CODE = "X-Api-Code"; + + @Autowired + private ITmsPriceQuoteService tmsPriceQuoteService; + + @Autowired + private TmsApiProperties tmsApiProperties; + + /** + * Quote by customerId + productCode + country + weight. + * Match: customer package FIXED > DISCOUNT > product default. + */ + @PostMapping("/quote") + public AjaxResult quote( + @RequestHeader(value = HEADER_API_CODE, required = false) String headerApiCode, + @Validated @RequestBody TmsPriceQuoteRequest request) + { + assertApiCode(headerApiCode, request == null ? null : request.getApiCode()); + TmsPriceQuoteResult result = tmsPriceQuoteService.quote(request); + return success(result); + } + + private void assertApiCode(String headerApiCode, String bodyApiCode) + { + String expected = tmsApiProperties.getApiCode(); + if (StringUtils.isEmpty(expected)) + { + throw new ServiceException("API_CODE_NOT_CONFIGURED: bms.tms.api-code is empty"); + } + String actual = StringUtils.isNotEmpty(headerApiCode) ? headerApiCode.trim() : null; + if (StringUtils.isEmpty(actual) && StringUtils.isNotEmpty(bodyApiCode)) + { + actual = bodyApiCode.trim(); + } + if (StringUtils.isEmpty(actual)) + { + throw new ServiceException("API_CODE_REQUIRED: missing header X-Api-Code (or body apiCode)"); + } + if (!expected.equals(actual)) + { + throw new ServiceException("API_CODE_INVALID: api identification code mismatch"); + } + } +} diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java new file mode 100644 index 00000000..f62c99ef --- /dev/null +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/BillingController.java @@ -0,0 +1,38 @@ +package com.ruoyi.web.controller.price; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest; +import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; +import com.ruoyi.price.service.ITmsPriceQuoteService; + +/** + * Billing center: order quote trial (authenticated). + * + * @author ruoyi + */ +@RestController +@RequestMapping("/price/billing") +public class BillingController extends BaseController +{ + @Autowired + private ITmsPriceQuoteService tmsPriceQuoteService; + + /** + * Same matching logic as TMS open API, for BMS console trial. + */ + @PreAuthorize("@ss.hasPermi('price:billing:quote')") + @PostMapping("/quote") + public AjaxResult quote(@Validated @RequestBody TmsPriceQuoteRequest request) + { + TmsPriceQuoteResult result = tmsPriceQuoteService.quote(request); + return success(result); + } +} diff --git a/bms-back/ruoyi-admin/src/main/resources/application.yml b/bms-back/ruoyi-admin/src/main/resources/application.yml index b5f12864..67fceba1 100644 --- a/bms-back/ruoyi-admin/src/main/resources/application.yml +++ b/bms-back/ruoyi-admin/src/main/resources/application.yml @@ -145,3 +145,9 @@ xss: excludes: /system/notice # 匹配链接 urlPatterns: /system/*,/monitor/*,/tool/* + +# BMS TMS open API +bms: + tms: + # Agreed API identification code for TMS callers + api-code: BMS.TMS._Q2WnPXiXwGW2Dl-iSGNzLfrhUgQogNo diff --git a/bms-back/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/TmsApiProperties.java b/bms-back/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/TmsApiProperties.java new file mode 100644 index 00000000..b2569feb --- /dev/null +++ b/bms-back/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/TmsApiProperties.java @@ -0,0 +1,29 @@ +package com.ruoyi.framework.config.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * TMS open API properties. + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "bms.tms") +public class TmsApiProperties +{ + /** + * Agreed API identification code (header X-Api-Code or body apiCode). + */ + private String apiCode = "BMS.TMS._Q2WnPXiXwGW2Dl-iSGNzLfrhUgQogNo"; + + public String getApiCode() + { + return apiCode; + } + + public void setApiCode(String apiCode) + { + this.apiCode = apiCode; + } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java new file mode 100644 index 00000000..90a009ee --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteRequest.java @@ -0,0 +1,60 @@ +package com.ruoyi.price.domain.vo; + +import java.io.Serializable; +import java.math.BigDecimal; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * TMS freight quote request. + * + * @author ruoyi + */ +public class TmsPriceQuoteRequest implements Serializable +{ + private static final long serialVersionUID = 1L; + + @NotNull(message = "customerId is required") + private Long customerId; + + @NotBlank(message = "productCode is required") + private String productCode; + + /** ISO2 country code; provide countryCode or countryName */ + private String countryCode; + + /** Country CN name; provide countryCode or countryName */ + private String countryName; + + @NotNull(message = "weight is required") + private BigDecimal weight; + + /** Optional business date yyyy-MM-dd; default today */ + private String bizDate; + + /** Optional TMS / tenant id */ + private Integer tmsId; + + /** + * Agreed API identification code (same as header X-Api-Code). + * Prefer header; body is fallback. + */ + private String apiCode; + + public Long getCustomerId() { return customerId; } + public void setCustomerId(Long customerId) { this.customerId = customerId; } + public String getProductCode() { return productCode; } + public void setProductCode(String productCode) { this.productCode = productCode; } + public String getCountryCode() { return countryCode; } + public void setCountryCode(String countryCode) { this.countryCode = countryCode; } + public String getCountryName() { return countryName; } + public void setCountryName(String countryName) { this.countryName = countryName; } + public BigDecimal getWeight() { return weight; } + public void setWeight(BigDecimal weight) { this.weight = weight; } + public String getBizDate() { return bizDate; } + public void setBizDate(String bizDate) { this.bizDate = bizDate; } + public Integer getTmsId() { return tmsId; } + public void setTmsId(Integer tmsId) { this.tmsId = tmsId; } + public String getApiCode() { return apiCode; } + public void setApiCode(String apiCode) { this.apiCode = apiCode; } +} 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 new file mode 100644 index 00000000..3617cff4 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java @@ -0,0 +1,105 @@ +package com.ruoyi.price.domain.vo; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * TMS freight quote response. + * + * matchSource: PACKAGE_FIXED / PACKAGE_DISCOUNT / PRODUCT_DEFAULT + * + * @author ruoyi + */ +public class TmsPriceQuoteResult implements Serializable +{ + private static final long serialVersionUID = 1L; + + private String matchSource; + private String matchMessage; + + private Long customerId; + private Long priceSetId; + private String priceSetCode; + private String priceSetName; + private String setType; + private Integer bindPriority; + private Long customerPricesetId; + + private String productCode; + private String productName; + private String countryCode; + private String countryName; + private Long productCountryId; + private BigDecimal weightMin; + private BigDecimal weightMax; + private String weightLabel; + + private BigDecimal weight; + private BigDecimal chargeWeight; + /** Freight unit price RMB/KG (after discount if any) */ + private BigDecimal freightRate; + /** Base freight before discount (DISCOUNT only) */ + private BigDecimal baseFreightRate; + /** Discount percent e.g. 88 */ + private BigDecimal discountPercent; + /** Registration fee RMB/pcs */ + private BigDecimal registrationFee; + /** freightRate * chargeWeight */ + private BigDecimal freightAmount; + /** freightAmount + registrationFee */ + private BigDecimal payableTotal; + private String currency = "RMB"; + + public String getMatchSource() { return matchSource; } + public void setMatchSource(String matchSource) { this.matchSource = matchSource; } + public String getMatchMessage() { return matchMessage; } + public void setMatchMessage(String matchMessage) { this.matchMessage = matchMessage; } + public Long getCustomerId() { return customerId; } + public void setCustomerId(Long customerId) { this.customerId = customerId; } + public Long getPriceSetId() { return priceSetId; } + public void setPriceSetId(Long priceSetId) { this.priceSetId = priceSetId; } + public String getPriceSetCode() { return priceSetCode; } + public void setPriceSetCode(String priceSetCode) { this.priceSetCode = priceSetCode; } + public String getPriceSetName() { return priceSetName; } + public void setPriceSetName(String priceSetName) { this.priceSetName = priceSetName; } + public String getSetType() { return setType; } + public void setSetType(String setType) { this.setType = setType; } + public Integer getBindPriority() { return bindPriority; } + public void setBindPriority(Integer bindPriority) { this.bindPriority = bindPriority; } + public Long getCustomerPricesetId() { return customerPricesetId; } + public void setCustomerPricesetId(Long customerPricesetId) { this.customerPricesetId = customerPricesetId; } + public String getProductCode() { return productCode; } + public void setProductCode(String productCode) { this.productCode = productCode; } + public String getProductName() { return productName; } + public void setProductName(String productName) { this.productName = productName; } + public String getCountryCode() { return countryCode; } + public void setCountryCode(String countryCode) { this.countryCode = countryCode; } + public String getCountryName() { return countryName; } + public void setCountryName(String countryName) { this.countryName = countryName; } + public Long getProductCountryId() { return productCountryId; } + public void setProductCountryId(Long productCountryId) { this.productCountryId = productCountryId; } + public BigDecimal getWeightMin() { return weightMin; } + public void setWeightMin(BigDecimal weightMin) { this.weightMin = weightMin; } + public BigDecimal getWeightMax() { return weightMax; } + public void setWeightMax(BigDecimal weightMax) { this.weightMax = weightMax; } + public String getWeightLabel() { return weightLabel; } + public void setWeightLabel(String weightLabel) { this.weightLabel = weightLabel; } + public BigDecimal getWeight() { return weight; } + public void setWeight(BigDecimal weight) { this.weight = weight; } + public BigDecimal getChargeWeight() { return chargeWeight; } + public void setChargeWeight(BigDecimal chargeWeight) { this.chargeWeight = chargeWeight; } + public BigDecimal getFreightRate() { return freightRate; } + public void setFreightRate(BigDecimal freightRate) { this.freightRate = freightRate; } + public BigDecimal getBaseFreightRate() { return baseFreightRate; } + public void setBaseFreightRate(BigDecimal baseFreightRate) { this.baseFreightRate = baseFreightRate; } + public BigDecimal getDiscountPercent() { return discountPercent; } + public void setDiscountPercent(BigDecimal discountPercent) { this.discountPercent = discountPercent; } + public BigDecimal getRegistrationFee() { return registrationFee; } + public void setRegistrationFee(BigDecimal registrationFee) { this.registrationFee = registrationFee; } + public BigDecimal getFreightAmount() { return freightAmount; } + public void setFreightAmount(BigDecimal freightAmount) { this.freightAmount = freightAmount; } + public BigDecimal getPayableTotal() { return payableTotal; } + public void setPayableTotal(BigDecimal payableTotal) { this.payableTotal = payableTotal; } + public String getCurrency() { return currency; } + public void setCurrency(String currency) { this.currency = currency; } +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java index 54b26331..864d737d 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java @@ -1,6 +1,8 @@ package com.ruoyi.price.mapper; +import java.math.BigDecimal; import java.util.List; +import org.apache.ibatis.annotations.Param; import com.ruoyi.price.domain.BmsProductCountry; /** @@ -17,4 +19,13 @@ public interface BmsProductCountryMapper public int deleteByProductCode(String productCode); public int deleteByProductCodes(String[] productCodes); + + /** + * 按产品 + 国家 + 重量落入重量段匹配一条默认价 + */ + public BmsProductCountry selectMatchByProductCountryWeight( + @Param("productCode") String productCode, + @Param("countryCode") String countryCode, + @Param("countryName") String countryName, + @Param("weight") BigDecimal weight); } 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 8f8f60ba..9afe926d 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 @@ -29,4 +29,11 @@ public interface PrcCustomerPriceSetMapper @Param("priceSetId") Long priceSetId, @Param("beginDate") java.util.Date beginDate, @Param("endDate") java.util.Date endDate); + + /** + * 客户在业务日有效、且套餐状态为生效(E) 的绑定,按优先级升序 + */ + public java.util.List selectEffectiveByCustomer( + @Param("customerId") Long customerId, + @Param("bizDate") java.util.Date bizDate); } 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 1931cf8a..180dfaea 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 @@ -1,6 +1,7 @@ package com.ruoyi.price.mapper; import java.util.List; +import org.apache.ibatis.annotations.Param; import com.ruoyi.price.domain.PrcPriceSetDetail; /** @@ -25,4 +26,18 @@ public interface PrcPriceSetDetailMapper public int countProductByPriceSetId(Long priceSetId); public int countByPriceSetId(Long priceSetId); + + /** FIXED:按产品+国家+关联产品国家明细/重量段命中 */ + public PrcPriceSetDetail selectFixedMatch( + @Param("priceSetId") Long priceSetId, + @Param("productCode") String productCode, + @Param("countryCode") String countryCode, + @Param("countryName") String countryName, + @Param("productCountryId") Long productCountryId, + @Param("weightLabel") String weightLabel); + + /** DISCOUNT:按产品命中明细折扣(可空) */ + public PrcPriceSetDetail selectDiscountMatch( + @Param("priceSetId") Long priceSetId, + @Param("productCode") String productCode); } 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 new file mode 100644 index 00000000..0669d5ac --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java @@ -0,0 +1,17 @@ +package com.ruoyi.price.service; + +import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest; +import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; + +/** + * TMS freight quote service. + * + * Match order: effective customer package (bind_priority ASC) FIXED override + * then DISCOUNT, else bms_product_country default freight/registration. + * + * @author ruoyi + */ +public interface ITmsPriceQuoteService +{ + TmsPriceQuoteResult quote(TmsPriceQuoteRequest request); +} 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 new file mode 100644 index 00000000..9f20a374 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -0,0 +1,324 @@ +package com.ruoyi.price.service.impl; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.price.domain.BmsProduct; +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.vo.TmsPriceQuoteRequest; +import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; +import com.ruoyi.price.mapper.BmsProductCountryMapper; +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.service.ITmsPriceQuoteService; + +/** + * TMS freight quote: customer package first, then product default. + * + * @author ruoyi + */ +@Service +public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService +{ + private static final String TYPE_FIXED = "FIXED"; + 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_DEFAULT = "PRODUCT_DEFAULT"; + + @Autowired + private PrcCustomerPriceSetMapper customerPriceSetMapper; + + @Autowired + private PrcPriceSetMapper priceSetMapper; + + @Autowired + private PrcPriceSetDetailMapper detailMapper; + + @Autowired + private BmsProductCountryMapper productCountryMapper; + + @Autowired + private BmsProductMapper productMapper; + + @Override + public TmsPriceQuoteResult quote(TmsPriceQuoteRequest request) + { + validateRequest(request); + + String productCode = request.getProductCode().trim(); + String countryCode = emptyToNull(request.getCountryCode()); + String countryName = emptyToNull(request.getCountryName()); + if (countryCode != null) + { + countryCode = countryCode.toUpperCase(); + } + BigDecimal weight = request.getWeight(); + Date bizDate = resolveBizDate(request.getBizDate()); + + BmsProductCountry base = productCountryMapper.selectMatchByProductCountryWeight( + productCode, countryCode, countryName, weight); + if (base == null) + { + throw new ServiceException("PRICE_NOT_HIT: no bms_product_country band for product/country/weight"); + } + + String productName = resolveProductName(productCode); + List binds = customerPriceSetMapper.selectEffectiveByCustomer( + request.getCustomerId(), bizDate); + + if (binds != null) + { + for (PrcCustomerPriceSet bind : binds) + { + PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId()); + if (set == null || !"E".equals(set.getStatus())) + { + 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); + if (hit != null) + { + return hit; + } + } + else if (TYPE_DISCOUNT.equals(setType)) + { + TmsPriceQuoteResult hit = tryDiscount(bind, set, base, productCode, productName, weight); + if (hit != null) + { + return hit; + } + } + } + } + + return buildDefault(request.getCustomerId(), base, productCode, productName, weight, + "no customer package override; use product default freight/registration"); + } + + private TmsPriceQuoteResult tryFixed(PrcCustomerPriceSet bind, PrcPriceSet set, + BmsProductCountry base, String productCode, String productName, BigDecimal weight) + { + int detailCount = detailMapper.countByPriceSetId(set.getPriceSetId()); + if (detailCount == 0) + { + TmsPriceQuoteResult r = buildFromRates(SRC_DEFAULT, base.getFreightPrice(), null, null, + base.getRegistrationFee(), base, productCode, productName, weight); + fillPackage(r, bind, set); + r.setMatchMessage("FIXED package has no details; use bms_product_country default"); + r.setCustomerId(bind.getCustomerId()); + return r; + } + + PrcPriceSetDetail detail = detailMapper.selectFixedMatch( + set.getPriceSetId(), productCode, + base.getCountryCode(), base.getCountryName(), base.getDetailId(), + formatWeightLabel(base.getWeightMin(), base.getWeightMax())); + if (detail == null) + { + return null; + } + if (detail.getFvValue() == null || detail.getFvRegistrationFee() == null) + { + return null; + } + TmsPriceQuoteResult r = buildFromRates(SRC_FIXED, detail.getFvValue(), null, null, + detail.getFvRegistrationFee(), base, productCode, productName, weight); + fillPackage(r, bind, set); + if (StringUtils.isNotEmpty(detail.getProductName())) + { + r.setProductName(detail.getProductName()); + } + if (StringUtils.isNotEmpty(detail.getWeightLabel())) + { + r.setWeightLabel(detail.getWeightLabel()); + } + if (detail.getProductCountryId() != null) + { + r.setProductCountryId(detail.getProductCountryId()); + } + r.setMatchMessage("hit customer FIXED package detail"); + r.setCustomerId(bind.getCustomerId()); + return r; + } + + private TmsPriceQuoteResult tryDiscount(PrcCustomerPriceSet bind, PrcPriceSet set, + BmsProductCountry base, String productCode, String productName, BigDecimal weight) + { + int detailCount = detailMapper.countByPriceSetId(set.getPriceSetId()); + BigDecimal discount = set.getDefaultDiscount(); + if (detailCount > 0) + { + PrcPriceSetDetail detail = detailMapper.selectDiscountMatch(set.getPriceSetId(), productCode); + if (detail == null) + { + return null; + } + if (detail.getDiscountValue() != null) + { + discount = detail.getDiscountValue(); + } + } + if (discount == null + || discount.compareTo(BigDecimal.ONE) < 0 + || discount.compareTo(new BigDecimal("99")) > 0) + { + return null; + } + if (base.getFreightPrice() == null) + { + throw new ServiceException("FREIGHT_VALUE_MISSING: product default freight rate is null"); + } + BigDecimal baseRate = base.getFreightPrice(); + BigDecimal rate = baseRate.multiply(discount) + .divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP); + TmsPriceQuoteResult r = buildFromRates(SRC_DISCOUNT, rate, baseRate, discount, + base.getRegistrationFee(), base, productCode, productName, weight); + fillPackage(r, bind, set); + r.setMatchMessage("hit customer DISCOUNT package; freight = default * discount%"); + r.setCustomerId(bind.getCustomerId()); + return r; + } + + private TmsPriceQuoteResult buildDefault(Long customerId, BmsProductCountry base, + String productCode, String productName, BigDecimal weight, String message) + { + if (base.getFreightPrice() == null || base.getRegistrationFee() == null) + { + throw new ServiceException("FREIGHT_VALUE_MISSING: product default freight or registration is null"); + } + TmsPriceQuoteResult r = buildFromRates(SRC_DEFAULT, base.getFreightPrice(), null, null, + base.getRegistrationFee(), base, productCode, productName, weight); + r.setCustomerId(customerId); + r.setMatchMessage(message); + return r; + } + + private TmsPriceQuoteResult buildFromRates(String source, BigDecimal freightRate, + BigDecimal baseFreightRate, BigDecimal discountPercent, BigDecimal registrationFee, + BmsProductCountry base, String productCode, String productName, BigDecimal weight) + { + if (freightRate == null) + { + throw new ServiceException("FREIGHT_VALUE_MISSING: freight rate is null"); + } + if (registrationFee == null) + { + registrationFee = BigDecimal.ZERO; + } + BigDecimal freightAmount = freightRate.multiply(weight).setScale(2, RoundingMode.HALF_UP); + BigDecimal payable = freightAmount.add(registrationFee).setScale(2, RoundingMode.HALF_UP); + + TmsPriceQuoteResult r = new TmsPriceQuoteResult(); + r.setMatchSource(source); + r.setProductCode(productCode); + r.setProductName(productName); + r.setCountryCode(base.getCountryCode()); + r.setCountryName(base.getCountryName()); + r.setProductCountryId(base.getDetailId()); + r.setWeightMin(base.getWeightMin()); + r.setWeightMax(base.getWeightMax()); + r.setWeightLabel(formatWeightLabel(base.getWeightMin(), base.getWeightMax())); + r.setWeight(weight); + r.setChargeWeight(weight); + r.setFreightRate(freightRate); + r.setBaseFreightRate(baseFreightRate); + r.setDiscountPercent(discountPercent); + r.setRegistrationFee(registrationFee); + r.setFreightAmount(freightAmount); + r.setPayableTotal(payable); + r.setCurrency("RMB"); + return r; + } + + private void fillPackage(TmsPriceQuoteResult r, PrcCustomerPriceSet bind, PrcPriceSet set) + { + r.setPriceSetId(set.getPriceSetId()); + r.setPriceSetCode(set.getPriceSetCode()); + r.setPriceSetName(set.getPriceSetName()); + r.setSetType(set.getSetType()); + r.setBindPriority(bind.getBindPriority()); + r.setCustomerPricesetId(bind.getCustomerPricesetId()); + } + + private void validateRequest(TmsPriceQuoteRequest request) + { + if (request == null) + { + throw new ServiceException("request body is required"); + } + if (request.getCustomerId() == null) + { + throw new ServiceException("customerId is required"); + } + if (StringUtils.isEmpty(request.getProductCode())) + { + throw new ServiceException("productCode is required"); + } + if (StringUtils.isEmpty(request.getCountryCode()) && StringUtils.isEmpty(request.getCountryName())) + { + throw new ServiceException("countryCode or countryName is required"); + } + if (request.getWeight() == null || request.getWeight().compareTo(BigDecimal.ZERO) <= 0) + { + throw new ServiceException("weight must be > 0"); + } + } + + private Date resolveBizDate(String bizDate) + { + if (StringUtils.isEmpty(bizDate)) + { + return new Date(); + } + try + { + return new SimpleDateFormat("yyyy-MM-dd").parse(bizDate.trim()); + } + catch (ParseException e) + { + throw new ServiceException("bizDate format must be yyyy-MM-dd"); + } + } + + private String resolveProductName(String productCode) + { + BmsProduct p = productMapper.selectBmsProductByCode(productCode); + return p == null ? null : p.getProductName(); + } + + private static String emptyToNull(String s) + { + return StringUtils.isEmpty(s) ? null : s.trim(); + } + + private static String formatWeightLabel(BigDecimal min, BigDecimal max) + { + return strip(min) + "\u2264W\u2264" + strip(max); + } + + private static String strip(BigDecimal v) + { + if (v == null) + { + return ""; + } + return v.stripTrailingZeros().toPlainString(); + } +} diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml index 5ca2e97b..3861b6b7 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml @@ -69,4 +69,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + 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 aaeec24c..c96844d5 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 @@ -64,6 +64,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and ifnull(c.pr_enddate, '9999-12-31') >= #{beginDate} + + insert into prc_customer_price_set ( price_set_id, 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 ea1b11db..60a89e14 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 @@ -124,4 +124,44 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" delete from prc_price_set_detail where price_set_id = #{priceSetId} + + + + diff --git a/bms-back/sql/price/billing_menu.sql b/bms-back/sql/price/billing_menu.sql new file mode 100644 index 00000000..1c9035e6 --- /dev/null +++ b/bms-back/sql/price/billing_menu.sql @@ -0,0 +1,18 @@ +-- Billing center menus +SET NAMES utf8mb4; + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2300, N'计费中心', 0, 9, 'billing', NULL, '', '', 1, 0, 'M', '0', '0', '', 'money', 'admin', sysdate(), N'计费中心目录' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2300); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2301, N'下单计费试算', 2300, 1, 'quote', 'price/billing/quote', '', 'BillingQuote', 1, 0, 'C', '0', '0', 'price:billing:quote', 'edit', 'admin', sysdate(), N'下单计费试算' +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2301); + +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time) +SELECT 2302, N'试算执行', 2301, 1, '', NULL, 1, 0, 'F', '0', '0', 'price:billing:quote', '#', 'admin', sysdate() +FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2302); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, m.menu_id FROM (SELECT 2300 AS menu_id UNION SELECT 2301 UNION SELECT 2302) m +WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); diff --git a/bms-front/src/api/price/billing.js b/bms-front/src/api/price/billing.js new file mode 100644 index 00000000..98f34218 --- /dev/null +++ b/bms-front/src/api/price/billing.js @@ -0,0 +1,10 @@ +import request from '@/utils/request' + +/** Quote trial for billing center */ +export function quoteBilling(data) { + return request({ + url: '/price/billing/quote', + method: 'post', + data + }) +} diff --git a/bms-front/src/views/price/billing/quote.vue b/bms-front/src/views/price/billing/quote.vue new file mode 100644 index 00000000..5ec3e52a --- /dev/null +++ b/bms-front/src/views/price/billing/quote.vue @@ -0,0 +1,268 @@ + + + + +