diff --git a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties index 93de0055..7091d4b6 100644 --- a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -36,3 +36,74 @@ no.update.permission=您没有修改数据的权限,请联系管理员添加 no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}] no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}] no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}] + +##价格模块提示语(price) +# 附加费 PrcSurcharge +price.surcharge.scName.empty=附加费名称不能为空 +price.surcharge.chargeType.empty=计费方式不能为空 +price.surcharge.chargeValue.empty=计费数值不能为空 +price.surcharge.chargeValue.negative=计费数值不能为负数 +# 附加费关联 PrcSurchargeRel +price.surchargeRel.scId.empty=附加费ID不能为空 +price.surchargeRel.productId.empty=产品ID不能为空 +price.surchargeRel.productCode.empty=产品代码不能为空 +# 套餐 PrcPriceSet +price.set.notExists=套餐不存在 +price.set.name.empty=套餐名称不能为空 +price.set.name.exists=套餐名称已存在 +price.set.effectiveDate.empty=生效日期不能为空 +price.set.date.invalid=生效日期不能晚于失效日期 +price.set.discount.range=折扣模式默认折扣需在 1~99 +price.set.delete.statusNotAllowed=仅草稿或驳回状态的套餐允许删除:{0} +price.set.delete.boundCustomer=套餐【{0}】已绑定客户,请先解绑后再删除 +price.set.submit.statusNotAllowed=当前状态不允许提交审核 +price.set.approve.statusNotAllowed=仅审核中的套餐可以生效 +price.set.reject.statusNotAllowed=仅审核中的套餐可以驳回 +price.set.disable.statusNotAllowed=仅生效中的套餐可以停用 +price.set.edit.audit=审核中的套餐不允许修改 +price.set.edit.stopped=已停用的套餐不允许修改,请使用复用新建 +price.set.childEdit.audit=审核中的套餐不允许改明细或客户 +price.set.childEdit.stopped=已停用的套餐不允许改明细或客户 +price.set.detail.notExists=套餐明细不存在 +price.set.detail.productCode.empty=明细产品代码不能为空 +price.set.detail.country.empty=固定价明细必须填写国家 +price.set.detail.weight.empty=固定价明细必须填写重量段 +price.set.detail.freight.empty=固定价明细必须填写运费单价 +price.set.detail.registration.empty=固定价明细必须填写挂号费 +price.set.detail.roundMode.invalid=取值方式仅支持 UP / DOWN / HALF_UP +price.set.customer.notExists=客户绑定不存在 +price.set.customer.unbindAudit=审核中的套餐不允许解绑客户 +price.set.customer.customerId.empty=客户ID不能为空 +price.set.customer.beginDate.empty=客户绑定生效日期不能为空 +price.set.customer.date.invalid=客户绑定生效日期不能晚于失效日期 +price.set.customer.priority.invalid=优先级必须大于等于 1 +# 重量段 PrcWeight +price.weight.range.empty=起始重量和结束重量不能为空 +price.weight.range.invalid=起始重量必须小于结束重量 +# 分区 XtdZone +price.zone.delete.hasDivision=分区方案【{0}】存在区划明细,不允许删除 +# 产品导入 BmsProduct +price.product.import.fileEmpty=上传文件不能为空 +price.product.import.fileType=仅支持 xls / xlsx 文件 +price.product.import.sheetNotFound=未找到「产品目录」sheet,请上传威狮B2C小包价表格格式文件 +price.product.import.noData=产品目录无有效数据行 +price.product.import.regionEmpty=bms_region 无可用国家数据,请先同步/维护国家表后再导入 +price.product.import.partialFail=导入完成但存在失败项:新增 {0},更新 {1},跳过 {2};失败明细:{3} +price.product.import.effTooEarly=生效日期 {0} 早于当前开放版 {1},请使用更晚的生效日期导入 +price.product.import.needFeeAlter=请先执行 SQL:bms-back/sql/price/product_country_fee_alter.sql(缺少运费/挂号费字段) +price.product.import.needExpireAlter=请先执行 SQL:bms-back/sql/price/product_country_expire_alter.sql(缺少失效日期/版本唯一键) +price.product.import.success=导入成功:新增 {0} 个产品,更新 {1} 个,跳过 {2} 个;写入国家/重量段明细 {3} 行。 +price.product.import.success.detail1=国家明细:若新生效日晚于上次,保留历史版且上次失效日=本次生效日;同日则替换当前开放版。 +price.product.import.success.detail2=范围:产品目录 + 重量段 + 运费/挂号费 + 尺寸/尾程/时效;国家码来自 bms_region。 +price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0} +# 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约) +price.quote.priceNotHit=PRICE_NOT_HIT: no bms_product_country band for product/country/weight +price.quote.freightMissing1=FREIGHT_VALUE_MISSING: product default freight rate is null +price.quote.freightMissing2=FREIGHT_VALUE_MISSING: product default freight or registration is null +price.quote.freightMissing3=FREIGHT_VALUE_MISSING: freight rate is null +price.quote.requestBodyRequired=request body is required +price.quote.customerIdRequired=customerId is required +price.quote.productCodeRequired=productCode is required +price.quote.countryRequired=countryCode or countryName is required +price.quote.weightInvalid=weight must be > 0 +price.quote.bizDateInvalid=bizDate format must be yyyy-MM-dd diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java new file mode 100644 index 00000000..250a6110 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuote.java @@ -0,0 +1,56 @@ +package com.ruoyi.price.domain; + +import java.math.BigDecimal; +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** billing quote header prc_quote */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PrcQuote extends BaseEntity +{ + private static final long serialVersionUID = 1L; + + private Long quoteId; + private String quoteKey; + private Long customerId; + private String productCode; + private String productName; + private String countryCode; + private String countryName; + private BigDecimal weight; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date bizDate; + private Long tmsId; + + private String matchSource; + private String matchMessage; + private Long priceSetId; + private String priceSetCode; + private String priceSetName; + private String setType; + private Integer bindPriority; + private Long customerPricesetId; + private Long productCountryId; + private BigDecimal weightMin; + private BigDecimal weightMax; + private String weightLabel; + @JsonFormat(pattern = "yyyy-MM-dd") + private Date effectiveDate; + private BigDecimal chargeWeight; + private BigDecimal freightRate; + private BigDecimal baseFreightRate; + private BigDecimal discountPercent; + private BigDecimal registrationFee; + private BigDecimal freightAmount; + private BigDecimal surchargeAmount; + private BigDecimal payableTotal; + private String currency; + private String roundMode; + + private List fees; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuoteFee.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuoteFee.java new file mode 100644 index 00000000..9ce02bf0 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/PrcQuoteFee.java @@ -0,0 +1,28 @@ +package com.ruoyi.price.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** billing quote fee detail prc_quote_fee, one row per bms_fee_type */ +@Data +public class PrcQuoteFee implements Serializable +{ + private static final long serialVersionUID = 1L; + + private Long feeId; + private Long quoteId; + /** bms_fee_type: BASIC_FREIGHT / REGISTRATION_FEE / SURCHARGE */ + private String feeType; + private String feeName; + /** prc_surcharge.sc_id, 0 for freight/registration */ + private Long scId; + private BigDecimal amount; + private String currency; + private String roundMode; + private String remark; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createdAt; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteFeeLine.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteFeeLine.java new file mode 100644 index 00000000..8f79e7a3 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteFeeLine.java @@ -0,0 +1,21 @@ +package com.ruoyi.price.domain.vo; + +import java.io.Serializable; +import java.math.BigDecimal; +import lombok.Data; + +/** quote fee line keyed by bms_fee_type */ +@Data +public class TmsPriceQuoteFeeLine implements Serializable +{ + private static final long serialVersionUID = 1L; + + private String feeType; + private String feeName; + /** surcharge config id, 0 for freight/registration */ + private Long scId; + private BigDecimal amount; + private String currency; + private String roundMode; + private String remark; +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java index c8e67302..6acb28ab 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/vo/TmsPriceQuoteResult.java @@ -3,6 +3,7 @@ package com.ruoyi.price.domain.vo; import java.io.Serializable; import java.math.BigDecimal; import java.util.Date; +import java.util.List; import lombok.Data; /** @@ -43,8 +44,15 @@ public class TmsPriceQuoteResult implements Serializable private BigDecimal discountPercent; private BigDecimal registrationFee; private BigDecimal freightAmount; + /** 附加费合计(按产品关联命中后汇总) */ + private BigDecimal surchargeAmount; private BigDecimal payableTotal; private String currency = "RMB"; /** UP / DOWN / HALF_UP */ private String roundMode; + /** 费用明细:运费/挂号费各一行,附加费按具体名称各一行 */ + private List feeDetails; + private Long quoteId; + /** true=命中已落库试算 */ + private Boolean fromCache; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcQuoteFeeMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcQuoteFeeMapper.java new file mode 100644 index 00000000..ffb19238 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcQuoteFeeMapper.java @@ -0,0 +1,17 @@ +package com.ruoyi.price.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.ruoyi.price.domain.PrcQuoteFee; + +/** billing quote fee detail mapper */ +public interface PrcQuoteFeeMapper +{ + public List selectByQuoteId(Long quoteId); + + public int insertPrcQuoteFee(PrcQuoteFee fee); + + public int insertBatch(@Param("list") List fees); + + public int deleteByQuoteId(Long quoteId); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcQuoteMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcQuoteMapper.java new file mode 100644 index 00000000..770573b5 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcQuoteMapper.java @@ -0,0 +1,19 @@ +package com.ruoyi.price.mapper; + +import com.ruoyi.price.domain.PrcQuote; +import java.math.BigDecimal; +import org.apache.ibatis.annotations.Param; + +/** billing quote header mapper */ +public interface PrcQuoteMapper +{ + public PrcQuote selectPrcQuoteByKey(String quoteKey); + + public PrcQuote selectPrcQuoteById(Long quoteId); + + public int insertPrcQuote(PrcQuote quote); + + public int updateQuoteAmounts(@Param("quoteId") Long quoteId, + @Param("surchargeAmount") BigDecimal surchargeAmount, + @Param("payableTotal") BigDecimal payableTotal); +} diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java index 0669d5ac..153c5716 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/ITmsPriceQuoteService.java @@ -8,6 +8,7 @@ import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; * * Match order: effective customer package (bind_priority ASC) FIXED override * then DISCOUNT, else bms_product_country default freight/registration. + * After calc, persist fee lines by bms_fee_type; same trial params return stored result. * * @author ruoyi */ diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java index c921643d..6a7fe7b0 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/BmsProductServiceImpl.java @@ -26,6 +26,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.price.domain.BmsProduct; import com.ruoyi.price.domain.BmsProductCountry; @@ -139,13 +140,13 @@ public class BmsProductServiceImpl implements IBmsProductService { if (file == null || file.isEmpty()) { - throw new ServiceException("\u4e0a\u4f20\u6587\u4ef6\u4e0d\u80fd\u4e3a\u7a7a"); + throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty")); } String filename = file.getOriginalFilename(); if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx") || filename.toLowerCase(Locale.ROOT).endsWith(".xls"))) { - throw new ServiceException("\u4ec5\u652f\u6301 xls / xlsx \u6587\u4ef6"); + throw new ServiceException(MessageUtils.message("price.product.import.fileType")); } try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is)) @@ -153,19 +154,19 @@ public class BmsProductServiceImpl implements IBmsProductService Sheet catalog = workbook.getSheet(CATALOG_SHEET); if (catalog == null) { - throw new ServiceException("\u672a\u627e\u5230\u300c\u4ea7\u54c1\u76ee\u5f55\u300dsheet\uff0c\u8bf7\u4e0a\u4f20\u5a01\u72eeB2C\u5c0f\u5305\u4ef7\u8868\u683c\u5f0f\u6587\u4ef6"); + throw new ServiceException(MessageUtils.message("price.product.import.sheetNotFound")); } List products = parseCatalog(catalog); if (products.isEmpty()) { - throw new ServiceException("\u4ea7\u54c1\u76ee\u5f55\u65e0\u6709\u6548\u6570\u636e\u884c"); + throw new ServiceException(MessageUtils.message("price.product.import.noData")); } Map regionByNameCn = loadRegionByNameCn(); if (regionByNameCn.isEmpty()) { - throw new ServiceException("bms_region \u65e0\u53ef\u7528\u56fd\u5bb6\u6570\u636e\uff0c\u8bf7\u5148\u540c\u6b65/\u7ef4\u62a4\u56fd\u5bb6\u8868\u540e\u518d\u5bfc\u5165"); + throw new ServiceException(MessageUtils.message("price.product.import.regionEmpty")); } int successNum = 0; @@ -219,16 +220,15 @@ public class BmsProductServiceImpl implements IBmsProductService if (failureMsg.length() > 0) { - throw new ServiceException("\u5bfc\u5165\u5b8c\u6210\u4f46\u5b58\u5728\u5931\u8d25\u9879\uff1a\u65b0\u589e " + successNum + "\uff0c\u66f4\u65b0 " + updateNum - + "\uff0c\u8df3\u8fc7 " + skipNum + "\uff1b\u5931\u8d25\u660e\u7ec6\uff1a" + failureMsg); + throw new ServiceException(MessageUtils.message("price.product.import.partialFail", + successNum, updateNum, skipNum, failureMsg.toString())); } - String msg = "\u5bfc\u5165\u6210\u529f\uff1a\u65b0\u589e " + successNum + " \u4e2a\u4ea7\u54c1\uff0c\u66f4\u65b0 " + updateNum + " \u4e2a\uff0c\u8df3\u8fc7 " - + skipNum + " \u4e2a\uff1b\u5199\u5165\u56fd\u5bb6/\u91cd\u91cf\u6bb5\u660e\u7ec6 " + countryNum + " \u884c\u3002" - + "
\u56fd\u5bb6\u660e\u7ec6\uff1a\u82e5\u65b0\u751f\u6548\u65e5\u665a\u4e8e\u4e0a\u6b21\uff0c\u4fdd\u7559\u5386\u53f2\u7248\u4e14\u4e0a\u6b21\u5931\u6548\u65e5=\u672c\u6b21\u751f\u6548\u65e5\uff1b\u540c\u65e5\u5219\u66ff\u6362\u5f53\u524d\u5f00\u653e\u7248\u3002" - + "
\u8303\u56f4\uff1a\u4ea7\u54c1\u76ee\u5f55 + \u91cd\u91cf\u6bb5 + \u8fd0\u8d39/\u6302\u53f7\u8d39 + \u5c3a\u5bf8/\u5c3e\u7a0b/\u65f6\u6548\uff1b\u56fd\u5bb6\u7801\u6765\u81ea bms_region\u3002"; + String msg = MessageUtils.message("price.product.import.success", successNum, updateNum, skipNum, countryNum) + + "
" + MessageUtils.message("price.product.import.success.detail1") + + "
" + MessageUtils.message("price.product.import.success.detail2"); if (!unmatchedCountries.isEmpty()) { - msg += "
\u672a\u5339\u914d\u56fd\u5bb6\u8868\uff08country_code \u4e3a\u7a7a\uff09\uff1a" + String.join("\u3001", unmatchedCountries); + msg += "
" + MessageUtils.message("price.product.import.success.unmatched", String.join("\u3001", unmatchedCountries)); } return msg; } @@ -251,9 +251,8 @@ public class BmsProductServiceImpl implements IBmsProductService if (lastOpenEff != null && newEff != null && newEff.before(lastOpenEff)) { - throw new ServiceException("\u751f\u6548\u65e5\u671f " - + formatDate(newEff) + " \u65e9\u4e8e\u5f53\u524d\u5f00\u653e\u7248 " - + formatDate(lastOpenEff) + "\uff0c\u8bf7\u4f7f\u7528\u66f4\u665a\u7684\u751f\u6548\u65e5\u671f\u5bfc\u5165"); + throw new ServiceException(MessageUtils.message("price.product.import.effTooEarly", + formatDate(newEff), formatDate(lastOpenEff))); } if (lastOpenEff != null && newEff != null && newEff.after(lastOpenEff)) @@ -284,13 +283,11 @@ public class BmsProductServiceImpl implements IBmsProductService String tip = e.getMessage() == null ? "" : e.getMessage(); if (tip.contains("freight_price") || tip.contains("registration_fee") || tip.contains("Unknown column")) { - throw new ServiceException( - "\u8bf7\u5148\u6267\u884c SQL\uff1abms-back/sql/price/product_country_fee_alter.sql\uff08\u7f3a\u5c11\u8fd0\u8d39/\u6302\u53f7\u8d39\u5b57\u6bb5\uff09"); + throw new ServiceException(MessageUtils.message("price.product.import.needFeeAlter")); } if (tip.contains("expire_date") || tip.contains("uk_product_country_weight")) { - throw new ServiceException( - "\u8bf7\u5148\u6267\u884c SQL\uff1abms-back/sql/price/product_country_expire_alter.sql\uff08\u7f3a\u5c11\u5931\u6548\u65e5\u671f/\u7248\u672c\u552f\u4e00\u952e\uff09"); + throw new ServiceException(MessageUtils.message("price.product.import.needExpireAlter")); } throw e; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java index 1c7a711f..5cefea3a 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcPriceSetServiceImpl.java @@ -11,6 +11,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.price.domain.BmsProductCountry; import com.ruoyi.price.domain.PrcCustomerPriceSet; @@ -139,12 +140,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet db = requireSet(id); if (!STATUS_DRAFT.equals(db.getStatus()) && !STATUS_REJECT.equals(db.getStatus())) { - throw new ServiceException("仅草稿或驳回状态的套餐允许删除:" + displayName(db)); + throw new ServiceException(MessageUtils.message("price.set.delete.statusNotAllowed", displayName(db))); } int bound = customerMapper.countByPriceSetId(id); if (bound > 0) { - throw new ServiceException("套餐【" + displayName(db) + "】已绑定客户,请先解绑后再删除"); + throw new ServiceException(MessageUtils.message("price.set.delete.boundCustomer", displayName(db))); } detailMapper.deletePrcPriceSetDetailByPriceSetId(id); } @@ -157,7 +158,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet db = requireSet(priceSetId); if (!STATUS_DRAFT.equals(db.getStatus()) && !STATUS_REJECT.equals(db.getStatus())) { - throw new ServiceException("当前状态不允许提交审核"); + throw new ServiceException(MessageUtils.message("price.set.submit.statusNotAllowed")); } PrcPriceSet payload = selectPrcPriceSetById(priceSetId); List items = validate(payload); @@ -175,7 +176,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet db = requireSet(priceSetId); if (!STATUS_AUDIT.equals(db.getStatus())) { - throw new ServiceException("仅审核中的套餐可以生效"); + throw new ServiceException(MessageUtils.message("price.set.approve.statusNotAllowed")); } PrcPriceSet upd = new PrcPriceSet(); upd.setPriceSetId(priceSetId); @@ -190,7 +191,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet db = requireSet(priceSetId); if (!STATUS_AUDIT.equals(db.getStatus())) { - throw new ServiceException("仅审核中的套餐可以驳回"); + throw new ServiceException(MessageUtils.message("price.set.reject.statusNotAllowed")); } PrcPriceSet upd = new PrcPriceSet(); upd.setPriceSetId(priceSetId); @@ -205,7 +206,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet db = requireSet(priceSetId); if (!STATUS_EFFECT.equals(db.getStatus())) { - throw new ServiceException("仅生效中的套餐可以停用"); + throw new ServiceException(MessageUtils.message("price.set.disable.statusNotAllowed")); } PrcPriceSet upd = new PrcPriceSet(); upd.setPriceSetId(priceSetId); @@ -221,7 +222,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet src = selectPrcPriceSetById(priceSetId); if (src == null) { - throw new ServiceException("套餐不存在"); + throw new ServiceException(MessageUtils.message("price.set.notExists")); } PrcPriceSet copy = new PrcPriceSet(); copy.setPriceSetName(src.getPriceSetName() + "-副本"); @@ -462,7 +463,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSetDetail old = detailMapper.selectPrcPriceSetDetailById(detail.getPsdId()); if (old == null) { - throw new ServiceException("套餐明细不存在"); + throw new ServiceException(MessageUtils.message("price.set.detail.notExists")); } PrcPriceSet db = requireSet(old.getPriceSetId()); assertChildEditable(db); @@ -512,7 +513,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcCustomerPriceSet old = customerMapper.selectPrcCustomerPriceSetById(customer.getCustomerPricesetId()); if (old == null) { - throw new ServiceException("客户绑定不存在"); + throw new ServiceException(MessageUtils.message("price.set.customer.notExists")); } PrcPriceSet db = requireSet(old.getPriceSetId()); assertChildEditable(db); @@ -534,7 +535,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet db = requireSet(old.getPriceSetId()); if (STATUS_AUDIT.equals(db.getStatus())) { - throw new ServiceException("审核中的套餐不允许解绑客户"); + throw new ServiceException(MessageUtils.message("price.set.customer.unbindAudit")); } } return customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds); @@ -575,15 +576,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { if (StringUtils.isEmpty(set.getPriceSetName())) { - throw new ServiceException("套餐名称不能为空"); + throw new ServiceException(MessageUtils.message("price.set.name.empty")); } if (set.getEffectiveDate() == null) { - throw new ServiceException("生效日期不能为空"); + throw new ServiceException(MessageUtils.message("price.set.effectiveDate.empty")); } if (set.getExpireDate() != null && set.getEffectiveDate().after(set.getExpireDate())) { - throw new ServiceException("生效日期不能晚于失效日期"); + throw new ServiceException(MessageUtils.message("price.set.date.invalid")); } if (TYPE_DISCOUNT.equals(set.getSetType())) { @@ -591,12 +592,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService || set.getDefaultDiscount().compareTo(BigDecimal.ONE) < 0 || set.getDefaultDiscount().compareTo(new BigDecimal("99")) > 0) { - throw new ServiceException("折扣模式默认折扣需在 1~99"); + throw new ServiceException(MessageUtils.message("price.set.discount.range")); } } if (setMapper.countByName(set) > 0) { - throw new ServiceException("套餐名称已存在"); + throw new ServiceException(MessageUtils.message("price.set.name.exists")); } if (strictSubmit) { @@ -608,25 +609,25 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { if (StringUtils.isEmpty(detail.getProductCode())) { - throw new ServiceException("明细产品代码不能为空"); + throw new ServiceException(MessageUtils.message("price.set.detail.productCode.empty")); } if (TYPE_FIXED.equals(set.getSetType())) { if (StringUtils.isEmpty(detail.getCountryCode()) && StringUtils.isEmpty(detail.getCountryName())) { - throw new ServiceException("\u56fa\u5b9a\u4ef7\u660e\u7ec6\u5fc5\u987b\u586b\u5199\u56fd\u5bb6"); + throw new ServiceException(MessageUtils.message("price.set.detail.country.empty")); } if (detail.getWsdId() == null && StringUtils.isEmpty(detail.getWeightLabel())) { - throw new ServiceException("固定价明细必须填写重量段"); + throw new ServiceException(MessageUtils.message("price.set.detail.weight.empty")); } if (detail.getFvValue() == null) { - throw new ServiceException("固定价明细必须填写运费单价"); + throw new ServiceException(MessageUtils.message("price.set.detail.freight.empty")); } if (detail.getFvRegistrationFee() == null) { - throw new ServiceException("固定价明细必须填写挂号费"); + throw new ServiceException(MessageUtils.message("price.set.detail.registration.empty")); } fillProductCountryId(detail); } @@ -639,7 +640,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService String mode = detail.getRoundMode().trim().toUpperCase(); if (!"UP".equals(mode) && !"DOWN".equals(mode) && !"HALF_UP".equals(mode)) { - throw new ServiceException("取值方式仅支持 UP / DOWN / HALF_UP"); + throw new ServiceException(MessageUtils.message("price.set.detail.roundMode.invalid")); } detail.setRoundMode(mode); } @@ -720,7 +721,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { if (customer.getCustomerId() == null || customer.getCustomerId().longValue() <= 0) { - throw new ServiceException("客户ID不能为空"); + throw new ServiceException(MessageUtils.message("price.set.customer.customerId.empty")); } if (customer.getPrBegindate() == null) { @@ -728,15 +729,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } if (customer.getPrBegindate() == null) { - throw new ServiceException("客户绑定生效日期不能为空"); + throw new ServiceException(MessageUtils.message("price.set.customer.beginDate.empty")); } if (customer.getPrEnddate() != null && customer.getPrBegindate().after(customer.getPrEnddate())) { - throw new ServiceException("客户绑定生效日期不能晚于失效日期"); + throw new ServiceException(MessageUtils.message("price.set.customer.date.invalid")); } if (customer.getBindPriority() != null && customer.getBindPriority().intValue() < 1) { - throw new ServiceException("优先级必须大于等于 1"); + throw new ServiceException(MessageUtils.message("price.set.customer.priority.invalid")); } } @@ -744,11 +745,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { if (STATUS_AUDIT.equals(db.getStatus())) { - throw new ServiceException("审核中的套餐不允许修改"); + throw new ServiceException(MessageUtils.message("price.set.edit.audit")); } if (STATUS_STOP.equals(db.getStatus())) { - throw new ServiceException("已停用的套餐不允许修改,请使用复用新建"); + throw new ServiceException(MessageUtils.message("price.set.edit.stopped")); } } @@ -756,11 +757,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { if (STATUS_AUDIT.equals(db.getStatus())) { - throw new ServiceException("审核中的套餐不允许改明细或客户"); + throw new ServiceException(MessageUtils.message("price.set.childEdit.audit")); } if (STATUS_STOP.equals(db.getStatus())) { - throw new ServiceException("已停用的套餐不允许改明细或客户"); + throw new ServiceException(MessageUtils.message("price.set.childEdit.stopped")); } } @@ -780,7 +781,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService PrcPriceSet db = setMapper.selectPrcPriceSetById(priceSetId); if (db == null) { - throw new ServiceException("套餐不存在"); + throw new ServiceException(MessageUtils.message("price.set.notExists")); } return db; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcSurchargeRelServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcSurchargeRelServiceImpl.java index 94c2c4fa..153c6c74 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcSurchargeRelServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcSurchargeRelServiceImpl.java @@ -4,6 +4,7 @@ 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.MessageUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.price.domain.PrcSurchargeRel; import com.ruoyi.price.mapper.PrcSurchargeRelMapper; @@ -65,15 +66,15 @@ public class PrcSurchargeRelServiceImpl implements IPrcSurchargeRelService { if (rel.getScId() == null) { - throw new ServiceException("附加费ID不能为空"); + throw new ServiceException(MessageUtils.message("price.surchargeRel.scId.empty")); } if (rel.getProductId() == null) { - throw new ServiceException("产品ID不能为空"); + throw new ServiceException(MessageUtils.message("price.surchargeRel.productId.empty")); } if (StringUtils.isEmpty(rel.getProductCode())) { - throw new ServiceException("产品代码不能为空"); + throw new ServiceException(MessageUtils.message("price.surchargeRel.productCode.empty")); } } } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcSurchargeServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcSurchargeServiceImpl.java index 9e8b08d1..069e54f5 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcSurchargeServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcSurchargeServiceImpl.java @@ -4,6 +4,7 @@ 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.MessageUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.price.domain.PrcSurcharge; import com.ruoyi.price.mapper.PrcSurchargeMapper; @@ -68,19 +69,19 @@ public class PrcSurchargeServiceImpl implements IPrcSurchargeService { if (StringUtils.isEmpty(surcharge.getScName())) { - throw new ServiceException("附加费名称不能为空"); + throw new ServiceException(MessageUtils.message("price.surcharge.scName.empty")); } if (StringUtils.isEmpty(surcharge.getChargeType())) { - throw new ServiceException("计费方式不能为空"); + throw new ServiceException(MessageUtils.message("price.surcharge.chargeType.empty")); } if (surcharge.getChargeValue() == null) { - throw new ServiceException("计费数值不能为空"); + throw new ServiceException(MessageUtils.message("price.surcharge.chargeValue.empty")); } if (surcharge.getChargeValue().compareTo(java.math.BigDecimal.ZERO) < 0) { - throw new ServiceException("计费数值不能为负数"); + throw new ServiceException(MessageUtils.message("price.surcharge.chargeValue.negative")); } } } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcWeightServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcWeightServiceImpl.java index 5a0e63e4..741dc70d 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcWeightServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/PrcWeightServiceImpl.java @@ -4,6 +4,7 @@ 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.MessageUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.price.domain.PrcWeightSectionDetail; import com.ruoyi.price.domain.PrcWeightSectionTemplate; @@ -106,11 +107,11 @@ public class PrcWeightServiceImpl implements IPrcWeightService { if (detail.getWlStartweight() == null || detail.getWlEndweight() == null) { - throw new ServiceException("起始重量和结束重量不能为空"); + throw new ServiceException(MessageUtils.message("price.weight.range.empty")); } if (detail.getWlStartweight().compareTo(detail.getWlEndweight()) >= 0) { - throw new ServiceException("起始重量必须小于结束重量"); + throw new ServiceException(MessageUtils.message("price.weight.range.invalid")); } } } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java index fb274573..c2147661 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -4,17 +4,28 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.MessageUtils; import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.sign.Md5Utils; 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.PrcQuote; +import com.ruoyi.price.domain.PrcQuoteFee; +import com.ruoyi.price.domain.PrcSurchargeRel; +import com.ruoyi.price.domain.vo.TmsPriceQuoteFeeLine; import com.ruoyi.price.domain.vo.TmsPriceQuoteRequest; import com.ruoyi.price.domain.vo.TmsPriceQuoteResult; import com.ruoyi.price.mapper.BmsProductCountryMapper; @@ -22,10 +33,14 @@ import com.ruoyi.price.mapper.BmsProductMapper; import com.ruoyi.price.mapper.PrcCustomerPriceSetMapper; import com.ruoyi.price.mapper.PrcPriceSetDetailMapper; import com.ruoyi.price.mapper.PrcPriceSetMapper; +import com.ruoyi.price.mapper.PrcQuoteFeeMapper; +import com.ruoyi.price.mapper.PrcQuoteMapper; +import com.ruoyi.price.mapper.PrcSurchargeRelMapper; import com.ruoyi.price.service.ITmsPriceQuoteService; /** * TMS freight quote: customer package first, then product default. + * After calc: persist fee lines by bms_fee_type; same trial params return stored rows. * * @author ruoyi */ @@ -38,6 +53,10 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService private static final String SRC_DISCOUNT = "PACKAGE_DISCOUNT"; private static final String SRC_DEFAULT = "PRODUCT_DEFAULT"; + private static final String FEE_BASIC = "BASIC_FREIGHT"; + private static final String FEE_REG = "REGISTRATION_FEE"; + private static final String FEE_SUR = "SURCHARGE"; + @Autowired private PrcCustomerPriceSetMapper customerPriceSetMapper; @@ -53,11 +72,48 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService @Autowired private BmsProductMapper productMapper; + @Autowired + private PrcSurchargeRelMapper surchargeRelMapper; + + @Autowired + private PrcQuoteMapper quoteMapper; + + @Autowired + private PrcQuoteFeeMapper quoteFeeMapper; + @Override + @Transactional(rollbackFor = Exception.class) public TmsPriceQuoteResult quote(TmsPriceQuoteRequest request) { validateRequest(request); + Date bizDate = resolveBizDate(request.getBizDate()); + BigDecimal weight = request.getWeight().setScale(3, RoundingMode.HALF_UP); + String quoteKey = buildQuoteKey(request, weight, bizDate); + PrcQuote cached = quoteMapper.selectPrcQuoteByKey(quoteKey); + if (cached != null) + { + List fees = quoteFeeMapper.selectByQuoteId(cached.getQuoteId()); + if (!isStaleSurchargeFees(fees)) + { + return toResult(cached, fees, true); + } + TmsPriceQuoteResult refreshed = doQuote(request, bizDate); + applySurchargesAndFees(refreshed); + replaceQuoteFees(cached.getQuoteId(), refreshed); + refreshed.setQuoteId(cached.getQuoteId()); + refreshed.setFromCache(false); + return refreshed; + } + + TmsPriceQuoteResult result = doQuote(request, bizDate); + applySurchargesAndFees(result); + persistQuote(quoteKey, request, weight, bizDate, result); + return result; + } + + private TmsPriceQuoteResult doQuote(TmsPriceQuoteRequest request, Date bizDate) + { String productCode = request.getProductCode().trim(); String countryCode = emptyToNull(request.getCountryCode()); String countryName = emptyToNull(request.getCountryName()); @@ -66,13 +122,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService countryCode = countryCode.toUpperCase(); } BigDecimal weight = request.getWeight(); - Date bizDate = resolveBizDate(request.getBizDate()); BmsProductCountry base = productCountryMapper.selectMatchByProductCountryWeight( productCode, countryCode, countryName, weight, bizDate); if (base == null) { - throw new ServiceException("PRICE_NOT_HIT: no bms_product_country band for product/country/weight"); + throw new ServiceException(MessageUtils.message("price.quote.priceNotHit")); } String productName = resolveProductName(productCode); @@ -185,7 +240,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } if (base.getFreightPrice() == null) { - throw new ServiceException("FREIGHT_VALUE_MISSING: product default freight rate is null"); + throw new ServiceException(MessageUtils.message("price.quote.freightMissing1")); } BigDecimal baseRate = base.getFreightPrice(); RoundingMode rm = resolveRoundingMode(roundMode); @@ -204,7 +259,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService { if (base.getFreightPrice() == null || base.getRegistrationFee() == null) { - throw new ServiceException("FREIGHT_VALUE_MISSING: product default freight or registration is null"); + throw new ServiceException(MessageUtils.message("price.quote.freightMissing2")); } TmsPriceQuoteResult r = buildFromRates(SRC_DEFAULT, base.getFreightPrice(), null, null, base.getRegistrationFee(), base, productCode, productName, weight, null); @@ -220,7 +275,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService { if (freightRate == null) { - throw new ServiceException("FREIGHT_VALUE_MISSING: freight rate is null"); + throw new ServiceException(MessageUtils.message("price.quote.freightMissing3")); } if (registrationFee == null) { @@ -249,12 +304,384 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService r.setDiscountPercent(discountPercent); r.setRegistrationFee(registrationFee); r.setFreightAmount(freightAmount); + r.setSurchargeAmount(BigDecimal.ZERO); r.setPayableTotal(payable); r.setCurrency("RMB"); r.setRoundMode(mode); + r.setFromCache(false); return r; } + private void applySurchargesAndFees(TmsPriceQuoteResult result) + { + BigDecimal freightAmount = nvl(result.getFreightAmount()); + BigDecimal registrationFee = nvl(result.getRegistrationFee()); + RoundingMode rm = resolveRoundingMode(result.getRoundMode()); + String currency = StringUtils.isEmpty(result.getCurrency()) ? "RMB" : result.getCurrency(); + + List rels = loadMatchedSurcharges(result); + List lines = new ArrayList(); + lines.add(feeLine(FEE_BASIC, feeNameOf(FEE_BASIC), 0L, freightAmount, currency, result.getRoundMode(), null)); + lines.add(feeLine(FEE_REG, feeNameOf(FEE_REG), 0L, registrationFee, currency, result.getRoundMode(), null)); + + BigDecimal surchargeTotal = BigDecimal.ZERO; + Map surchargeLines = new LinkedHashMap(); + for (PrcSurchargeRel rel : rels) + { + BigDecimal itemAmount = calcSurchargeAmount(rel, result.getWeight(), freightAmount); + if (itemAmount == null) + { + continue; + } + Long scId = rel.getScId() == null ? 0L : rel.getScId(); + TmsPriceQuoteFeeLine exist = surchargeLines.get(scId); + if (exist != null) + { + exist.setAmount(nvl(exist.getAmount()).add(itemAmount)); + continue; + } + String feeType = StringUtils.isEmpty(rel.getFeeType()) ? FEE_SUR : rel.getFeeType(); + String feeName = StringUtils.isEmpty(rel.getScName()) ? feeNameOf(FEE_SUR) : rel.getScName(); + String itemCcy = StringUtils.isEmpty(rel.getCurrency()) ? currency : rel.getCurrency(); + String itemRound = StringUtils.isEmpty(rel.getRoundMode()) ? result.getRoundMode() : rel.getRoundMode(); + surchargeLines.put(scId, feeLine(feeType, feeName, scId, itemAmount, itemCcy, itemRound, rel.getScNote())); + } + for (TmsPriceQuoteFeeLine line : surchargeLines.values()) + { + line.setAmount(nvl(line.getAmount()).setScale(2, rm)); + surchargeTotal = surchargeTotal.add(nvl(line.getAmount())); + lines.add(line); + } + surchargeTotal = surchargeTotal.setScale(2, rm); + result.setSurchargeAmount(surchargeTotal); + result.setPayableTotal(freightAmount.add(registrationFee).add(surchargeTotal).setScale(2, rm)); + result.setFeeDetails(lines); + } + + private List loadMatchedSurcharges(TmsPriceQuoteResult result) + { + List matched = new ArrayList(); + if (result == null || StringUtils.isEmpty(result.getProductCode())) + { + return matched; + } + PrcSurchargeRel query = new PrcSurchargeRel(); + query.setProductCode(result.getProductCode()); + List rels = surchargeRelMapper.selectPrcSurchargeRelList(query); + if (rels == null) + { + return matched; + } + for (PrcSurchargeRel rel : rels) + { + if (surchargeApplies(rel, result)) + { + matched.add(rel); + } + } + return matched; + } + + private boolean surchargeApplies(PrcSurchargeRel rel, TmsPriceQuoteResult result) + { + if (rel == null) + { + return false; + } + if (StringUtils.isNotEmpty(rel.getSsCode()) && !"on".equalsIgnoreCase(rel.getSsCode())) + { + return false; + } + if (StringUtils.isNotEmpty(rel.getCountryCode()) && StringUtils.isNotEmpty(result.getCountryCode()) + && !rel.getCountryCode().equalsIgnoreCase(result.getCountryCode())) + { + return false; + } + if (rel.getDetailId() == null) + { + return true; + } + return result.getProductCountryId() != null && rel.getDetailId().equals(result.getProductCountryId()); + } + + private BigDecimal calcSurchargeAmount(PrcSurchargeRel rel, BigDecimal weight, BigDecimal freightAmount) + { + if (rel.getChargeValue() == null) + { + return BigDecimal.ZERO; + } + RoundingMode rm = resolveRoundingMode(rel.getRoundMode()); + String chargeType = rel.getChargeType() == null ? "FIXED" : rel.getChargeType().toUpperCase(); + BigDecimal amount; + if ("WEIGHT".equals(chargeType)) + { + amount = rel.getChargeValue().multiply(nvl(weight)); + } + else if ("RATE".equals(chargeType)) + { + amount = nvl(freightAmount).multiply(rel.getChargeValue()); + } + else + { + amount = rel.getChargeValue(); + } + return amount.setScale(2, rm); + } + + private void persistQuote(String quoteKey, TmsPriceQuoteRequest request, BigDecimal weight, + Date bizDate, TmsPriceQuoteResult result) + { + PrcQuote quote = new PrcQuote(); + quote.setQuoteKey(quoteKey); + quote.setCustomerId(request.getCustomerId()); + quote.setProductCode(result.getProductCode()); + quote.setProductName(result.getProductName()); + quote.setCountryCode(result.getCountryCode()); + quote.setCountryName(result.getCountryName()); + quote.setWeight(weight); + quote.setBizDate(bizDate); + quote.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId())); + quote.setMatchSource(result.getMatchSource()); + quote.setMatchMessage(result.getMatchMessage()); + quote.setPriceSetId(result.getPriceSetId()); + quote.setPriceSetCode(result.getPriceSetCode()); + quote.setPriceSetName(result.getPriceSetName()); + quote.setSetType(result.getSetType()); + quote.setBindPriority(result.getBindPriority()); + quote.setCustomerPricesetId(result.getCustomerPricesetId()); + quote.setProductCountryId(result.getProductCountryId()); + quote.setWeightMin(result.getWeightMin()); + quote.setWeightMax(result.getWeightMax()); + quote.setWeightLabel(result.getWeightLabel()); + quote.setEffectiveDate(result.getEffectiveDate()); + quote.setChargeWeight(result.getChargeWeight()); + quote.setFreightRate(result.getFreightRate()); + quote.setBaseFreightRate(result.getBaseFreightRate()); + quote.setDiscountPercent(result.getDiscountPercent()); + quote.setRegistrationFee(result.getRegistrationFee()); + quote.setFreightAmount(result.getFreightAmount()); + quote.setSurchargeAmount(result.getSurchargeAmount()); + quote.setPayableTotal(result.getPayableTotal()); + quote.setCurrency(result.getCurrency()); + quote.setRoundMode(result.getRoundMode()); + try + { + quoteMapper.insertPrcQuote(quote); + } + catch (DuplicateKeyException ex) + { + PrcQuote existed = quoteMapper.selectPrcQuoteByKey(quoteKey); + if (existed == null) + { + throw ex; + } + TmsPriceQuoteResult cached = toResult(existed, quoteFeeMapper.selectByQuoteId(existed.getQuoteId()), true); + copyResult(cached, result); + return; + } + + List fees = toFeeRows(quote.getQuoteId(), result); + if (!fees.isEmpty()) + { + quoteFeeMapper.insertBatch(fees); + } + result.setQuoteId(quote.getQuoteId()); + result.setFromCache(false); + } + + private boolean isStaleSurchargeFees(List fees) + { + if (fees == null || fees.isEmpty()) + { + return false; + } + for (PrcQuoteFee fee : fees) + { + if (!FEE_SUR.equals(fee.getFeeType())) + { + continue; + } + if (fee.getScId() == null || fee.getScId().longValue() == 0L) + { + return true; + } + String name = fee.getFeeName() == null ? "" : fee.getFeeName().trim(); + if (name.isEmpty() || "SURCHARGE".equalsIgnoreCase(name) + || "\u9644\u52a0\u8d39".equals(name)) + { + return true; + } + } + return false; + } + + private void replaceQuoteFees(Long quoteId, TmsPriceQuoteResult result) + { + quoteFeeMapper.deleteByQuoteId(quoteId); + List fees = toFeeRows(quoteId, result); + if (!fees.isEmpty()) + { + quoteFeeMapper.insertBatch(fees); + } + quoteMapper.updateQuoteAmounts(quoteId, result.getSurchargeAmount(), result.getPayableTotal()); + } + + private List toFeeRows(Long quoteId, TmsPriceQuoteResult result) + { + List fees = new ArrayList(); + if (result.getFeeDetails() == null) + { + return fees; + } + for (TmsPriceQuoteFeeLine line : result.getFeeDetails()) + { + PrcQuoteFee fee = new PrcQuoteFee(); + fee.setQuoteId(quoteId); + fee.setFeeType(line.getFeeType()); + fee.setFeeName(line.getFeeName()); + fee.setScId(line.getScId() == null ? 0L : line.getScId()); + fee.setAmount(nvl(line.getAmount())); + fee.setCurrency(line.getCurrency()); + fee.setRoundMode(line.getRoundMode()); + fee.setRemark(line.getRemark()); + fees.add(fee); + } + return fees; + } + + private TmsPriceQuoteResult toResult(PrcQuote quote, List fees, boolean fromCache) + { + TmsPriceQuoteResult r = new TmsPriceQuoteResult(); + r.setQuoteId(quote.getQuoteId()); + r.setFromCache(fromCache); + r.setMatchSource(quote.getMatchSource()); + r.setMatchMessage(quote.getMatchMessage()); + r.setCustomerId(quote.getCustomerId()); + r.setPriceSetId(quote.getPriceSetId()); + r.setPriceSetCode(quote.getPriceSetCode()); + r.setPriceSetName(quote.getPriceSetName()); + r.setSetType(quote.getSetType()); + r.setBindPriority(quote.getBindPriority()); + r.setCustomerPricesetId(quote.getCustomerPricesetId()); + r.setProductCode(quote.getProductCode()); + r.setProductName(quote.getProductName()); + r.setCountryCode(quote.getCountryCode()); + r.setCountryName(quote.getCountryName()); + r.setProductCountryId(quote.getProductCountryId()); + r.setWeightMin(quote.getWeightMin()); + r.setWeightMax(quote.getWeightMax()); + r.setWeightLabel(quote.getWeightLabel()); + r.setEffectiveDate(quote.getEffectiveDate()); + r.setWeight(quote.getWeight()); + r.setChargeWeight(quote.getChargeWeight()); + r.setFreightRate(quote.getFreightRate()); + r.setBaseFreightRate(quote.getBaseFreightRate()); + r.setDiscountPercent(quote.getDiscountPercent()); + r.setRegistrationFee(quote.getRegistrationFee()); + r.setFreightAmount(quote.getFreightAmount()); + r.setSurchargeAmount(quote.getSurchargeAmount()); + r.setPayableTotal(quote.getPayableTotal()); + r.setCurrency(quote.getCurrency()); + r.setRoundMode(quote.getRoundMode()); + List lines = new ArrayList(); + if (fees != null) + { + for (PrcQuoteFee fee : fees) + { + lines.add(feeLine(fee.getFeeType(), fee.getFeeName(), fee.getScId(), fee.getAmount(), + fee.getCurrency(), fee.getRoundMode(), fee.getRemark())); + } + } + r.setFeeDetails(lines); + return r; + } + + private void copyResult(TmsPriceQuoteResult from, TmsPriceQuoteResult to) + { + to.setQuoteId(from.getQuoteId()); + to.setFromCache(from.getFromCache()); + to.setMatchSource(from.getMatchSource()); + to.setMatchMessage(from.getMatchMessage()); + to.setCustomerId(from.getCustomerId()); + to.setPriceSetId(from.getPriceSetId()); + to.setPriceSetCode(from.getPriceSetCode()); + to.setPriceSetName(from.getPriceSetName()); + to.setSetType(from.getSetType()); + to.setBindPriority(from.getBindPriority()); + to.setCustomerPricesetId(from.getCustomerPricesetId()); + to.setProductCode(from.getProductCode()); + to.setProductName(from.getProductName()); + to.setCountryCode(from.getCountryCode()); + to.setCountryName(from.getCountryName()); + to.setProductCountryId(from.getProductCountryId()); + to.setWeightMin(from.getWeightMin()); + to.setWeightMax(from.getWeightMax()); + to.setWeightLabel(from.getWeightLabel()); + to.setEffectiveDate(from.getEffectiveDate()); + to.setWeight(from.getWeight()); + to.setChargeWeight(from.getChargeWeight()); + to.setFreightRate(from.getFreightRate()); + to.setBaseFreightRate(from.getBaseFreightRate()); + to.setDiscountPercent(from.getDiscountPercent()); + to.setRegistrationFee(from.getRegistrationFee()); + to.setFreightAmount(from.getFreightAmount()); + to.setSurchargeAmount(from.getSurchargeAmount()); + to.setPayableTotal(from.getPayableTotal()); + to.setCurrency(from.getCurrency()); + to.setRoundMode(from.getRoundMode()); + to.setFeeDetails(from.getFeeDetails()); + } + + private String buildQuoteKey(TmsPriceQuoteRequest request, BigDecimal weight, Date bizDate) + { + String countryCode = emptyToNull(request.getCountryCode()); + if (countryCode != null) + { + countryCode = countryCode.toUpperCase(); + } + String countryName = emptyToNull(request.getCountryName()); + String biz = new SimpleDateFormat("yyyy-MM-dd").format(bizDate); + String raw = request.getCustomerId() + "|" + + request.getProductCode().trim() + "|" + + nvlStr(countryCode) + "|" + + nvlStr(countryName) + "|" + + weight.toPlainString() + "|" + + biz + "|" + + (request.getTmsId() == null ? "" : request.getTmsId()); + return Md5Utils.hash(raw); + } + + private static TmsPriceQuoteFeeLine feeLine(String feeType, String feeName, Long scId, BigDecimal amount, + String currency, String roundMode, String remark) + { + TmsPriceQuoteFeeLine line = new TmsPriceQuoteFeeLine(); + line.setFeeType(feeType); + line.setFeeName(feeName); + line.setScId(scId == null ? 0L : scId); + line.setAmount(nvl(amount)); + line.setCurrency(currency); + line.setRoundMode(roundMode); + line.setRemark(remark); + return line; + } + + private static String feeNameOf(String feeType) + { + if (FEE_BASIC.equals(feeType)) + { + return "\u57fa\u7840\u8fd0\u8d39"; + } + if (FEE_REG.equals(feeType)) + { + return "\u6302\u53f7\u8d39"; + } + if (FEE_SUR.equals(feeType)) + { + return "\u9644\u52a0\u8d39"; + } + return feeType; + } + private static String normalizeRoundMode(String roundMode) { if (StringUtils.isEmpty(roundMode)) @@ -297,23 +724,23 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService { if (request == null) { - throw new ServiceException("request body is required"); + throw new ServiceException(MessageUtils.message("price.quote.requestBodyRequired")); } if (request.getCustomerId() == null) { - throw new ServiceException("customerId is required"); + throw new ServiceException(MessageUtils.message("price.quote.customerIdRequired")); } if (StringUtils.isEmpty(request.getProductCode())) { - throw new ServiceException("productCode is required"); + throw new ServiceException(MessageUtils.message("price.quote.productCodeRequired")); } if (StringUtils.isEmpty(request.getCountryCode()) && StringUtils.isEmpty(request.getCountryName())) { - throw new ServiceException("countryCode or countryName is required"); + throw new ServiceException(MessageUtils.message("price.quote.countryRequired")); } if (request.getWeight() == null || request.getWeight().compareTo(BigDecimal.ZERO) <= 0) { - throw new ServiceException("weight must be > 0"); + throw new ServiceException(MessageUtils.message("price.quote.weightInvalid")); } } @@ -321,7 +748,15 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService { if (StringUtils.isEmpty(bizDate)) { - return new Date(); + try + { + return new SimpleDateFormat("yyyy-MM-dd").parse( + new SimpleDateFormat("yyyy-MM-dd").format(new Date())); + } + catch (ParseException e) + { + return new Date(); + } } try { @@ -329,7 +764,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } catch (ParseException e) { - throw new ServiceException("bizDate format must be yyyy-MM-dd"); + throw new ServiceException(MessageUtils.message("price.quote.bizDateInvalid")); } } @@ -344,6 +779,16 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService return StringUtils.isEmpty(s) ? null : s.trim(); } + private static String nvlStr(String s) + { + return s == null ? "" : s; + } + + private static BigDecimal nvl(BigDecimal v) + { + return v == null ? BigDecimal.ZERO : v; + } + private static String formatWeightLabel(BigDecimal min, BigDecimal max) { return strip(min) + "\u2264W\u2264" + strip(max); diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/XtdZoneServiceImpl.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/XtdZoneServiceImpl.java index 575c7130..b18843c2 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/XtdZoneServiceImpl.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/impl/XtdZoneServiceImpl.java @@ -4,6 +4,7 @@ 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.MessageUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.price.domain.XtdZoneCountry; import com.ruoyi.price.domain.XtdZoneDivision; @@ -78,7 +79,7 @@ public class XtdZoneServiceImpl implements IXtdZoneService { XtdZoneScheme scheme = schemeMapper.selectXtdZoneSchemeById(zsId); String name = scheme != null ? scheme.getZsName() : String.valueOf(zsId); - throw new ServiceException("分区方案【" + name + "】存在区划明细,不允许删除"); + throw new ServiceException(MessageUtils.message("price.zone.delete.hasDivision", name)); } } return schemeMapper.deleteXtdZoneSchemeByIds(zsIds); diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteFeeMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteFeeMapper.xml new file mode 100644 index 00000000..0e0fd649 --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteFeeMapper.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + insert into prc_quote_fee ( + quote_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, created_at + ) values ( + #{quoteId}, #{feeType}, #{feeName}, #{scId}, #{amount}, #{currency}, #{roundMode}, #{remark}, sysdate() + ) + + + + insert into prc_quote_fee ( + quote_id, fee_type, fee_name, sc_id, amount, currency, round_mode, remark, created_at + ) values + + (#{item.quoteId}, #{item.feeType}, #{item.feeName}, #{item.scId}, #{item.amount}, + #{item.currency}, #{item.roundMode}, #{item.remark}, sysdate()) + + + + + delete from prc_quote_fee where quote_id = #{quoteId} + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml new file mode 100644 index 00000000..419220dc --- /dev/null +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcQuoteMapper.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select quote_id, quote_key, customer_id, product_code, product_name, + country_code, country_name, weight, biz_date, tms_id, + match_source, match_message, price_set_id, price_set_code, price_set_name, + set_type, bind_priority, customer_priceset_id, product_country_id, + weight_min, weight_max, weight_label, effective_date, charge_weight, + freight_rate, base_freight_rate, discount_percent, registration_fee, + freight_amount, surcharge_amount, payable_total, currency, round_mode, + created_by, created_at, updated_by, updated_at + from prc_quote + + + + + + + + insert into prc_quote ( + quote_key, customer_id, product_code, product_name, + country_code, country_name, weight, biz_date, tms_id, + match_source, match_message, price_set_id, price_set_code, price_set_name, + set_type, bind_priority, customer_priceset_id, product_country_id, + weight_min, weight_max, weight_label, effective_date, charge_weight, + freight_rate, base_freight_rate, discount_percent, registration_fee, + freight_amount, surcharge_amount, payable_total, currency, round_mode, + created_at + ) values ( + #{quoteKey}, #{customerId}, #{productCode}, #{productName}, + #{countryCode}, #{countryName}, #{weight}, #{bizDate}, #{tmsId}, + #{matchSource}, #{matchMessage}, #{priceSetId}, #{priceSetCode}, #{priceSetName}, + #{setType}, #{bindPriority}, #{customerPricesetId}, #{productCountryId}, + #{weightMin}, #{weightMax}, #{weightLabel}, #{effectiveDate}, #{chargeWeight}, + #{freightRate}, #{baseFreightRate}, #{discountPercent}, #{registrationFee}, + #{freightAmount}, #{surchargeAmount}, #{payableTotal}, #{currency}, #{roundMode}, + sysdate() + ) + + + + update prc_quote + set surcharge_amount = #{surchargeAmount}, + payable_total = #{payableTotal}, + updated_at = sysdate() + where quote_id = #{quoteId} + + diff --git a/bms-back/sql/price/quote_fee.sql b/bms-back/sql/price/quote_fee.sql new file mode 100644 index 00000000..c95962b5 --- /dev/null +++ b/bms-back/sql/price/quote_fee.sql @@ -0,0 +1,68 @@ +-- ---------------------------- +-- BMS 下单计费试算落库 +-- charset: utf8mb4 +-- prc_quote:相同试算参数唯一,命中则直接返回 +-- prc_quote_fee:运费/挂号费各一行,附加费按具体名称各一行 +-- ---------------------------- +SET NAMES utf8mb4; + +CREATE TABLE IF NOT EXISTS prc_quote ( + quote_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '试算ID', + quote_key CHAR(32) NOT NULL COMMENT '试算指纹 MD5(客户|产品|国家|重量|计费日期)', + customer_id BIGINT(20) NOT NULL COMMENT '客户ID', + product_code VARCHAR(20) NOT NULL COMMENT '产品代码', + product_name VARCHAR(100) DEFAULT NULL COMMENT '产品名称', + country_code CHAR(2) DEFAULT NULL COMMENT '国家二字码', + country_name VARCHAR(50) DEFAULT NULL COMMENT '国家名称', + weight DECIMAL(10,3) NOT NULL COMMENT '计费重量KG', + biz_date DATE NOT NULL COMMENT '计费日期', + tms_id BIGINT(20) DEFAULT NULL COMMENT '多租户系统ID', + match_source VARCHAR(30) DEFAULT NULL COMMENT '命中来源 PACKAGE_FIXED/套餐固定价 PACKAGE_DISCOUNT/套餐折扣 PRODUCT_DEFAULT/产品默认价', + match_message VARCHAR(200) DEFAULT NULL COMMENT '命中说明', + price_set_id BIGINT(20) DEFAULT NULL COMMENT '价格套餐ID', + price_set_code VARCHAR(20) DEFAULT NULL COMMENT '套餐编码', + price_set_name VARCHAR(50) DEFAULT NULL COMMENT '套餐名称', + set_type VARCHAR(10) DEFAULT NULL COMMENT '计价类型 FIXED=固定价 DISCOUNT=折扣', + bind_priority INT(11) DEFAULT NULL COMMENT '客户套餐优先级', + customer_priceset_id BIGINT(20) DEFAULT NULL COMMENT '客户套餐绑定ID', + product_country_id BIGINT(20) DEFAULT NULL COMMENT '关联 bms_product_country.detail_id', + weight_min DECIMAL(10,4) DEFAULT NULL COMMENT '重量段下限', + weight_max DECIMAL(10,4) DEFAULT NULL COMMENT '重量段上限', + weight_label VARCHAR(50) DEFAULT NULL COMMENT '重量段展示', + effective_date DATE DEFAULT NULL COMMENT '产品明细生效日', + charge_weight DECIMAL(10,3) DEFAULT NULL COMMENT '计费重量', + freight_rate DECIMAL(10,4) DEFAULT NULL COMMENT '运费单价', + base_freight_rate DECIMAL(10,4) DEFAULT NULL COMMENT '折扣前单价', + discount_percent DECIMAL(5,2) DEFAULT NULL COMMENT '折扣百分数', + registration_fee DECIMAL(10,2) DEFAULT NULL COMMENT '挂号费', + freight_amount DECIMAL(10,2) DEFAULT NULL COMMENT '运费金额', + surcharge_amount DECIMAL(10,2) DEFAULT NULL COMMENT '附加费合计', + payable_total DECIMAL(10,2) DEFAULT NULL COMMENT '应付总额', + currency VARCHAR(10) NOT NULL DEFAULT 'RMB' COMMENT '币种', + round_mode VARCHAR(20) NOT NULL DEFAULT 'HALF_UP' COMMENT '取值方式(字典bms_round_mode)', + created_by VARCHAR(64) DEFAULT '' COMMENT '创建人', + created_at DATETIME DEFAULT NULL COMMENT '创建时间', + updated_by VARCHAR(64) DEFAULT '' COMMENT '更新人', + updated_at DATETIME DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (quote_id), + UNIQUE KEY uk_quote_key (quote_key), + INDEX idx_quote_customer (customer_id, biz_date), + INDEX idx_quote_product (product_code, country_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='计费试算主表'; + +CREATE TABLE IF NOT EXISTS prc_quote_fee ( + fee_id BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '费用明细ID', + quote_id BIGINT(20) NOT NULL COMMENT '试算ID', + fee_type VARCHAR(30) NOT NULL COMMENT '费用类型(字典bms_fee_type)', + fee_name VARCHAR(100) DEFAULT NULL COMMENT '费用名称(附加费取配置名称)', + sc_id BIGINT(20) NOT NULL DEFAULT 0 COMMENT '附加费配置ID,运费/挂号费为0', + amount DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT '金额', + currency VARCHAR(10) NOT NULL DEFAULT 'RMB' COMMENT '币种', + round_mode VARCHAR(20) DEFAULT 'HALF_UP' COMMENT '取值方式(字典bms_round_mode)', + remark VARCHAR(500) DEFAULT NULL COMMENT '备注', + created_at DATETIME DEFAULT NULL COMMENT '创建时间', + PRIMARY KEY (fee_id), + UNIQUE KEY uk_quote_fee_item (quote_id, fee_type, sc_id), + INDEX idx_quote_fee_type (fee_type), + CONSTRAINT fk_quote_fee_quote FOREIGN KEY (quote_id) REFERENCES prc_quote (quote_id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='计费试算费用明细-附加费按具体名称一行'; diff --git a/bms-back/sql/price/quote_fee_surcharge_name_alter.sql b/bms-back/sql/price/quote_fee_surcharge_name_alter.sql new file mode 100644 index 00000000..f4519ebe --- /dev/null +++ b/bms-back/sql/price/quote_fee_surcharge_name_alter.sql @@ -0,0 +1,21 @@ +-- ---------------------------- +-- prc_quote_fee:附加费按具体名称多行 +-- 原 uk_quote_fee_type(quote_id, fee_type) 被外键 fk_quote_fee_quote 依赖, +-- 必须先补 quote_id 索引再删旧唯一键 +-- ---------------------------- +SET NAMES utf8mb4; + +-- 1) 外键所需的 quote_id 索引(与旧唯一键分离) +ALTER TABLE prc_quote_fee ADD INDEX idx_quote_fee_quote_id (quote_id); + +-- 2) 附加费配置ID +ALTER TABLE prc_quote_fee ADD COLUMN sc_id BIGINT(20) NOT NULL DEFAULT 0 COMMENT '附加费配置ID,运费/挂号费为0' AFTER fee_name; + +-- 3) 新唯一:同一试算可有多条附加费 +ALTER TABLE prc_quote_fee ADD UNIQUE KEY uk_quote_fee_item (quote_id, fee_type, sc_id); + +-- 4) 现可安全删掉旧唯一键 +ALTER TABLE prc_quote_fee DROP INDEX uk_quote_fee_type; + +-- 5) 更新费用名称注释 +ALTER TABLE prc_quote_fee MODIFY COLUMN fee_name VARCHAR(100) DEFAULT NULL COMMENT '费用名称(附加费取配置名称)'; diff --git a/bms-front/src/views/price/billing/quote.vue b/bms-front/src/views/price/billing/quote.vue index 2b2050dc..a0b866d0 100644 --- a/bms-front/src/views/price/billing/quote.vue +++ b/bms-front/src/views/price/billing/quote.vue @@ -102,12 +102,43 @@ {{ roundModeLabel(result.roundMode) }} {{ formatMoney(result.freightAmount) }} {{ formatMoney(result.registrationFee) }} + {{ formatMoney(result.surchargeAmount) }} {{ formatMoney(result.payableTotal) }} + + 命中历史试算 + 实时计算并落库 + {{ result.matchMessage || '-' }} -
计算公式:运费金额 = 运费单价 × 计费重量;应付总额 = 运费金额 + 挂号费
+ + + + + + + + + + + + + + + + + + +
计算公式:运费金额 = 运费单价 × 计费重量;应付总额 = 运费金额 + 挂号费 + 附加费。相同试算参数直接返回已落库费用明细。
@@ -120,6 +151,7 @@ import { quoteBilling } from '@/api/price/billing' import { listProduct, listProductCountries } from '@/api/price/product' const { proxy } = getCurrentInstance() +const { bms_fee_type } = useDict('bms_fee_type') const formRef = ref() const loading = ref(false) const productOptions = ref([]) @@ -257,6 +289,33 @@ function roundModeLabel(mode) { return map[mode] || mode || '四舍五入' } +function feeTypeLabel(type) { + const found = (bms_fee_type.value || []).find(d => d.value === type) + return found ? found.label : '' +} + +function displayFeeName(row) { + const typeLabel = feeTypeLabel(row.feeType) + const name = String(row.feeName || row.fee_name || '').trim() + const isSurcharge = row.feeType === 'SURCHARGE' || row.feeType === '附加费' + if (isSurcharge) { + if (name && name !== typeLabel && name !== 'SURCHARGE' && name !== '附加费') { + return name + } + const fromRemark = specificNameFromRemark(row.remark) + if (fromRemark) { + return fromRemark + } + } + return name || typeLabel || row.feeType || '-' +} + +function specificNameFromRemark(remark) { + if (!remark) return '' + const first = String(remark).split(';')[0].trim() + return first.replace(/\s+-?\d+(\.\d+)?$/, '').trim() +} + function sourceTagType(source) { if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success' if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning' @@ -284,4 +343,7 @@ loadProducts() color: #909399; font-size: 12px; } +.billing-quote .fee-table { + margin-top: 12px; +}