增加试算功能
This commit is contained in:
+76
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -145,3 +145,9 @@ xss:
|
|||||||
excludes: /system/notice
|
excludes: /system/notice
|
||||||
# 匹配链接
|
# 匹配链接
|
||||||
urlPatterns: /system/*,/monitor/*,/tool/*
|
urlPatterns: /system/*,/monitor/*,/tool/*
|
||||||
|
|
||||||
|
# BMS TMS open API
|
||||||
|
bms:
|
||||||
|
tms:
|
||||||
|
# Agreed API identification code for TMS callers
|
||||||
|
api-code: BMS.TMS._Q2WnPXiXwGW2Dl-iSGNzLfrhUgQogNo
|
||||||
|
|||||||
+29
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -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; }
|
||||||
|
}
|
||||||
+105
@@ -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; }
|
||||||
|
}
|
||||||
+11
@@ -1,6 +1,8 @@
|
|||||||
package com.ruoyi.price.mapper;
|
package com.ruoyi.price.mapper;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.price.domain.BmsProductCountry;
|
import com.ruoyi.price.domain.BmsProductCountry;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,4 +19,13 @@ public interface BmsProductCountryMapper
|
|||||||
public int deleteByProductCode(String productCode);
|
public int deleteByProductCode(String productCode);
|
||||||
|
|
||||||
public int deleteByProductCodes(String[] productCodes);
|
public int deleteByProductCodes(String[] productCodes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按产品 + 国家 + 重量落入重量段匹配一条默认价
|
||||||
|
*/
|
||||||
|
public BmsProductCountry selectMatchByProductCountryWeight(
|
||||||
|
@Param("productCode") String productCode,
|
||||||
|
@Param("countryCode") String countryCode,
|
||||||
|
@Param("countryName") String countryName,
|
||||||
|
@Param("weight") BigDecimal weight);
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -29,4 +29,11 @@ public interface PrcCustomerPriceSetMapper
|
|||||||
@Param("priceSetId") Long priceSetId,
|
@Param("priceSetId") Long priceSetId,
|
||||||
@Param("beginDate") java.util.Date beginDate,
|
@Param("beginDate") java.util.Date beginDate,
|
||||||
@Param("endDate") java.util.Date endDate);
|
@Param("endDate") java.util.Date endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户在业务日有效、且套餐状态为生效(E) 的绑定,按优先级升序
|
||||||
|
*/
|
||||||
|
public java.util.List<PrcCustomerPriceSet> selectEffectiveByCustomer(
|
||||||
|
@Param("customerId") Long customerId,
|
||||||
|
@Param("bizDate") java.util.Date bizDate);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.price.mapper;
|
package com.ruoyi.price.mapper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
import com.ruoyi.price.domain.PrcPriceSetDetail;
|
import com.ruoyi.price.domain.PrcPriceSetDetail;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,4 +26,18 @@ public interface PrcPriceSetDetailMapper
|
|||||||
public int countProductByPriceSetId(Long priceSetId);
|
public int countProductByPriceSetId(Long priceSetId);
|
||||||
|
|
||||||
public int countByPriceSetId(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);
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -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);
|
||||||
|
}
|
||||||
+324
@@ -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<PrcCustomerPriceSet> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,4 +69,27 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
</foreach>
|
</foreach>
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
|
<select id="selectMatchByProductCountryWeight" resultMap="BmsProductCountryResult">
|
||||||
|
select detail_id, product_code, country_name, country_code, weight_min, weight_max,
|
||||||
|
freight_price, registration_fee, size_limit, last_mile, lead_time,
|
||||||
|
effective_date, created_at, updated_at
|
||||||
|
from bms_product_country
|
||||||
|
where product_code = #{productCode}
|
||||||
|
and weight_min <= #{weight}
|
||||||
|
and weight_max >= #{weight}
|
||||||
|
<choose>
|
||||||
|
<when test="countryCode != null and countryCode != ''">
|
||||||
|
and upper(country_code) = upper(#{countryCode})
|
||||||
|
</when>
|
||||||
|
<when test="countryName != null and countryName != ''">
|
||||||
|
and country_name = #{countryName}
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
and 1 = 0
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
order by weight_min asc, detail_id asc
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -64,6 +64,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
and ifnull(c.pr_enddate, '9999-12-31') >= #{beginDate}
|
and ifnull(c.pr_enddate, '9999-12-31') >= #{beginDate}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectEffectiveByCustomer" resultMap="PrcCustomerPriceSetResult">
|
||||||
|
select c.customer_priceset_id, c.price_set_id, c.customer_id, c.customer_name, c.bind_priority,
|
||||||
|
c.pr_begindate, c.pr_enddate, c.price_set_note,
|
||||||
|
c.created_by, c.created_at, c.updated_by, c.updated_at
|
||||||
|
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(c.pr_enddate, '9999-12-31') >= #{bizDate}
|
||||||
|
order by c.bind_priority asc, c.pr_begindate desc, c.customer_priceset_id asc
|
||||||
|
</select>
|
||||||
|
|
||||||
<insert id="insertPrcCustomerPriceSet" parameterType="PrcCustomerPriceSet" useGeneratedKeys="true" keyProperty="customerPricesetId">
|
<insert id="insertPrcCustomerPriceSet" parameterType="PrcCustomerPriceSet" useGeneratedKeys="true" keyProperty="customerPricesetId">
|
||||||
insert into prc_customer_price_set (
|
insert into prc_customer_price_set (
|
||||||
<if test="priceSetId != null">price_set_id,</if>
|
<if test="priceSetId != null">price_set_id,</if>
|
||||||
|
|||||||
@@ -124,4 +124,44 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
delete from prc_price_set_detail where price_set_id = #{priceSetId}
|
delete from prc_price_set_detail where price_set_id = #{priceSetId}
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
|
<select id="selectFixedMatch" resultMap="PrcPriceSetDetailResult">
|
||||||
|
<include refid="selectPrcPriceSetDetailVo"/>
|
||||||
|
where price_set_id = #{priceSetId}
|
||||||
|
and product_code = #{productCode}
|
||||||
|
and (
|
||||||
|
<if test="productCountryId != null">
|
||||||
|
product_country_id = #{productCountryId}
|
||||||
|
or
|
||||||
|
</if>
|
||||||
|
(
|
||||||
|
<choose>
|
||||||
|
<when test="countryCode != null and countryCode != ''">
|
||||||
|
upper(ifnull(country_code,'')) = upper(#{countryCode})
|
||||||
|
</when>
|
||||||
|
<when test="countryName != null and countryName != ''">
|
||||||
|
country_name = #{countryName}
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
1 = 0
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
<if test="weightLabel != null and weightLabel != ''">
|
||||||
|
and (weight_label is null or weight_label = #{weightLabel} or weight_label like concat('%', #{weightLabel}, '%'))
|
||||||
|
</if>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
order by
|
||||||
|
case when #{productCountryId} is not null and product_country_id = #{productCountryId} then 0 else 1 end,
|
||||||
|
psd_id asc
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectDiscountMatch" resultMap="PrcPriceSetDetailResult">
|
||||||
|
<include refid="selectPrcPriceSetDetailVo"/>
|
||||||
|
where price_set_id = #{priceSetId}
|
||||||
|
and product_code = #{productCode}
|
||||||
|
order by psd_id asc
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container billing-quote">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :xs="24" :lg="10">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header><span>试算参数</span></template>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
|
<el-form-item label="客户ID" prop="customerId">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.customerId"
|
||||||
|
:min="1"
|
||||||
|
:controls="false"
|
||||||
|
placeholder="请输入客户ID"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div class="form-hint">与套餐绑定客户ID一致;无套餐时走产品默认价</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="产品" prop="productCode">
|
||||||
|
<el-select
|
||||||
|
v-model="form.productCode"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="请选择产品"
|
||||||
|
style="width: 100%"
|
||||||
|
@change="handleProductChange"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in productOptions"
|
||||||
|
:key="item.productCode"
|
||||||
|
:label="item.productName + ' (' + item.productCode + ')'"
|
||||||
|
:value="item.productCode"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="目的国/地区" prop="countryCode">
|
||||||
|
<el-select
|
||||||
|
v-model="form.countryCode"
|
||||||
|
filterable
|
||||||
|
clearable
|
||||||
|
placeholder="请先选择产品"
|
||||||
|
style="width: 100%"
|
||||||
|
:disabled="!form.productCode"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in countryOptions"
|
||||||
|
:key="item.countryCode + '-' + (item.detailId || item.weightMin)"
|
||||||
|
:label="countryOptionLabel(item)"
|
||||||
|
:value="item.countryCode"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="计费重量(kg)" prop="weight">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.weight"
|
||||||
|
:min="0.001"
|
||||||
|
:precision="3"
|
||||||
|
:step="0.1"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="业务日期" prop="bizDate">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="form.bizDate"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="默认今天"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :loading="loading" @click="handleQuote">计算价格</el-button>
|
||||||
|
<el-button @click="resetForm">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<el-col :xs="24" :lg="14">
|
||||||
|
<el-card shadow="never">
|
||||||
|
<template #header><span>试算结果</span></template>
|
||||||
|
<el-empty v-if="!result && !errorMsg" description="请输入参数后点击计算价格" />
|
||||||
|
<el-alert v-else-if="errorMsg" :title="errorMsg" type="error" show-icon :closable="false" />
|
||||||
|
<div v-else class="quote-result">
|
||||||
|
<el-descriptions :column="2" border size="small">
|
||||||
|
<el-descriptions-item label="客户ID">{{ result.customerId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="产品">{{ result.productName || '-' }} ({{ result.productCode }})</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="国家">{{ result.countryName || '-' }} ({{ result.countryCode }})</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="计费重量(kg)">{{ result.chargeWeight ?? result.weight }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="重量段">{{ result.weightLabel || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="计价来源">
|
||||||
|
<el-tag size="small" :type="sourceTagType(result.matchSource)">{{ sourceLabel(result.matchSource) }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="套餐编号">{{ result.priceSetCode || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="套餐名称">{{ result.priceSetName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="套餐类型">{{ result.setType || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="币种">{{ result.currency || 'RMB' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="运费单价">{{ formatMoney(result.freightRate) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="折扣前单价">{{ formatMoney(result.baseFreightRate) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="折扣%">{{ result.discountPercent != null ? result.discountPercent : '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="运费金额">{{ formatMoney(result.freightAmount) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="挂号费">{{ formatMoney(result.registrationFee) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="应付总额">
|
||||||
|
<span class="payable">{{ formatMoney(result.payableTotal) }}</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="命中说明" :span="2">{{ result.matchMessage || '-' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<div class="formula-tip">计算公式:运费金额 = 运费单价 × 计费重量;应付总额 = 运费金额 + 挂号费</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup name="BillingQuote">
|
||||||
|
import { quoteBilling } from '@/api/price/billing'
|
||||||
|
import { listProduct, listProductCountries } from '@/api/price/product'
|
||||||
|
|
||||||
|
const { proxy } = getCurrentInstance()
|
||||||
|
const formRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
const productOptions = ref([])
|
||||||
|
const countryOptions = ref([])
|
||||||
|
const result = ref(null)
|
||||||
|
const errorMsg = ref('')
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
customerId: undefined,
|
||||||
|
productCode: undefined,
|
||||||
|
countryCode: undefined,
|
||||||
|
weight: undefined,
|
||||||
|
bizDate: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
customerId: [{ required: true, message: '请输入客户ID', trigger: 'blur' }],
|
||||||
|
productCode: [{ required: true, message: '请选择产品', trigger: 'change' }],
|
||||||
|
countryCode: [{ required: true, message: '请选择目的国/地区', trigger: 'change' }],
|
||||||
|
weight: [{ required: true, message: '请输入计费重量', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadProducts() {
|
||||||
|
listProduct({ pageNum: 1, pageSize: 500, status: '0' }).then(res => {
|
||||||
|
productOptions.value = res.rows || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueCountries(list) {
|
||||||
|
const map = new Map()
|
||||||
|
;(list || []).forEach(item => {
|
||||||
|
if (item && item.countryCode && !map.has(item.countryCode)) {
|
||||||
|
map.set(item.countryCode, item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Array.from(map.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryOptionLabel(item) {
|
||||||
|
const name = item.countryName || item.countryCode
|
||||||
|
return name + ' (' + item.countryCode + ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProductChange(productCode) {
|
||||||
|
form.countryCode = undefined
|
||||||
|
countryOptions.value = []
|
||||||
|
if (!productCode) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listProductCountries(productCode).then(res => {
|
||||||
|
countryOptions.value = uniqueCountries(res.data || [])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleQuote() {
|
||||||
|
formRef.value.validate(valid => {
|
||||||
|
if (!valid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
result.value = null
|
||||||
|
errorMsg.value = ''
|
||||||
|
const payload = {
|
||||||
|
customerId: form.customerId,
|
||||||
|
productCode: form.productCode,
|
||||||
|
countryCode: form.countryCode,
|
||||||
|
weight: form.weight
|
||||||
|
}
|
||||||
|
if (form.bizDate) {
|
||||||
|
payload.bizDate = form.bizDate
|
||||||
|
}
|
||||||
|
quoteBilling(payload).then(res => {
|
||||||
|
result.value = res.data || null
|
||||||
|
if (!result.value) {
|
||||||
|
errorMsg.value = res.msg || '计算失败'
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
errorMsg.value = (err && (err.msg || err.message)) || '计算失败'
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
form.customerId = undefined
|
||||||
|
form.productCode = undefined
|
||||||
|
form.countryCode = undefined
|
||||||
|
form.weight = undefined
|
||||||
|
form.bizDate = undefined
|
||||||
|
countryOptions.value = []
|
||||||
|
result.value = null
|
||||||
|
errorMsg.value = ''
|
||||||
|
proxy.$nextTick(() => {
|
||||||
|
formRef.value && formRef.value.clearValidate()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMoney(val) {
|
||||||
|
if (val === null || val === undefined || val === '') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const num = Number(val)
|
||||||
|
if (Number.isNaN(num)) {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return num.toFixed(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceLabel(source) {
|
||||||
|
const map = {
|
||||||
|
PACKAGE_FIXED: '客户固定价套餐',
|
||||||
|
PACKAGE_DISCOUNT: '客户折扣套餐',
|
||||||
|
PRODUCT_DEFAULT: '产品默认价',
|
||||||
|
FIXED: '客户固定价套餐',
|
||||||
|
DISCOUNT: '客户折扣套餐'
|
||||||
|
}
|
||||||
|
return map[source] || source || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceTagType(source) {
|
||||||
|
if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success'
|
||||||
|
if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning'
|
||||||
|
if (source === 'PRODUCT_DEFAULT') return 'info'
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProducts()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.billing-quote .form-hint {
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.billing-quote .payable {
|
||||||
|
color: #f56c6c;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.billing-quote .formula-tip {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user