From e265656060fa6c607b5d5ce3c4a5b0aee147289a Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Wed, 9 Sep 2026 17:06:50 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=A7=E5=93=81=E4=BB=B7?= =?UTF-8?q?=E6=A0=BC=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/price/ProductController.java | 8 +- .../main/resources/i18n/messages.properties | 33 +- .../com/ruoyi/price/domain/BmsProduct.java | 12 +- .../ruoyi/price/domain/BmsProductCountry.java | 6 +- .../price/domain/vo/TmsPriceQuoteResult.java | 2 +- .../price/mapper/BmsProductCountryMapper.java | 9 +- .../ruoyi/price/mapper/BmsProductMapper.java | 20 ++ .../mapper/PrcCustomerPriceSetMapper.java | 5 + .../price/service/IBmsProductService.java | 7 +- .../service/impl/BmsProductServiceImpl.java | 321 ++++++++++++++---- .../service/impl/PrcPriceSetServiceImpl.java | 80 ++++- .../impl/TmsPriceQuoteServiceImpl.java | 29 +- .../mapper/price/BmsProductCountryMapper.xml | 56 ++- .../mapper/price/BmsProductMapper.xml | 78 ++++- .../price/PrcCustomerPriceSetMapper.xml | 11 + .../price/customer_one_set_unique_alter.sql | 5 + .../sql/price/dict_product_effect_status.sql | 14 +- bms-back/sql/price/price_set.sql | 1 + bms-back/sql/price/product.sql | 13 +- .../price/product_code_eff_unique_alter.sql | 20 ++ .../product_country_product_id_alter.sql | 27 ++ .../sql/price/product_expire_status_alter.sql | 58 ++++ bms-front/src/api/price/product.js | 10 +- .../components/ProductCountryDrawer.vue | 18 +- .../product/components/ProductFormDialog.vue | 10 + .../product/components/SurchargeRelDialog.vue | 2 +- .../src/views/price/basic/product/index.vue | 78 +++-- bms-front/src/views/price/billing/quote.vue | 23 +- bms-front/src/views/price/set/create.vue | 7 +- bms-front/src/views/price/set/detail.vue | 2 +- 30 files changed, 771 insertions(+), 194 deletions(-) create mode 100644 bms-back/sql/price/customer_one_set_unique_alter.sql create mode 100644 bms-back/sql/price/product_code_eff_unique_alter.sql create mode 100644 bms-back/sql/price/product_country_product_id_alter.sql create mode 100644 bms-back/sql/price/product_expire_status_alter.sql diff --git a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java index 73aa33e4..6f794ae0 100644 --- a/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java +++ b/bms-back/ruoyi-admin/src/main/java/com/ruoyi/web/controller/price/ProductController.java @@ -1,5 +1,6 @@ package com.ruoyi.web.controller.price; +import java.util.Date; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import com.ruoyi.common.annotation.Log; @@ -45,9 +47,11 @@ public class ProductController extends BaseController @PreAuthorize("@ss.hasPermi('price:product:query')") @GetMapping("/{productCode}/countries") - public AjaxResult countries(@PathVariable String productCode) + public AjaxResult countries(@PathVariable String productCode, + @RequestParam(required = false) Date effectiveDate, + @RequestParam(required = false) Long productId) { - List list = productService.selectCountryListByProductCode(productCode); + List list = productService.selectCountryListByProductCode(productCode, effectiveDate, productId); return success(list); } 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 5524bef7..c4863965 100644 --- a/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/ruoyi-admin/src/main/resources/i18n/messages.properties @@ -76,6 +76,7 @@ price.set.customer.unbindAudit=审核中的套餐不允许解绑客户 price.set.customer.customerId.empty=客户ID不能为空 price.set.customer.beginDate.empty=客户绑定生效日期不能为空 price.set.customer.date.invalid=客户绑定生效日期不能晚于失效日期 +price.set.customer.alreadyBound=该客户已绑定套餐,一个客户只能绑定一个套餐 price.set.customer.priority.invalid=优先级必须大于等于 1 # 重量段 PrcWeight price.weight.range.empty=起始重量和结束重量不能为空 @@ -83,31 +84,35 @@ price.weight.range.invalid=起始重量必须小于结束重量 # 分区 XtdZone price.zone.delete.hasDivision=分区方案【{0}】存在区划明细,不允许删除 # 产品导入 BmsProduct +price.product.codeDate.exists=同一产品代码与生效日期已存在,不能覆盖历史数据 +price.product.effectiveDate.empty=生效日期不能为空 +price.product.import.effEmpty=缺少生效日期,已跳过 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.partialFail=导入完成但存在失败项:新增 {0},跳过 {1};失败明细:{2} 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.needCodeEffUnique=无法新增「同代码、不同生效日」的产品。请先执行 SQL:bms-back/sql/price/product_code_eff_unique_alter.sql(去掉 product_code 单列唯一,改为代码+生效日唯一) +price.product.import.success=导入成功:新增 {0} 个产品,跳过 {1} 个(同代码同生效日不覆盖);写入国家/重量段明细 {2} 行。 +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 +price.quote.priceNotHit=没有产品/国家/重量段绑定,暂无价格 +price.quote.freightMissing1=产品默认运费为空 +price.quote.freightMissing2=产品默认运费或挂号费为空 +price.quote.freightMissing3=运费为空 +price.quote.requestBodyRequired=请求体为空 +price.quote.customerIdRequired=客户ID为空 +price.quote.productCodeRequired=产品代码为空 +price.quote.countryRequired=国家代码或国家名称为空 +price.quote.weightInvalid=重量必须大于0 +price.quote.bizDateInvalid=业务日期格式必须为yyyy-MM-dd price.quote.fixedPackageNoDetails=套餐固定价格没有明细,使用产品默认价格 price.quote.noCustomerPackageOverride=没有客户套餐覆盖,使用产品默认运费/挂号费 -price.quote.quoteIdRequired=quoteId is required +price.quote.quoteIdRequired=报价ID为空 price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细 \ No newline at end of file diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProduct.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProduct.java index f9811633..289bbc56 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProduct.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProduct.java @@ -25,16 +25,18 @@ public class BmsProduct extends BaseEntity private String taxMode; @JsonFormat(pattern = "yyyy-MM-dd") private Date effectiveDate; - /** 0启用(按日期展示已生效/待生效) 1已失效 */ + @JsonFormat(pattern = "yyyy-MM-dd") + private Date expireDate; + /** 0 pending, 1 active, 2 expired */ + public static final String STATUS_PENDING = "0"; + public static final String STATUS_ACTIVE = "1"; + public static final String STATUS_EXPIRED = "2"; private String status; private String sourceSheet; private Long tmsId; private Integer countryCount; private Integer surchargeCount; private List countries; - /** - * 查询用:ACTIVE已生效 / PENDING待生效 / EXPIRED已失效 - * (非表字段,由生效日期+status 推导) - */ + /** query alias of status (0/1/2) */ private String effectStatus; } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProductCountry.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProductCountry.java index 46e12cd3..0bde8b5c 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProductCountry.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/domain/BmsProductCountry.java @@ -13,6 +13,8 @@ public class BmsProductCountry implements Serializable private static final long serialVersionUID = 1L; private Long detailId; + /** FK to bms_product.product_id (one country version belongs to one product row) */ + private Long productId; private String productCode; private String countryName; private String countryCode; @@ -28,12 +30,14 @@ public class BmsProductCountry implements Serializable /** null=open; closed to next import effective_date */ @JsonFormat(pattern = "yyyy-MM-dd") private Date expireDate; + /** 0 pending, 1 active, 2 expired */ + private String status; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createdAt; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date updatedAt; /** query only: true = expire_date is null */ private Boolean openOnly; - /** query only: ACTIVE已生效 / PENDING待生效 / EXPIRED已失效(按生效/失效日期与当前日期派生) */ + /** query alias of status (0/1/2) */ private String effectStatus; } 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 6acb28ab..dc00323d 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 @@ -47,7 +47,7 @@ public class TmsPriceQuoteResult implements Serializable /** 附加费合计(按产品关联命中后汇总) */ private BigDecimal surchargeAmount; private BigDecimal payableTotal; - private String currency = "RMB"; + private String currency = "CNY"; /** UP / DOWN / HALF_UP */ private String roundMode; /** 费用明细:运费/挂号费各一行,附加费按具体名称各一行 */ diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java index 61f2528d..90911c7e 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductCountryMapper.java @@ -17,9 +17,13 @@ public interface BmsProductCountryMapper public int insertBmsProductCountry(BmsProductCountry country); + public int deleteByProductId(Long productId); + public int deleteByProductCode(String productCode); - public int deleteByProductCodes(String[] productCodes); + public int deleteByProductCodeAndEffectiveDate( + @Param("productCode") String productCode, + @Param("effectiveDate") Date effectiveDate); /** Max open (expire_date is null) effective_date for product */ public Date selectMaxOpenEffectiveDate(@Param("productCode") String productCode); @@ -29,6 +33,9 @@ public interface BmsProductCountryMapper @Param("productCode") String productCode, @Param("newEffectiveDate") Date newEffectiveDate); + /** Recompute status from effective_date / expire_date vs today */ + public int refreshStatusByDates(); + /** Replace same-day version only */ public int deleteOpenByProductCodeAndEffectiveDate( @Param("productCode") String productCode, diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductMapper.java index 24988a62..1601668e 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/BmsProductMapper.java @@ -1,6 +1,8 @@ package com.ruoyi.price.mapper; +import java.util.Date; import java.util.List; +import org.apache.ibatis.annotations.Param; import com.ruoyi.price.domain.BmsProduct; /** @@ -12,8 +14,17 @@ public interface BmsProductMapper { public BmsProduct selectBmsProductById(Long productId); + /** Latest row by product_code (effective_date desc) */ public BmsProduct selectBmsProductByCode(String productCode); + /** Unique business key: product_code + effective_date */ + public BmsProduct selectBmsProductByCodeAndDate(@Param("productCode") String productCode, + @Param("effectiveDate") Date effectiveDate); + + /** Product version effective on bizDate */ + public BmsProduct selectEffectiveBmsProductByCode(@Param("productCode") String productCode, + @Param("bizDate") Date bizDate); + public List selectBmsProductList(BmsProduct product); public int insertBmsProduct(BmsProduct product); @@ -21,4 +32,13 @@ public interface BmsProductMapper public int updateBmsProduct(BmsProduct product); public int deleteBmsProductByIds(Long[] productIds); + + /** Close previous open versions: expire_date = newEffectiveDate */ + public int expireOpenByProductCode( + @Param("productCode") String productCode, + @Param("newEffectiveDate") Date newEffectiveDate, + @Param("excludeId") Long excludeId); + + /** Recompute status from effective_date / expire_date vs today */ + public int refreshStatusByDates(); } diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcCustomerPriceSetMapper.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcCustomerPriceSetMapper.java index 9afe926d..2e965b6a 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcCustomerPriceSetMapper.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/mapper/PrcCustomerPriceSetMapper.java @@ -25,6 +25,11 @@ public interface PrcCustomerPriceSetMapper public int countByPriceSetId(Long priceSetId); + /** Count binds of this customer; optional exclude current bind or current set */ + public int countByCustomerId(@Param("customerId") Long customerId, + @Param("excludeId") Long excludeId, + @Param("excludePriceSetId") Long excludePriceSetId); + public int countOverlap(@Param("customerId") Long customerId, @Param("priceSetId") Long priceSetId, @Param("beginDate") java.util.Date beginDate, diff --git a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java index afb1fca4..125d0524 100644 --- a/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java +++ b/bms-back/ruoyi-system/src/main/java/com/ruoyi/price/service/IBmsProductService.java @@ -1,5 +1,6 @@ package com.ruoyi.price.service; +import java.util.Date; import java.util.List; import org.springframework.web.multipart.MultipartFile; import com.ruoyi.price.domain.BmsProduct; @@ -24,11 +25,15 @@ public interface IBmsProductService public List selectCountryListByProductCode(String productCode); + public List selectCountryListByProductCode(String productCode, Date effectiveDate); + + public List selectCountryListByProductCode(String productCode, Date effectiveDate, Long productId); + /** * 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家×重量段(含运费/挂号费/尺寸/尾程/时效) * * @param file 上传的 xlsx/xls - * @param updateSupport 是否更新已存在产品 + * @param updateSupport unused; same code+date never overwrites, different date always inserts * @param operName 操作人 * @return 导入结果说明(可含 HTML 换行) */ 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 6a7fe7b0..fbd2c490 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 @@ -51,6 +51,10 @@ public class BmsProductServiceImpl implements IBmsProductService private static final Pattern EFFECTIVE_DATE_PATTERN = Pattern.compile( "\u751f\u6548\u65e5\u671f[\uff1a:]\\s*(\\d{4})[/.-](\\d{1,2})[/.-](\\d{1,2})"); + private static final Pattern EFFECTIVE_DATE_YMD = Pattern.compile( + "(\\d{4})[/.-](\\d{1,2})[/.-](\\d{1,2})"); + private static final Pattern EFFECTIVE_DATE_CN = Pattern.compile( + "(\\d{4})\\s*\u5e74\\s*(\\d{1,2})\\s*\u6708\\s*(\\d{1,2})"); /** Parse 0.01\u2264W\u22640.3 / 0.3 selectBmsProductList(BmsProduct product) { + productMapper.refreshStatusByDates(); + countryMapper.refreshStatusByDates(); + if (product != null) + { + product.setStatus(normalizeStatusCode(product.getStatus())); + product.setEffectStatus(normalizeStatusCode(product.getEffectStatus())); + } return productMapper.selectBmsProductList(product); } @Override public int insertBmsProduct(BmsProduct product) { - if (StringUtils.isEmpty(product.getStatus())) - { - product.setStatus("0"); - } if (StringUtils.isEmpty(product.getTaxMode())) { product.setTaxMode(resolveTaxMode(product.getProductName())); } - return productMapper.insertBmsProduct(product); + if (product.getEffectiveDate() == null) + { + throw new ServiceException(MessageUtils.message("price.product.effectiveDate.empty")); + } + applyProductStatus(product); + assertCodeDateUnique(product, null); + int rows = productMapper.insertBmsProduct(product); + productMapper.expireOpenByProductCode(product.getProductCode(), product.getEffectiveDate(), product.getProductId()); + productMapper.refreshStatusByDates(); + return rows; } @Override public int updateBmsProduct(BmsProduct product) { + if (product.getEffectiveDate() == null) + { + throw new ServiceException(MessageUtils.message("price.product.effectiveDate.empty")); + } + applyProductStatus(product); + assertCodeDateUnique(product, product.getProductId()); return productMapper.updateBmsProduct(product); } @@ -109,31 +131,81 @@ public class BmsProductServiceImpl implements IBmsProductService { return 0; } - List codes = new ArrayList<>(); + List codesNoRemain = new ArrayList<>(); for (Long id : productIds) { BmsProduct p = productMapper.selectBmsProductById(id); - if (p != null && StringUtils.isNotEmpty(p.getProductCode())) + if (p == null || StringUtils.isEmpty(p.getProductCode())) { - codes.add(p.getProductCode()); + continue; + } + if (p.getProductId() != null) + { + countryMapper.deleteByProductId(p.getProductId()); + } + countryMapper.deleteByProductCodeAndEffectiveDate(p.getProductCode(), p.getEffectiveDate()); + codesNoRemain.add(p.getProductCode()); + } + int rows = productMapper.deleteBmsProductByIds(productIds); + List orphanCodes = new ArrayList<>(); + for (String code : codesNoRemain) + { + if (productMapper.selectBmsProductByCode(code) == null) + { + orphanCodes.add(code); } } - if (!codes.isEmpty()) + if (!orphanCodes.isEmpty()) { - countryMapper.deleteByProductCodes(codes.toArray(new String[0])); - surchargeRelService.deleteRelByProductCodes(codes.toArray(new String[0])); + surchargeRelService.deleteRelByProductCodes(orphanCodes.toArray(new String[0])); } - return productMapper.deleteBmsProductByIds(productIds); + return rows; } @Override public List selectCountryListByProductCode(String productCode) { + return selectCountryListByProductCode(productCode, null); + } + + @Override + public List selectCountryListByProductCode(String productCode, Date effectiveDate) + { + return selectCountryListByProductCode(productCode, effectiveDate, null); + } + + @Override + public List selectCountryListByProductCode(String productCode, Date effectiveDate, Long productId) + { + if (productId != null) + { + BmsProductCountry byId = new BmsProductCountry(); + byId.setProductId(productId); + List list = countryMapper.selectBmsProductCountryList(byId); + if (!list.isEmpty()) + { + return list; + } + } BmsProductCountry query = new BmsProductCountry(); query.setProductCode(productCode); + query.setEffectiveDate(effectiveDate); return countryMapper.selectBmsProductCountryList(query); } + private void assertCodeDateUnique(BmsProduct product, Long excludeId) + { + if (product == null || StringUtils.isEmpty(product.getProductCode())) + { + return; + } + BmsProduct dup = productMapper.selectBmsProductByCodeAndDate(product.getProductCode(), product.getEffectiveDate()); + if (dup != null && (excludeId == null || !excludeId.equals(dup.getProductId()))) + { + throw new ServiceException(MessageUtils.message("price.product.codeDate.exists")); + } + } + @Override @Transactional(rollbackFor = Exception.class) public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception @@ -170,7 +242,6 @@ public class BmsProductServiceImpl implements IBmsProductService } int successNum = 0; - int updateNum = 0; int skipNum = 0; int countryNum = 0; Set unmatchedCountries = new HashSet<>(); @@ -185,31 +256,42 @@ public class BmsProductServiceImpl implements IBmsProductService product.setUpdateBy(operName); List countries = parseProductSheet(workbook, product, regionByNameCn, unmatchedCountries); - BmsProduct existing = productMapper.selectBmsProductByCode(product.getProductCode()); + // Sheet header date is the price version; catalog column is often stale + Date sheetEff = resolveImportEffectiveDate(countries); + Date newEff = toDateOnly(sheetEff != null ? sheetEff : product.getEffectiveDate()); + product.setEffectiveDate(newEff); + if (newEff == null) + { + failureMsg.append("
").append(product.getProductCode()).append("\uff1a") + .append(MessageUtils.message("price.product.import.effEmpty")); + continue; + } + BmsProduct existing = productMapper.selectBmsProductByCodeAndDate( + product.getProductCode(), newEff); - if (existing == null) + if (existing != null) { - product.setStatus("0"); - productMapper.insertBmsProduct(product); - countryNum += saveCountriesVersioned(product.getProductCode(), countries); - successNum++; - } - else if (updateSupport) - { - product.setProductId(existing.getProductId()); - productMapper.updateBmsProduct(product); - countryNum += saveCountriesVersioned(product.getProductCode(), countries); - updateNum++; - } - else if (!countries.isEmpty()) - { - // Keep product master; version country rows by effective_date - countryNum += saveCountriesVersioned(product.getProductCode(), countries); - updateNum++; + skipNum++; } else { - skipNum++; + applyProductStatus(product); + try + { + productMapper.insertBmsProduct(product); + } + catch (Exception e) + { + if (isProductCodeUniqueViolation(e)) + { + throw new ServiceException(MessageUtils.message("price.product.import.needCodeEffUnique")); + } + throw e; + } + productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId()); + productMapper.refreshStatusByDates(); + countryNum += saveCountriesVersioned(product, countries); + successNum++; } } catch (Exception e) @@ -221,9 +303,9 @@ public class BmsProductServiceImpl implements IBmsProductService if (failureMsg.length() > 0) { throw new ServiceException(MessageUtils.message("price.product.import.partialFail", - successNum, updateNum, skipNum, failureMsg.toString())); + successNum, skipNum, failureMsg.toString())); } - String msg = MessageUtils.message("price.product.import.success", successNum, updateNum, skipNum, countryNum) + String msg = MessageUtils.message("price.product.import.success", successNum, skipNum, countryNum) + "
" + MessageUtils.message("price.product.import.success.detail1") + "
" + MessageUtils.message("price.product.import.success.detail2"); if (!unmatchedCountries.isEmpty()) @@ -236,16 +318,18 @@ public class BmsProductServiceImpl implements IBmsProductService /** * Versioned country save: - * - newEff > last open eff: close old (expire_date = newEff), insert new (keep history) - * - newEff equals last open: replace that open version only + * - newEff > last open: close old (expire_date = newEff), insert new (keep history) + * - newEff equals last open: skip (never overwrite) * - newEff < last open: reject */ - private int saveCountriesVersioned(String productCode, List countries) + private int saveCountriesVersioned(BmsProduct product, List countries) { - if (countries == null || countries.isEmpty()) + if (product == null || countries == null || countries.isEmpty()) { return 0; } + String productCode = product.getProductCode(); + Long productId = product.getProductId(); Date newEff = resolveImportEffectiveDate(countries); Date lastOpenEff = countryMapper.selectMaxOpenEffectiveDate(productCode); @@ -259,21 +343,24 @@ public class BmsProductServiceImpl implements IBmsProductService { // Previous open version expire_date = this effective_date; keep old rows countryMapper.expireOpenByProductCode(productCode, newEff); + countryMapper.refreshStatusByDates(); } else if (lastOpenEff != null) { - // Same effective day (or both null): replace current open version only - countryMapper.deleteOpenByProductCodeAndEffectiveDate(productCode, newEff); + // Same effective day: never overwrite existing country rows + return 0; } for (BmsProductCountry c : countries) { + c.setProductId(productId); c.setProductCode(productCode); c.setExpireDate(null); if (c.getEffectiveDate() == null) { c.setEffectiveDate(newEff); } + c.setStatus(resolveStoredStatus(c.getEffectiveDate(), c.getExpireDate())); try { countryMapper.insertBmsProductCountry(c); @@ -321,9 +408,93 @@ public class BmsProductServiceImpl implements IBmsProductService return new SimpleDateFormat("yyyy-MM-dd").format(d); } + private Date toDateOnly(Date d) + { + if (d == null) + { + return null; + } + try + { + return new SimpleDateFormat("yyyy-MM-dd").parse(formatDate(d)); + } + catch (Exception e) + { + return d; + } + } + + /** 0 pending / 1 active / 2 expired from dates vs today */ + private String resolveStoredStatus(Date effectiveDate, Date expireDate) + { + Date today = toDateOnly(new Date()); + Date expire = toDateOnly(expireDate); + Date eff = toDateOnly(effectiveDate); + if (expire != null && today != null && !expire.after(today)) + { + return BmsProduct.STATUS_EXPIRED; + } + if (eff == null || (today != null && eff.after(today))) + { + return BmsProduct.STATUS_PENDING; + } + return BmsProduct.STATUS_ACTIVE; + } + + private void applyProductStatus(BmsProduct product) + { + if (product == null) + { + return; + } + product.setStatus(resolveStoredStatus(product.getEffectiveDate(), product.getExpireDate())); + } + + /** Map ACTIVE/PENDING/EXPIRED or 0/1/2 to stored status code. */ + private String normalizeStatusCode(String status) + { + if (status == null || status.isEmpty()) + { + return status; + } + if ("ACTIVE".equalsIgnoreCase(status) || BmsProduct.STATUS_ACTIVE.equals(status)) + { + return BmsProduct.STATUS_ACTIVE; + } + if ("PENDING".equalsIgnoreCase(status) || BmsProduct.STATUS_PENDING.equals(status)) + { + return BmsProduct.STATUS_PENDING; + } + if ("EXPIRED".equalsIgnoreCase(status) || BmsProduct.STATUS_EXPIRED.equals(status)) + { + return BmsProduct.STATUS_EXPIRED; + } + return status; + } + + private boolean isProductCodeUniqueViolation(Throwable e) + { + while (e != null) + { + String m = e.getMessage(); + if (m != null && m.contains("uk_product_code") && !m.contains("uk_product_code_eff")) + { + return true; + } + if (m != null && m.contains("Duplicate") && m.contains("product_code") && !m.contains("effective")) + { + return true; + } + e = e.getCause(); + } + return false; + } + private void saveCountries(String productCode, List countries) { - saveCountriesVersioned(productCode, countries); + BmsProduct p = new BmsProduct(); + p.setProductCode(productCode); + saveCountriesVersioned(p, countries); } private List parseCatalog(Sheet sheet) @@ -590,35 +761,67 @@ public class BmsProductServiceImpl implements IBmsProductService private Date parseSheetEffectiveDate(Sheet sheet) { - Row row0 = sheet.getRow(0); - if (row0 == null) + int lastRow = Math.min(sheet.getLastRowNum(), 4); + for (int r = 0; r <= lastRow; r++) { - return null; - } - for (int c = 0; c < 12; c++) - { - String text = cellString(row0.getCell(c)); - if (StringUtils.isEmpty(text)) + Row row = sheet.getRow(r); + if (row == null) { continue; } - Matcher m = EFFECTIVE_DATE_PATTERN.matcher(text); - if (m.find()) + int lastCell = Math.max(row.getLastCellNum(), 12); + lastCell = Math.min(lastCell, 20); + for (int c = 0; c < lastCell; c++) { - try + Date parsed = parseEffectiveDateText(cellString(row.getCell(c))); + if (parsed != null) { - String d = m.group(1) + "-" + pad2(m.group(2)) + "-" + pad2(m.group(3)); - return new SimpleDateFormat("yyyy-MM-dd").parse(d); - } - catch (Exception ignored) - { - return null; + return parsed; } } } return null; } + private Date parseEffectiveDateText(String text) + { + if (StringUtils.isEmpty(text)) + { + return null; + } + Matcher labeled = EFFECTIVE_DATE_PATTERN.matcher(text); + if (labeled.find()) + { + return parseYmd(labeled.group(1), labeled.group(2), labeled.group(3)); + } + Matcher cn = EFFECTIVE_DATE_CN.matcher(text); + if (cn.find()) + { + return parseYmd(cn.group(1), cn.group(2), cn.group(3)); + } + if (text.contains("\u751f\u6548")) + { + Matcher ymd = EFFECTIVE_DATE_YMD.matcher(text); + if (ymd.find()) + { + return parseYmd(ymd.group(1), ymd.group(2), ymd.group(3)); + } + } + return null; + } + + private Date parseYmd(String y, String m, String d) + { + try + { + return new SimpleDateFormat("yyyy-MM-dd").parse(y + "-" + pad2(m) + "-" + pad2(d)); + } + catch (Exception ignored) + { + return null; + } + } + private static String pad2(String n) { return n.length() == 1 ? "0" + n : n; 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 5cefea3a..9a3b0d20 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 @@ -406,24 +406,37 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } List customers = filterCustomers(set.getCustomers()); - int overlap = 0; + Set seenCustomers = new HashSet(); + int bound = 0; + boolean dupInSet = false; for (PrcCustomerPriceSet c : customers) { - overlap += customerMapper.countOverlap(c.getCustomerId(), set.getPriceSetId(), - c.getPrBegindate() != null ? c.getPrBegindate() : set.getEffectiveDate(), - c.getPrEnddate() != null ? c.getPrEnddate() : set.getExpireDate()); + if (c.getCustomerId() == null) + { + continue; + } + if (!seenCustomers.add(c.getCustomerId())) + { + dupInSet = true; + continue; + } + bound += customerMapper.countByCustomerId(c.getCustomerId(), c.getCustomerPricesetId(), + set.getPriceSetId()); } - if (overlap > 0) + if (dupInSet || bound > 0) { - items.add(new PrcPriceSetCheckItem("与同客户现有套餐是否冲突", "WARN", "存在 " + overlap + " 条与已生效套餐日期重叠的客户绑定,可共存但需确认优先级")); + items.add(new PrcPriceSetCheckItem("\u4e00\u4e2a\u5ba2\u6237\u53ea\u80fd\u7ed1\u5b9a\u4e00\u4e2a\u5957\u9910", "FAIL", + MessageUtils.message("price.set.customer.alreadyBound"))); } else if (customers.isEmpty()) { - items.add(new PrcPriceSetCheckItem("与同客户现有套餐是否冲突", "INFO", "暂未绑定客户,可后续补充")); + items.add(new PrcPriceSetCheckItem("\u4e00\u4e2a\u5ba2\u6237\u53ea\u80fd\u7ed1\u5b9a\u4e00\u4e2a\u5957\u9910", "INFO", + "\u6682\u672a\u7ed1\u5b9a\u5ba2\u6237\uff0c\u53ef\u540e\u7eed\u8865\u5145")); } else { - items.add(new PrcPriceSetCheckItem("与同客户现有套餐是否冲突", "PASS", "未发现与已生效套餐的日期重叠")); + items.add(new PrcPriceSetCheckItem("\u4e00\u4e2a\u5ba2\u6237\u53ea\u80fd\u7ed1\u5b9a\u4e00\u4e2a\u5957\u9910", "PASS", + "\u5ba2\u6237\u672a\u7ed1\u5b9a\u5176\u4ed6\u5957\u9910")); } if (TYPE_FIXED.equals(setType) && !details.isEmpty()) @@ -504,7 +517,18 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { customer.setBindPriority(1); } - return customerMapper.insertPrcCustomerPriceSet(customer); + try + { + return customerMapper.insertPrcCustomerPriceSet(customer); + } + catch (Exception e) + { + if (isCustomerUniqueViolation(e)) + { + throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound")); + } + throw e; + } } @Override @@ -557,8 +581,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService if (set.getCustomers() != null) { customerMapper.deletePrcCustomerPriceSetByPriceSetId(set.getPriceSetId()); + Set seenCustomers = new HashSet(); for (PrcCustomerPriceSet c : filterCustomers(set.getCustomers())) { + if (c.getCustomerId() != null && !seenCustomers.add(c.getCustomerId())) + { + throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound")); + } validateCustomer(c, set); c.setCustomerPricesetId(null); c.setPriceSetId(set.getPriceSetId()); @@ -567,7 +596,18 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { c.setBindPriority(1); } - customerMapper.insertPrcCustomerPriceSet(c); + try + { + customerMapper.insertPrcCustomerPriceSet(c); + } + catch (Exception e) + { + if (isCustomerUniqueViolation(e)) + { + throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound")); + } + throw e; + } } } } @@ -739,6 +779,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { throw new ServiceException(MessageUtils.message("price.set.customer.priority.invalid")); } + int exists = customerMapper.countByCustomerId(customer.getCustomerId(), + customer.getCustomerPricesetId(), null); + if (exists > 0) + { + throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound")); + } } private void assertBasicEditable(PrcPriceSet db) @@ -786,6 +832,20 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService return db; } + private boolean isCustomerUniqueViolation(Throwable e) + { + while (e != null) + { + String m = e.getMessage(); + if (m != null && (m.contains("uk_customer_one_set") || m.contains("Duplicate"))) + { + return m.contains("uk_customer_one_set") || m.contains("customer_id"); + } + e = e.getCause(); + } + return false; + } + private void fillExpired(PrcPriceSet set) { Date today = DateUtils.parseDate(DateUtils.getDate()); 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 b22f5143..0a7338f6 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 @@ -151,7 +151,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService throw new ServiceException(MessageUtils.message("price.quote.priceNotHit")); } - String productName = resolveProductName(productCode); + String productName = resolveProductName(productCode, bizDate); List binds = customerPriceSetMapper.selectEffectiveByCustomer( request.getCustomerId(), bizDate); @@ -329,7 +329,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService r.setFreightAmount(freightAmount); r.setSurchargeAmount(BigDecimal.ZERO); r.setPayableTotal(payable); - r.setCurrency("RMB"); + r.setCurrency("CNY"); r.setRoundMode(mode); r.setFromCache(false); return r; @@ -340,7 +340,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService BigDecimal freightAmount = nvl(result.getFreightAmount()); BigDecimal registrationFee = nvl(result.getRegistrationFee()); RoundingMode rm = resolveRoundingMode(result.getRoundMode()); - String currency = StringUtils.isEmpty(result.getCurrency()) ? "RMB" : result.getCurrency(); + String currency = normalizeCurrency(result.getCurrency()); List rels = loadMatchedSurcharges(result); List lines = new ArrayList(); @@ -365,7 +365,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } 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 itemCcy = normalizeCurrency(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())); } @@ -604,7 +604,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService r.setFreightAmount(quote.getFreightAmount()); r.setSurchargeAmount(quote.getSurchargeAmount()); r.setPayableTotal(quote.getPayableTotal()); - r.setCurrency(quote.getCurrency()); + r.setCurrency(normalizeCurrency(quote.getCurrency())); r.setRoundMode(quote.getRoundMode()); List lines = new ArrayList(); if (fees != null) @@ -682,12 +682,21 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService line.setFeeName(feeName); line.setScId(scId == null ? 0L : scId); line.setAmount(nvl(amount)); - line.setCurrency(currency); + line.setCurrency(normalizeCurrency(currency)); line.setRoundMode(roundMode); line.setRemark(remark); return line; } + private static String normalizeCurrency(String currency) + { + if (StringUtils.isEmpty(currency) || "RMB".equalsIgnoreCase(currency)) + { + return "CNY"; + } + return currency.trim().toUpperCase(); + } + private static String feeNameOf(String feeType) { if (FEE_BASIC.equals(feeType)) @@ -791,9 +800,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService } } - private String resolveProductName(String productCode) + private String resolveProductName(String productCode, Date bizDate) { - BmsProduct p = productMapper.selectBmsProductByCode(productCode); + BmsProduct p = productMapper.selectEffectiveBmsProductByCode(productCode, bizDate); + if (p == null) + { + p = productMapper.selectBmsProductByCode(productCode); + } return p == null ? null : p.getProductName(); } diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml index 1e9b6125..6d1c959f 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductCountryMapper.xml @@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -18,31 +19,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + - + - - - case - when expire_date is not null and expire_date <= curdate() then 'EXPIRED' - when effective_date is null or effective_date > curdate() then 'PENDING' - else 'ACTIVE' - end - - - select detail_id, product_code, country_name, country_code, weight_min, weight_max, + select detail_id, product_id, product_code, country_name, country_code, weight_min, weight_max, freight_price, registration_fee, size_limit, last_mile, lead_time, - effective_date, expire_date, created_at, updated_at, - as effect_status + effective_date, expire_date, status, created_at, updated_at from bms_product_country select max(effective_date) from bms_product_country where product_code = #{productCode} @@ -109,6 +120,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and (effective_date is null or effective_date < #{newEffectiveDate}) + + update bms_product_country + set status = case + when expire_date is not null and expire_date <= curdate() then '2' + when effective_date is null or effective_date > curdate() then '0' + else '1' + end + + delete from bms_product_country where product_code = #{productCode} diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductMapper.xml index 788e7ead..bebb6a78 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/BmsProductMapper.xml @@ -14,6 +14,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -28,9 +29,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select p.product_id, p.product_code, p.product_name, p.service_level, p.cargo_attr, - p.dest_region, p.update_note, p.tax_mode, p.effective_date, p.status, + p.dest_region, p.update_note, p.tax_mode, p.effective_date, p.expire_date, p.status, p.source_sheet, p.remark, p.tms_id, p.created_by, p.created_at, p.updated_by, p.updated_at, - (select count(distinct c.country_name) from bms_product_country c where c.product_code = p.product_code) as country_count, + (select count(distinct c.country_name) from bms_product_country c + where c.product_id = p.product_id + or (c.product_id is null and c.product_code = p.product_code + and ( + (p.effective_date is null and c.effective_date is null) + or c.effective_date = p.effective_date + )) + ) as country_count, (select count(*) from prc_surcharge_rel r where r.product_id = p.product_id) as surcharge_count from bms_product p @@ -43,6 +51,31 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + @@ -97,6 +117,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update_note, tax_mode, effective_date, + expire_date, status, source_sheet, remark, @@ -112,6 +133,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{updateNote}, #{taxMode}, #{effectiveDate}, + #{expireDate}, #{status}, #{sourceSheet}, #{remark}, @@ -132,6 +154,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update_note = #{updateNote}, tax_mode = #{taxMode}, effective_date = #{effectiveDate}, + expire_date = #{expireDate}, status = #{status}, source_sheet = #{sourceSheet}, remark = #{remark}, @@ -149,4 +172,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + update bms_product + set expire_date = #{newEffectiveDate}, + updated_at = sysdate() + where product_code = #{productCode} + and expire_date is null + and (effective_date is null or effective_date < #{newEffectiveDate}) + + and product_id <> #{excludeId} + + + + + update bms_product + set status = case + when expire_date is not null and expire_date <= curdate() then '2' + when effective_date is null or effective_date > curdate() then '0' + else '1' + end + + diff --git a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcCustomerPriceSetMapper.xml b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcCustomerPriceSetMapper.xml index c96844d5..cfb9e56d 100644 --- a/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcCustomerPriceSetMapper.xml +++ b/bms-back/ruoyi-system/src/main/resources/mapper/price/PrcCustomerPriceSetMapper.xml @@ -51,6 +51,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select count(1) from prc_customer_price_set where price_set_id = #{priceSetId} + +