修改产品价格显示

This commit is contained in:
wuweihua
2026-09-09 17:06:50 +08:00
parent fddd8f0586
commit e265656060
30 changed files with 771 additions and 194 deletions
@@ -1,5 +1,6 @@
package com.ruoyi.web.controller.price; package com.ruoyi.web.controller.price;
import java.util.Date;
import java.util.List; import java.util.List;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; 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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; 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.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.Log;
@@ -45,9 +47,11 @@ public class ProductController extends BaseController
@PreAuthorize("@ss.hasPermi('price:product:query')") @PreAuthorize("@ss.hasPermi('price:product:query')")
@GetMapping("/{productCode}/countries") @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<BmsProductCountry> list = productService.selectCountryListByProductCode(productCode); List<BmsProductCountry> list = productService.selectCountryListByProductCode(productCode, effectiveDate, productId);
return success(list); return success(list);
} }
@@ -76,6 +76,7 @@ price.set.customer.unbindAudit=审核中的套餐不允许解绑客户
price.set.customer.customerId.empty=客户ID不能为空 price.set.customer.customerId.empty=客户ID不能为空
price.set.customer.beginDate.empty=客户绑定生效日期不能为空 price.set.customer.beginDate.empty=客户绑定生效日期不能为空
price.set.customer.date.invalid=客户绑定生效日期不能晚于失效日期 price.set.customer.date.invalid=客户绑定生效日期不能晚于失效日期
price.set.customer.alreadyBound=该客户已绑定套餐,一个客户只能绑定一个套餐
price.set.customer.priority.invalid=优先级必须大于等于 1 price.set.customer.priority.invalid=优先级必须大于等于 1
# 重量段 PrcWeight # 重量段 PrcWeight
price.weight.range.empty=起始重量和结束重量不能为空 price.weight.range.empty=起始重量和结束重量不能为空
@@ -83,31 +84,35 @@ price.weight.range.invalid=起始重量必须小于结束重量
# 分区 XtdZone # 分区 XtdZone
price.zone.delete.hasDivision=分区方案【{0}】存在区划明细,不允许删除 price.zone.delete.hasDivision=分区方案【{0}】存在区划明细,不允许删除
# 产品导入 BmsProduct # 产品导入 BmsProduct
price.product.codeDate.exists=同一产品代码与生效日期已存在,不能覆盖历史数据
price.product.effectiveDate.empty=生效日期不能为空
price.product.import.effEmpty=缺少生效日期,已跳过
price.product.import.fileEmpty=上传文件不能为空 price.product.import.fileEmpty=上传文件不能为空
price.product.import.fileType=仅支持 xls / xlsx 文件 price.product.import.fileType=仅支持 xls / xlsx 文件
price.product.import.sheetNotFound=未找到「产品目录」sheet,请上传威狮B2C小包价表格格式文件 price.product.import.sheetNotFound=未找到「产品目录」sheet,请上传威狮B2C小包价表格格式文件
price.product.import.noData=产品目录无有效数据行 price.product.import.noData=产品目录无有效数据行
price.product.import.regionEmpty=bms_region 无可用国家数据,请先同步/维护国家表后再导入 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.effTooEarly=生效日期 {0} 早于当前开放版 {1},请使用更晚的生效日期导入
price.product.import.needFeeAlter=请先执行 SQL:bms-back/sql/price/product_country_fee_alter.sql(缺少运费/挂号费字段) 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.needExpireAlter=请先执行 SQL:bms-back/sql/price/product_country_expire_alter.sql(缺少失效日期/版本唯一键)
price.product.import.success=导入成功:新增 {0} 个产品,更新 {1} 个,跳过 {2} 个;写入国家/重量段明细 {3} 行。 price.product.import.needCodeEffUnique=无法新增「同代码、不同生效日」的产品。请先执行 SQL:bms-back/sql/price/product_code_eff_unique_alter.sql(去掉 product_code 单列唯一,改为代码+生效日唯一)
price.product.import.success.detail1=国家明细:若新生效日晚于上次,保留历史版且上次失效日=本次生效日;同日则替换当前开放版。 price.product.import.success=导入成功:新增 {0} 个产品,跳过 {1} 个(同代码同生效日不覆盖);写入国家/重量段明细 {2} 行。
price.product.import.success.detail1=产品主数据按「产品代码+生效日期」新增,不覆盖已有记录。国家明细:新生效日晚于上次则保留历史,上次失效日=本次生效日。
price.product.import.success.detail2=范围:产品目录 + 重量段 + 运费/挂号费 + 尺寸/尾程/时效;国家码来自 bms_region。 price.product.import.success.detail2=范围:产品目录 + 重量段 + 运费/挂号费 + 尺寸/尾程/时效;国家码来自 bms_region。
price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0} price.product.import.success.unmatched=未匹配国家表(country_code 为空):{0}
# 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约) # 报价试算 TmsPriceQuote(对外 TMS API 错误码,保持英文契约)
price.quote.priceNotHit=PRICE_NOT_HIT: no bms_product_country band for product/country/weight price.quote.priceNotHit=没有产品/国家/重量段绑定,暂无价格
price.quote.freightMissing1=FREIGHT_VALUE_MISSING: product default freight rate is null price.quote.freightMissing1=产品默认运费为空
price.quote.freightMissing2=FREIGHT_VALUE_MISSING: product default freight or registration is null price.quote.freightMissing2=产品默认运费或挂号费为空
price.quote.freightMissing3=FREIGHT_VALUE_MISSING: freight rate is null price.quote.freightMissing3=运费为空
price.quote.requestBodyRequired=request body is required price.quote.requestBodyRequired=请求体为空
price.quote.customerIdRequired=customerId is required price.quote.customerIdRequired=客户ID为空
price.quote.productCodeRequired=productCode is required price.quote.productCodeRequired=产品代码为空
price.quote.countryRequired=countryCode or countryName is required price.quote.countryRequired=国家代码或国家名称为空
price.quote.weightInvalid=weight must be > 0 price.quote.weightInvalid=重量必须大于0
price.quote.bizDateInvalid=bizDate format must be yyyy-MM-dd price.quote.bizDateInvalid=业务日期格式必须为yyyy-MM-dd
price.quote.fixedPackageNoDetails=套餐固定价格没有明细,使用产品默认价格 price.quote.fixedPackageNoDetails=套餐固定价格没有明细,使用产品默认价格
price.quote.noCustomerPackageOverride=没有客户套餐覆盖,使用产品默认运费/挂号费 price.quote.noCustomerPackageOverride=没有客户套餐覆盖,使用产品默认运费/挂号费
price.quote.quoteIdRequired=quoteId is required price.quote.quoteIdRequired=报价ID为空
price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细 price.quote.hitCustomerFixedPackageDetail=命中客户套餐固定价格明细
@@ -25,16 +25,18 @@ public class BmsProduct extends BaseEntity
private String taxMode; private String taxMode;
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
private Date effectiveDate; 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 status;
private String sourceSheet; private String sourceSheet;
private Long tmsId; private Long tmsId;
private Integer countryCount; private Integer countryCount;
private Integer surchargeCount; private Integer surchargeCount;
private List<BmsProductCountry> countries; private List<BmsProductCountry> countries;
/** /** query alias of status (0/1/2) */
* 查询用:ACTIVE已生效 / PENDING待生效 / EXPIRED已失效
* (非表字段,由生效日期+status 推导)
*/
private String effectStatus; private String effectStatus;
} }
@@ -13,6 +13,8 @@ public class BmsProductCountry implements Serializable
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private Long detailId; 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 productCode;
private String countryName; private String countryName;
private String countryCode; private String countryCode;
@@ -28,12 +30,14 @@ public class BmsProductCountry implements Serializable
/** null=open; closed to next import effective_date */ /** null=open; closed to next import effective_date */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd")
private Date expireDate; private Date expireDate;
/** 0 pending, 1 active, 2 expired */
private String status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createdAt; private Date createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updatedAt; private Date updatedAt;
/** query only: true = expire_date is null */ /** query only: true = expire_date is null */
private Boolean openOnly; private Boolean openOnly;
/** query only: ACTIVE已生效 / PENDING待生效 / EXPIRED已失效(按生效/失效日期与当前日期派生) */ /** query alias of status (0/1/2) */
private String effectStatus; private String effectStatus;
} }
@@ -47,7 +47,7 @@ public class TmsPriceQuoteResult implements Serializable
/** 附加费合计(按产品关联命中后汇总) */ /** 附加费合计(按产品关联命中后汇总) */
private BigDecimal surchargeAmount; private BigDecimal surchargeAmount;
private BigDecimal payableTotal; private BigDecimal payableTotal;
private String currency = "RMB"; private String currency = "CNY";
/** UP / DOWN / HALF_UP */ /** UP / DOWN / HALF_UP */
private String roundMode; private String roundMode;
/** 费用明细:运费/挂号费各一行,附加费按具体名称各一行 */ /** 费用明细:运费/挂号费各一行,附加费按具体名称各一行 */
@@ -17,9 +17,13 @@ public interface BmsProductCountryMapper
public int insertBmsProductCountry(BmsProductCountry country); public int insertBmsProductCountry(BmsProductCountry country);
public int deleteByProductId(Long productId);
public int deleteByProductCode(String productCode); 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 */ /** Max open (expire_date is null) effective_date for product */
public Date selectMaxOpenEffectiveDate(@Param("productCode") String productCode); public Date selectMaxOpenEffectiveDate(@Param("productCode") String productCode);
@@ -29,6 +33,9 @@ public interface BmsProductCountryMapper
@Param("productCode") String productCode, @Param("productCode") String productCode,
@Param("newEffectiveDate") Date newEffectiveDate); @Param("newEffectiveDate") Date newEffectiveDate);
/** Recompute status from effective_date / expire_date vs today */
public int refreshStatusByDates();
/** Replace same-day version only */ /** Replace same-day version only */
public int deleteOpenByProductCodeAndEffectiveDate( public int deleteOpenByProductCodeAndEffectiveDate(
@Param("productCode") String productCode, @Param("productCode") String productCode,
@@ -1,6 +1,8 @@
package com.ruoyi.price.mapper; package com.ruoyi.price.mapper;
import java.util.Date;
import java.util.List; import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.price.domain.BmsProduct; import com.ruoyi.price.domain.BmsProduct;
/** /**
@@ -12,8 +14,17 @@ public interface BmsProductMapper
{ {
public BmsProduct selectBmsProductById(Long productId); public BmsProduct selectBmsProductById(Long productId);
/** Latest row by product_code (effective_date desc) */
public BmsProduct selectBmsProductByCode(String productCode); 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<BmsProduct> selectBmsProductList(BmsProduct product); public List<BmsProduct> selectBmsProductList(BmsProduct product);
public int insertBmsProduct(BmsProduct product); public int insertBmsProduct(BmsProduct product);
@@ -21,4 +32,13 @@ public interface BmsProductMapper
public int updateBmsProduct(BmsProduct product); public int updateBmsProduct(BmsProduct product);
public int deleteBmsProductByIds(Long[] productIds); 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();
} }
@@ -25,6 +25,11 @@ public interface PrcCustomerPriceSetMapper
public int countByPriceSetId(Long priceSetId); 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, public int countOverlap(@Param("customerId") Long customerId,
@Param("priceSetId") Long priceSetId, @Param("priceSetId") Long priceSetId,
@Param("beginDate") java.util.Date beginDate, @Param("beginDate") java.util.Date beginDate,
@@ -1,5 +1,6 @@
package com.ruoyi.price.service; package com.ruoyi.price.service;
import java.util.Date;
import java.util.List; import java.util.List;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.price.domain.BmsProduct; import com.ruoyi.price.domain.BmsProduct;
@@ -24,11 +25,15 @@ public interface IBmsProductService
public List<BmsProductCountry> selectCountryListByProductCode(String productCode); public List<BmsProductCountry> selectCountryListByProductCode(String productCode);
public List<BmsProductCountry> selectCountryListByProductCode(String productCode, Date effectiveDate);
public List<BmsProductCountry> selectCountryListByProductCode(String productCode, Date effectiveDate, Long productId);
/** /**
* 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家×重量段(含运费/挂号费/尺寸/尾程/时效) * 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家×重量段(含运费/挂号费/尺寸/尾程/时效)
* *
* @param file 上传的 xlsx/xls * @param file 上传的 xlsx/xls
* @param updateSupport 是否更新已存在产品 * @param updateSupport unused; same code+date never overwrites, different date always inserts
* @param operName 操作人 * @param operName 操作人
* @return 导入结果说明(可含 HTML 换行) * @return 导入结果说明(可含 HTML 换行)
*/ */
@@ -51,6 +51,10 @@ public class BmsProductServiceImpl implements IBmsProductService
private static final Pattern EFFECTIVE_DATE_PATTERN = Pattern.compile( private static final Pattern EFFECTIVE_DATE_PATTERN = Pattern.compile(
"\u751f\u6548\u65e5\u671f[\uff1a:]\\s*(\\d{4})[/.-](\\d{1,2})[/.-](\\d{1,2})"); "\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<W\u22641 */ /** Parse 0.01\u2264W\u22640.3 / 0.3<W\u22641 */
private static final Pattern WEIGHT_RANGE_PATTERN = Pattern.compile( private static final Pattern WEIGHT_RANGE_PATTERN = Pattern.compile(
@@ -78,26 +82,44 @@ public class BmsProductServiceImpl implements IBmsProductService
@Override @Override
public List<BmsProduct> selectBmsProductList(BmsProduct product) public List<BmsProduct> selectBmsProductList(BmsProduct product)
{ {
productMapper.refreshStatusByDates();
countryMapper.refreshStatusByDates();
if (product != null)
{
product.setStatus(normalizeStatusCode(product.getStatus()));
product.setEffectStatus(normalizeStatusCode(product.getEffectStatus()));
}
return productMapper.selectBmsProductList(product); return productMapper.selectBmsProductList(product);
} }
@Override @Override
public int insertBmsProduct(BmsProduct product) public int insertBmsProduct(BmsProduct product)
{ {
if (StringUtils.isEmpty(product.getStatus()))
{
product.setStatus("0");
}
if (StringUtils.isEmpty(product.getTaxMode())) if (StringUtils.isEmpty(product.getTaxMode()))
{ {
product.setTaxMode(resolveTaxMode(product.getProductName())); 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 @Override
public int updateBmsProduct(BmsProduct product) 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); return productMapper.updateBmsProduct(product);
} }
@@ -109,31 +131,81 @@ public class BmsProductServiceImpl implements IBmsProductService
{ {
return 0; return 0;
} }
List<String> codes = new ArrayList<>(); List<String> codesNoRemain = new ArrayList<>();
for (Long id : productIds) for (Long id : productIds)
{ {
BmsProduct p = productMapper.selectBmsProductById(id); 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)
if (!codes.isEmpty())
{ {
countryMapper.deleteByProductCodes(codes.toArray(new String[0])); countryMapper.deleteByProductId(p.getProductId());
surchargeRelService.deleteRelByProductCodes(codes.toArray(new String[0]));
} }
return productMapper.deleteBmsProductByIds(productIds); countryMapper.deleteByProductCodeAndEffectiveDate(p.getProductCode(), p.getEffectiveDate());
codesNoRemain.add(p.getProductCode());
}
int rows = productMapper.deleteBmsProductByIds(productIds);
List<String> orphanCodes = new ArrayList<>();
for (String code : codesNoRemain)
{
if (productMapper.selectBmsProductByCode(code) == null)
{
orphanCodes.add(code);
}
}
if (!orphanCodes.isEmpty())
{
surchargeRelService.deleteRelByProductCodes(orphanCodes.toArray(new String[0]));
}
return rows;
} }
@Override @Override
public List<BmsProductCountry> selectCountryListByProductCode(String productCode) public List<BmsProductCountry> selectCountryListByProductCode(String productCode)
{ {
return selectCountryListByProductCode(productCode, null);
}
@Override
public List<BmsProductCountry> selectCountryListByProductCode(String productCode, Date effectiveDate)
{
return selectCountryListByProductCode(productCode, effectiveDate, null);
}
@Override
public List<BmsProductCountry> selectCountryListByProductCode(String productCode, Date effectiveDate, Long productId)
{
if (productId != null)
{
BmsProductCountry byId = new BmsProductCountry();
byId.setProductId(productId);
List<BmsProductCountry> list = countryMapper.selectBmsProductCountryList(byId);
if (!list.isEmpty())
{
return list;
}
}
BmsProductCountry query = new BmsProductCountry(); BmsProductCountry query = new BmsProductCountry();
query.setProductCode(productCode); query.setProductCode(productCode);
query.setEffectiveDate(effectiveDate);
return countryMapper.selectBmsProductCountryList(query); 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 @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception
@@ -170,7 +242,6 @@ public class BmsProductServiceImpl implements IBmsProductService
} }
int successNum = 0; int successNum = 0;
int updateNum = 0;
int skipNum = 0; int skipNum = 0;
int countryNum = 0; int countryNum = 0;
Set<String> unmatchedCountries = new HashSet<>(); Set<String> unmatchedCountries = new HashSet<>();
@@ -185,31 +256,42 @@ public class BmsProductServiceImpl implements IBmsProductService
product.setUpdateBy(operName); product.setUpdateBy(operName);
List<BmsProductCountry> countries = parseProductSheet(workbook, product, regionByNameCn, unmatchedCountries); List<BmsProductCountry> 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("<br/>").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"); skipNum++;
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++;
} }
else 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) catch (Exception e)
@@ -221,9 +303,9 @@ public class BmsProductServiceImpl implements IBmsProductService
if (failureMsg.length() > 0) if (failureMsg.length() > 0)
{ {
throw new ServiceException(MessageUtils.message("price.product.import.partialFail", 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)
+ "<br/>" + MessageUtils.message("price.product.import.success.detail1") + "<br/>" + MessageUtils.message("price.product.import.success.detail1")
+ "<br/>" + MessageUtils.message("price.product.import.success.detail2"); + "<br/>" + MessageUtils.message("price.product.import.success.detail2");
if (!unmatchedCountries.isEmpty()) if (!unmatchedCountries.isEmpty())
@@ -236,16 +318,18 @@ public class BmsProductServiceImpl implements IBmsProductService
/** /**
* Versioned country save: * Versioned country save:
* - newEff &gt; last open eff: close old (expire_date = newEff), insert new (keep history) * - newEff &gt; last open: close old (expire_date = newEff), insert new (keep history)
* - newEff equals last open: replace that open version only * - newEff equals last open: skip (never overwrite)
* - newEff &lt; last open: reject * - newEff &lt; last open: reject
*/ */
private int saveCountriesVersioned(String productCode, List<BmsProductCountry> countries) private int saveCountriesVersioned(BmsProduct product, List<BmsProductCountry> countries)
{ {
if (countries == null || countries.isEmpty()) if (product == null || countries == null || countries.isEmpty())
{ {
return 0; return 0;
} }
String productCode = product.getProductCode();
Long productId = product.getProductId();
Date newEff = resolveImportEffectiveDate(countries); Date newEff = resolveImportEffectiveDate(countries);
Date lastOpenEff = countryMapper.selectMaxOpenEffectiveDate(productCode); 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 // Previous open version expire_date = this effective_date; keep old rows
countryMapper.expireOpenByProductCode(productCode, newEff); countryMapper.expireOpenByProductCode(productCode, newEff);
countryMapper.refreshStatusByDates();
} }
else if (lastOpenEff != null) else if (lastOpenEff != null)
{ {
// Same effective day (or both null): replace current open version only // Same effective day: never overwrite existing country rows
countryMapper.deleteOpenByProductCodeAndEffectiveDate(productCode, newEff); return 0;
} }
for (BmsProductCountry c : countries) for (BmsProductCountry c : countries)
{ {
c.setProductId(productId);
c.setProductCode(productCode); c.setProductCode(productCode);
c.setExpireDate(null); c.setExpireDate(null);
if (c.getEffectiveDate() == null) if (c.getEffectiveDate() == null)
{ {
c.setEffectiveDate(newEff); c.setEffectiveDate(newEff);
} }
c.setStatus(resolveStoredStatus(c.getEffectiveDate(), c.getExpireDate()));
try try
{ {
countryMapper.insertBmsProductCountry(c); countryMapper.insertBmsProductCountry(c);
@@ -321,9 +408,93 @@ public class BmsProductServiceImpl implements IBmsProductService
return new SimpleDateFormat("yyyy-MM-dd").format(d); 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<BmsProductCountry> countries) private void saveCountries(String productCode, List<BmsProductCountry> countries)
{ {
saveCountriesVersioned(productCode, countries); BmsProduct p = new BmsProduct();
p.setProductCode(productCode);
saveCountriesVersioned(p, countries);
} }
private List<BmsProduct> parseCatalog(Sheet sheet) private List<BmsProduct> parseCatalog(Sheet sheet)
@@ -590,34 +761,66 @@ public class BmsProductServiceImpl implements IBmsProductService
private Date parseSheetEffectiveDate(Sheet sheet) private Date parseSheetEffectiveDate(Sheet sheet)
{ {
Row row0 = sheet.getRow(0); int lastRow = Math.min(sheet.getLastRowNum(), 4);
if (row0 == null) for (int r = 0; r <= lastRow; r++)
{ {
return null; Row row = sheet.getRow(r);
} if (row == null)
for (int c = 0; c < 12; c++)
{
String text = cellString(row0.getCell(c));
if (StringUtils.isEmpty(text))
{ {
continue; continue;
} }
Matcher m = EFFECTIVE_DATE_PATTERN.matcher(text); int lastCell = Math.max(row.getLastCellNum(), 12);
if (m.find()) lastCell = Math.min(lastCell, 20);
for (int c = 0; c < lastCell; c++)
{
Date parsed = parseEffectiveDateText(cellString(row.getCell(c)));
if (parsed != 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 try
{ {
String d = m.group(1) + "-" + pad2(m.group(2)) + "-" + pad2(m.group(3)); return new SimpleDateFormat("yyyy-MM-dd").parse(y + "-" + pad2(m) + "-" + pad2(d));
return new SimpleDateFormat("yyyy-MM-dd").parse(d);
} }
catch (Exception ignored) catch (Exception ignored)
{ {
return null; return null;
} }
} }
}
return null;
}
private static String pad2(String n) private static String pad2(String n)
{ {
@@ -406,24 +406,37 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
} }
List<PrcCustomerPriceSet> customers = filterCustomers(set.getCustomers()); List<PrcCustomerPriceSet> customers = filterCustomers(set.getCustomers());
int overlap = 0; Set<Long> seenCustomers = new HashSet<Long>();
int bound = 0;
boolean dupInSet = false;
for (PrcCustomerPriceSet c : customers) for (PrcCustomerPriceSet c : customers)
{ {
overlap += customerMapper.countOverlap(c.getCustomerId(), set.getPriceSetId(), if (c.getCustomerId() == null)
c.getPrBegindate() != null ? c.getPrBegindate() : set.getEffectiveDate(),
c.getPrEnddate() != null ? c.getPrEnddate() : set.getExpireDate());
}
if (overlap > 0)
{ {
items.add(new PrcPriceSetCheckItem("与同客户现有套餐是否冲突", "WARN", "存在 " + overlap + " 条与已生效套餐日期重叠的客户绑定,可共存但需确认优先级")); continue;
}
if (!seenCustomers.add(c.getCustomerId()))
{
dupInSet = true;
continue;
}
bound += customerMapper.countByCustomerId(c.getCustomerId(), c.getCustomerPricesetId(),
set.getPriceSetId());
}
if (dupInSet || bound > 0)
{
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()) 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 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()) if (TYPE_FIXED.equals(setType) && !details.isEmpty())
@@ -504,8 +517,19 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{ {
customer.setBindPriority(1); customer.setBindPriority(1);
} }
try
{
return customerMapper.insertPrcCustomerPriceSet(customer); return customerMapper.insertPrcCustomerPriceSet(customer);
} }
catch (Exception e)
{
if (isCustomerUniqueViolation(e))
{
throw new ServiceException(MessageUtils.message("price.set.customer.alreadyBound"));
}
throw e;
}
}
@Override @Override
public int updateCustomer(PrcCustomerPriceSet customer) public int updateCustomer(PrcCustomerPriceSet customer)
@@ -557,8 +581,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
if (set.getCustomers() != null) if (set.getCustomers() != null)
{ {
customerMapper.deletePrcCustomerPriceSetByPriceSetId(set.getPriceSetId()); customerMapper.deletePrcCustomerPriceSetByPriceSetId(set.getPriceSetId());
Set<Long> seenCustomers = new HashSet<Long>();
for (PrcCustomerPriceSet c : filterCustomers(set.getCustomers())) 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); validateCustomer(c, set);
c.setCustomerPricesetId(null); c.setCustomerPricesetId(null);
c.setPriceSetId(set.getPriceSetId()); c.setPriceSetId(set.getPriceSetId());
@@ -567,8 +596,19 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{ {
c.setBindPriority(1); c.setBindPriority(1);
} }
try
{
customerMapper.insertPrcCustomerPriceSet(c); 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")); 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) private void assertBasicEditable(PrcPriceSet db)
@@ -786,6 +832,20 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return db; 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) private void fillExpired(PrcPriceSet set)
{ {
Date today = DateUtils.parseDate(DateUtils.getDate()); Date today = DateUtils.parseDate(DateUtils.getDate());
@@ -151,7 +151,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
throw new ServiceException(MessageUtils.message("price.quote.priceNotHit")); throw new ServiceException(MessageUtils.message("price.quote.priceNotHit"));
} }
String productName = resolveProductName(productCode); String productName = resolveProductName(productCode, bizDate);
List<PrcCustomerPriceSet> binds = customerPriceSetMapper.selectEffectiveByCustomer( List<PrcCustomerPriceSet> binds = customerPriceSetMapper.selectEffectiveByCustomer(
request.getCustomerId(), bizDate); request.getCustomerId(), bizDate);
@@ -329,7 +329,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
r.setFreightAmount(freightAmount); r.setFreightAmount(freightAmount);
r.setSurchargeAmount(BigDecimal.ZERO); r.setSurchargeAmount(BigDecimal.ZERO);
r.setPayableTotal(payable); r.setPayableTotal(payable);
r.setCurrency("RMB"); r.setCurrency("CNY");
r.setRoundMode(mode); r.setRoundMode(mode);
r.setFromCache(false); r.setFromCache(false);
return r; return r;
@@ -340,7 +340,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
BigDecimal freightAmount = nvl(result.getFreightAmount()); BigDecimal freightAmount = nvl(result.getFreightAmount());
BigDecimal registrationFee = nvl(result.getRegistrationFee()); BigDecimal registrationFee = nvl(result.getRegistrationFee());
RoundingMode rm = resolveRoundingMode(result.getRoundMode()); RoundingMode rm = resolveRoundingMode(result.getRoundMode());
String currency = StringUtils.isEmpty(result.getCurrency()) ? "RMB" : result.getCurrency(); String currency = normalizeCurrency(result.getCurrency());
List<PrcSurchargeRel> rels = loadMatchedSurcharges(result); List<PrcSurchargeRel> rels = loadMatchedSurcharges(result);
List<TmsPriceQuoteFeeLine> lines = new ArrayList<TmsPriceQuoteFeeLine>(); List<TmsPriceQuoteFeeLine> lines = new ArrayList<TmsPriceQuoteFeeLine>();
@@ -365,7 +365,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
} }
String feeType = StringUtils.isEmpty(rel.getFeeType()) ? FEE_SUR : rel.getFeeType(); String feeType = StringUtils.isEmpty(rel.getFeeType()) ? FEE_SUR : rel.getFeeType();
String feeName = StringUtils.isEmpty(rel.getScName()) ? feeNameOf(FEE_SUR) : rel.getScName(); 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(); String itemRound = StringUtils.isEmpty(rel.getRoundMode()) ? result.getRoundMode() : rel.getRoundMode();
surchargeLines.put(scId, feeLine(feeType, feeName, scId, itemAmount, itemCcy, itemRound, rel.getScNote())); 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.setFreightAmount(quote.getFreightAmount());
r.setSurchargeAmount(quote.getSurchargeAmount()); r.setSurchargeAmount(quote.getSurchargeAmount());
r.setPayableTotal(quote.getPayableTotal()); r.setPayableTotal(quote.getPayableTotal());
r.setCurrency(quote.getCurrency()); r.setCurrency(normalizeCurrency(quote.getCurrency()));
r.setRoundMode(quote.getRoundMode()); r.setRoundMode(quote.getRoundMode());
List<TmsPriceQuoteFeeLine> lines = new ArrayList<TmsPriceQuoteFeeLine>(); List<TmsPriceQuoteFeeLine> lines = new ArrayList<TmsPriceQuoteFeeLine>();
if (fees != null) if (fees != null)
@@ -682,12 +682,21 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
line.setFeeName(feeName); line.setFeeName(feeName);
line.setScId(scId == null ? 0L : scId); line.setScId(scId == null ? 0L : scId);
line.setAmount(nvl(amount)); line.setAmount(nvl(amount));
line.setCurrency(currency); line.setCurrency(normalizeCurrency(currency));
line.setRoundMode(roundMode); line.setRoundMode(roundMode);
line.setRemark(remark); line.setRemark(remark);
return line; 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) private static String feeNameOf(String feeType)
{ {
if (FEE_BASIC.equals(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(); return p == null ? null : p.getProductName();
} }
@@ -6,6 +6,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<resultMap type="BmsProductCountry" id="BmsProductCountryResult"> <resultMap type="BmsProductCountry" id="BmsProductCountryResult">
<id property="detailId" column="detail_id" /> <id property="detailId" column="detail_id" />
<result property="productId" column="product_id" />
<result property="productCode" column="product_code" /> <result property="productCode" column="product_code" />
<result property="countryName" column="country_name" /> <result property="countryName" column="country_name" />
<result property="countryCode" column="country_code" /> <result property="countryCode" column="country_code" />
@@ -18,31 +19,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="leadTime" column="lead_time" /> <result property="leadTime" column="lead_time" />
<result property="effectiveDate" column="effective_date" /> <result property="effectiveDate" column="effective_date" />
<result property="expireDate" column="expire_date" /> <result property="expireDate" column="expire_date" />
<result property="status" column="status" />
<result property="createdAt" column="created_at" /> <result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" /> <result property="updatedAt" column="updated_at" />
<result property="effectStatus" column="effect_status" /> <result property="effectStatus" column="status" />
</resultMap> </resultMap>
<!-- 派生状态:已失效(过期) > 待生效(未到生效日) > 已生效 -->
<sql id="effectStatusCase">
case
when expire_date is not null and expire_date &lt;= curdate() then 'EXPIRED'
when effective_date is null or effective_date &gt; curdate() then 'PENDING'
else 'ACTIVE'
end
</sql>
<sql id="selectVo"> <sql id="selectVo">
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, freight_price, registration_fee, size_limit, last_mile, lead_time,
effective_date, expire_date, created_at, updated_at, effective_date, expire_date, status, created_at, updated_at
<include refid="effectStatusCase"/> as effect_status
from bms_product_country from bms_product_country
</sql> </sql>
<select id="selectBmsProductCountryList" parameterType="BmsProductCountry" resultMap="BmsProductCountryResult"> <select id="selectBmsProductCountryList" parameterType="BmsProductCountry" resultMap="BmsProductCountryResult">
<include refid="selectVo"/> <include refid="selectVo"/>
<where> <where>
<if test="productId != null">
AND product_id = #{productId}
</if>
<if test="productCode != null and productCode != ''"> <if test="productCode != null and productCode != ''">
AND product_code = #{productCode} AND product_code = #{productCode}
</if> </if>
@@ -64,8 +59,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="openOnly != null and openOnly"> <if test="openOnly != null and openOnly">
AND expire_date is null AND expire_date is null
</if> </if>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="effectStatus != null and effectStatus != ''"> <if test="effectStatus != null and effectStatus != ''">
AND (<include refid="effectStatusCase"/>) = #{effectStatus} AND status = #{effectStatus}
</if> </if>
</where> </where>
order by effective_date desc, country_name asc, weight_min asc, detail_id asc order by effective_date desc, country_name asc, weight_min asc, detail_id asc
@@ -73,16 +71,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<insert id="insertBmsProductCountry" parameterType="BmsProductCountry" useGeneratedKeys="true" keyProperty="detailId"> <insert id="insertBmsProductCountry" parameterType="BmsProductCountry" useGeneratedKeys="true" keyProperty="detailId">
insert into bms_product_country ( insert into bms_product_country (
product_code, country_name, country_code, weight_min, weight_max, product_id, product_code, country_name, country_code, weight_min, weight_max,
freight_price, registration_fee, size_limit, last_mile, lead_time, freight_price, registration_fee, size_limit, last_mile, lead_time,
effective_date, expire_date, created_at effective_date, expire_date, status, created_at
) values ( ) values (
#{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax}, #{productId}, #{productCode}, #{countryName}, #{countryCode}, #{weightMin}, #{weightMax},
#{freightPrice}, #{registrationFee}, #{sizeLimit}, #{lastMile}, #{leadTime}, #{freightPrice}, #{registrationFee}, #{sizeLimit}, #{lastMile}, #{leadTime},
#{effectiveDate}, #{expireDate}, sysdate() #{effectiveDate}, #{expireDate}, IFNULL(#{status}, '0'), sysdate()
) )
</insert> </insert>
<delete id="deleteByProductId" parameterType="Long">
delete from bms_product_country where product_id = #{productId}
</delete>
<delete id="deleteByProductCode" parameterType="String"> <delete id="deleteByProductCode" parameterType="String">
delete from bms_product_country where product_code = #{productCode} delete from bms_product_country where product_code = #{productCode}
</delete> </delete>
@@ -94,6 +96,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach> </foreach>
</delete> </delete>
<delete id="deleteByProductCodeAndEffectiveDate">
delete from bms_product_country
where product_code = #{productCode}
and (
(#{effectiveDate} is null and effective_date is null)
or effective_date = #{effectiveDate}
)
</delete>
<select id="selectMaxOpenEffectiveDate" resultType="java.util.Date"> <select id="selectMaxOpenEffectiveDate" resultType="java.util.Date">
select max(effective_date) from bms_product_country select max(effective_date) from bms_product_country
where product_code = #{productCode} where product_code = #{productCode}
@@ -109,6 +120,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
and (effective_date is null or effective_date &lt; #{newEffectiveDate}) and (effective_date is null or effective_date &lt; #{newEffectiveDate})
</update> </update>
<update id="refreshStatusByDates">
update bms_product_country
set status = case
when expire_date is not null and expire_date &lt;= curdate() then '2'
when effective_date is null or effective_date &gt; curdate() then '0'
else '1'
end
</update>
<delete id="deleteOpenByProductCodeAndEffectiveDate"> <delete id="deleteOpenByProductCodeAndEffectiveDate">
delete from bms_product_country delete from bms_product_country
where product_code = #{productCode} where product_code = #{productCode}
@@ -14,6 +14,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateNote" column="update_note" /> <result property="updateNote" column="update_note" />
<result property="taxMode" column="tax_mode" /> <result property="taxMode" column="tax_mode" />
<result property="effectiveDate" column="effective_date" /> <result property="effectiveDate" column="effective_date" />
<result property="expireDate" column="expire_date" />
<result property="status" column="status" /> <result property="status" column="status" />
<result property="sourceSheet" column="source_sheet" /> <result property="sourceSheet" column="source_sheet" />
<result property="remark" column="remark" /> <result property="remark" column="remark" />
@@ -28,9 +29,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<sql id="selectBmsProductVo"> <sql id="selectBmsProductVo">
select p.product_id, p.product_code, p.product_name, p.service_level, p.cargo_attr, 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, 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 (select count(*) from prc_surcharge_rel r where r.product_id = p.product_id) as surcharge_count
from bms_product p from bms_product p
</sql> </sql>
@@ -43,6 +51,31 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectBmsProductByCode" parameterType="String" resultMap="BmsProductResult"> <select id="selectBmsProductByCode" parameterType="String" resultMap="BmsProductResult">
<include refid="selectBmsProductVo"/> <include refid="selectBmsProductVo"/>
where p.product_code = #{productCode} where p.product_code = #{productCode}
order by p.effective_date desc, p.product_id desc
limit 1
</select>
<select id="selectBmsProductByCodeAndDate" resultMap="BmsProductResult">
<include refid="selectBmsProductVo"/>
where p.product_code = #{productCode}
<choose>
<when test="effectiveDate != null">
AND p.effective_date = #{effectiveDate}
</when>
<otherwise>
AND p.effective_date is null
</otherwise>
</choose>
limit 1
</select>
<select id="selectEffectiveBmsProductByCode" resultMap="BmsProductResult">
<include refid="selectBmsProductVo"/>
where p.product_code = #{productCode}
and (p.effective_date is null or #{bizDate} is null or p.effective_date &lt;= #{bizDate})
and (p.expire_date is null or #{bizDate} is null or p.expire_date &gt; #{bizDate})
order by p.effective_date desc, p.product_id desc
limit 1
</select> </select>
<select id="selectBmsProductList" parameterType="BmsProduct" resultMap="BmsProductResult"> <select id="selectBmsProductList" parameterType="BmsProduct" resultMap="BmsProductResult">
@@ -61,20 +94,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
AND p.status = #{status} AND p.status = #{status}
</if> </if>
<if test="effectStatus != null and effectStatus != ''"> <if test="effectStatus != null and effectStatus != ''">
<choose> AND p.status = #{effectStatus}
<when test="effectStatus == 'EXPIRED'">
AND p.status = '1'
</when>
<when test="effectStatus == 'PENDING'">
AND p.status = '0'
AND (p.effective_date is null or p.effective_date &gt; curdate())
</when>
<when test="effectStatus == 'ACTIVE'">
AND p.status = '0'
AND p.effective_date is not null
AND p.effective_date &lt;= curdate()
</when>
</choose>
</if> </if>
<if test="cargoAttr != null and cargoAttr != ''"> <if test="cargoAttr != null and cargoAttr != ''">
AND p.cargo_attr like concat('%', #{cargoAttr}, '%') AND p.cargo_attr like concat('%', #{cargoAttr}, '%')
@@ -84,7 +104,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
OR p.product_name like concat('%', #{params.keyword}, '%')) OR p.product_name like concat('%', #{params.keyword}, '%'))
</if> </if>
</where> </where>
order by p.product_id desc order by p.product_code asc, p.effective_date desc, p.product_id desc
</select> </select>
<insert id="insertBmsProduct" parameterType="BmsProduct" useGeneratedKeys="true" keyProperty="productId"> <insert id="insertBmsProduct" parameterType="BmsProduct" useGeneratedKeys="true" keyProperty="productId">
@@ -97,6 +117,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateNote != null">update_note,</if> <if test="updateNote != null">update_note,</if>
<if test="taxMode != null and taxMode != ''">tax_mode,</if> <if test="taxMode != null and taxMode != ''">tax_mode,</if>
<if test="effectiveDate != null">effective_date,</if> <if test="effectiveDate != null">effective_date,</if>
<if test="expireDate != null">expire_date,</if>
<if test="status != null and status != ''">status,</if> <if test="status != null and status != ''">status,</if>
<if test="sourceSheet != null">source_sheet,</if> <if test="sourceSheet != null">source_sheet,</if>
<if test="remark != null">remark,</if> <if test="remark != null">remark,</if>
@@ -112,6 +133,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateNote != null">#{updateNote},</if> <if test="updateNote != null">#{updateNote},</if>
<if test="taxMode != null and taxMode != ''">#{taxMode},</if> <if test="taxMode != null and taxMode != ''">#{taxMode},</if>
<if test="effectiveDate != null">#{effectiveDate},</if> <if test="effectiveDate != null">#{effectiveDate},</if>
<if test="expireDate != null">#{expireDate},</if>
<if test="status != null and status != ''">#{status},</if> <if test="status != null and status != ''">#{status},</if>
<if test="sourceSheet != null">#{sourceSheet},</if> <if test="sourceSheet != null">#{sourceSheet},</if>
<if test="remark != null">#{remark},</if> <if test="remark != null">#{remark},</if>
@@ -132,6 +154,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="updateNote != null">update_note = #{updateNote},</if> <if test="updateNote != null">update_note = #{updateNote},</if>
<if test="taxMode != null and taxMode != ''">tax_mode = #{taxMode},</if> <if test="taxMode != null and taxMode != ''">tax_mode = #{taxMode},</if>
<if test="effectiveDate != null">effective_date = #{effectiveDate},</if> <if test="effectiveDate != null">effective_date = #{effectiveDate},</if>
expire_date = #{expireDate},
<if test="status != null and status != ''">status = #{status},</if> <if test="status != null and status != ''">status = #{status},</if>
<if test="sourceSheet != null">source_sheet = #{sourceSheet},</if> <if test="sourceSheet != null">source_sheet = #{sourceSheet},</if>
<if test="remark != null">remark = #{remark},</if> <if test="remark != null">remark = #{remark},</if>
@@ -149,4 +172,25 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</foreach> </foreach>
</delete> </delete>
<update id="expireOpenByProductCode">
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 &lt; #{newEffectiveDate})
<if test="excludeId != null">
and product_id &lt;&gt; #{excludeId}
</if>
</update>
<update id="refreshStatusByDates">
update bms_product
set status = case
when expire_date is not null and expire_date &lt;= curdate() then '2'
when effective_date is null or effective_date &gt; curdate() then '0'
else '1'
end
</update>
</mapper> </mapper>
@@ -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} select count(1) from prc_customer_price_set where price_set_id = #{priceSetId}
</select> </select>
<select id="countByCustomerId" resultType="int">
select count(1) from prc_customer_price_set
where customer_id = #{customerId}
<if test="excludeId != null">
AND customer_priceset_id &lt;&gt; #{excludeId}
</if>
<if test="excludePriceSetId != null">
AND price_set_id &lt;&gt; #{excludePriceSetId}
</if>
</select>
<select id="countOverlap" resultType="int"> <select id="countOverlap" resultType="int">
select count(1) select count(1)
from prc_customer_price_set c from prc_customer_price_set c
@@ -0,0 +1,5 @@
-- prc_customer_price_set:一个客户只能绑定一个套餐
SET NAMES utf8mb4;
ALTER TABLE prc_customer_price_set
ADD UNIQUE KEY uk_customer_one_set (customer_id);
@@ -1,6 +1,6 @@
-- BMS product effect status dict -- BMS product effect status dict
-- dict_type: bms_product_effect_status -- dict_type: bms_product_effect_status
-- values: ACTIVE / PENDING / EXPIRED -- values: 1 active / 0 pending / 2 expired
SET NAMES utf8mb4; SET NAMES utf8mb4;
INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark) INSERT INTO sys_dict_type (dict_name, dict_type, status, create_by, create_time, remark)
@@ -8,13 +8,13 @@ SELECT N'产品生效状态', 'bms_product_effect_status', '0', 'admin', sysdate
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bms_product_effect_status'); FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_type WHERE dict_type = 'bms_product_effect_status');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 1, N'已生效', 'ACTIVE', 'bms_product_effect_status', '', 'success', 'N', '0', 'admin', sysdate(), N'启用且生效日期<=今天' SELECT 1, N'已生效', '1', 'bms_product_effect_status', '', 'success', 'Y', '0', 'admin', sysdate(), N'1 已生效'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = 'ACTIVE'); FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = '1');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 2, N'待生效', 'PENDING', 'bms_product_effect_status', '', 'warning', 'N', '0', 'admin', sysdate(), N'启用且生效日期为空或>今天' SELECT 2, N'待生效', '0', 'bms_product_effect_status', '', 'warning', 'N', '0', 'admin', sysdate(), N'0 待生效'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = 'PENDING'); FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = '0');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark) INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 3, N'已失效', 'EXPIRED', 'bms_product_effect_status', '', 'info', 'N', '0', 'admin', sysdate(), N'手动标记失效 status=1' SELECT 3, N'已失效', '2', 'bms_product_effect_status', '', 'info', 'N', '0', 'admin', sysdate(), N'2 已失效'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = 'EXPIRED'); FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = '2');
+1
View File
@@ -72,6 +72,7 @@ CREATE TABLE IF NOT EXISTS prc_customer_price_set (
updated_by VARCHAR(64) DEFAULT '' COMMENT '更新人', updated_by VARCHAR(64) DEFAULT '' COMMENT '更新人',
updated_at DATETIME DEFAULT NULL COMMENT '更新时间', updated_at DATETIME DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (customer_priceset_id), PRIMARY KEY (customer_priceset_id),
UNIQUE KEY uk_customer_one_set (customer_id),
INDEX idx_customer_set (customer_id, pr_begindate, pr_enddate), INDEX idx_customer_set (customer_id, pr_begindate, pr_enddate),
INDEX idx_cps_set (price_set_id), INDEX idx_cps_set (price_set_id),
CONSTRAINT fk_customer_price_set FOREIGN KEY (price_set_id) REFERENCES prc_price_set (price_set_id) ON DELETE RESTRICT CONSTRAINT fk_customer_price_set FOREIGN KEY (price_set_id) REFERENCES prc_price_set (price_set_id) ON DELETE RESTRICT
+9 -4
View File
@@ -13,7 +13,8 @@ CREATE TABLE IF NOT EXISTS bms_product (
update_note varchar(500) DEFAULT NULL COMMENT '更新内容说明', update_note varchar(500) DEFAULT NULL COMMENT '更新内容说明',
tax_mode varchar(20) NOT NULL DEFAULT 'TAX_INCLUDED' COMMENT 'TAX_INCLUDED包税 TAX_EXCLUDED不包税', tax_mode varchar(20) NOT NULL DEFAULT 'TAX_INCLUDED' COMMENT 'TAX_INCLUDED包税 TAX_EXCLUDED不包税',
effective_date date DEFAULT NULL COMMENT '目录生效日期', effective_date date DEFAULT NULL COMMENT '目录生效日期',
status char(1) NOT NULL DEFAULT '0' COMMENT '0正常 1停用', expire_date date DEFAULT NULL COMMENT '产品失效日期(空=长期;下一版生效日=本版失效日)',
status char(1) NOT NULL DEFAULT '0' COMMENT '0待生效 1已生效 2已失效',
source_sheet varchar(100) DEFAULT NULL COMMENT '来源Excel sheet名', source_sheet varchar(100) DEFAULT NULL COMMENT '来源Excel sheet名',
remark varchar(500) DEFAULT NULL, remark varchar(500) DEFAULT NULL,
tms_id int DEFAULT NULL COMMENT '多租户ID', tms_id int DEFAULT NULL COMMENT '多租户ID',
@@ -22,12 +23,14 @@ CREATE TABLE IF NOT EXISTS bms_product (
updated_by varchar(50) NOT NULL DEFAULT '', updated_by varchar(50) NOT NULL DEFAULT '',
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (product_id), PRIMARY KEY (product_id),
UNIQUE KEY uk_product_code (product_code), UNIQUE KEY uk_product_code_eff (product_code, effective_date),
KEY idx_product_code (product_code),
KEY idx_tax_status (tax_mode, status) KEY idx_tax_status (tax_mode, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BMS产品主表-来自产品目录'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BMS产品主表-来自产品目录';
CREATE TABLE IF NOT EXISTS bms_product_country ( CREATE TABLE IF NOT EXISTS bms_product_country (
detail_id bigint NOT NULL AUTO_INCREMENT COMMENT '明细ID', detail_id bigint NOT NULL AUTO_INCREMENT COMMENT '明细ID',
product_id bigint DEFAULT NULL COMMENT '产品ID(bms_product.product_id)',
product_code varchar(20) NOT NULL COMMENT '产品代码', product_code varchar(20) NOT NULL COMMENT '产品代码',
country_name varchar(50) NOT NULL COMMENT '国家中文名', country_name varchar(50) NOT NULL COMMENT '国家中文名',
country_code char(2) DEFAULT NULL COMMENT 'ISO2,可空待补齐', country_code char(2) DEFAULT NULL COMMENT 'ISO2,可空待补齐',
@@ -40,14 +43,15 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
lead_time varchar(100) DEFAULT NULL COMMENT '参考时效', lead_time varchar(100) DEFAULT NULL COMMENT '参考时效',
effective_date date DEFAULT NULL COMMENT '产品价表生效日期', effective_date date DEFAULT NULL COMMENT '产品价表生效日期',
expire_date date DEFAULT NULL COMMENT '失效日期(空=长期;新导入生效日=上一版失效日)', expire_date date DEFAULT NULL COMMENT '失效日期(空=长期;新导入生效日=上一版失效日)',
status char(1) NOT NULL DEFAULT '0' COMMENT '0待生效 1已生效 2已失效',
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (detail_id), PRIMARY KEY (detail_id),
UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date), UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date),
KEY idx_product_country_pid (product_id),
KEY idx_country_code (country_code), KEY idx_country_code (country_code),
KEY idx_weight_match (product_code, country_code, weight_min, weight_max), KEY idx_weight_match (product_code, country_code, weight_min, weight_max),
KEY idx_product_eff (product_code, effective_date, expire_date), KEY idx_product_eff (product_code, effective_date, expire_date)
CONSTRAINT fk_product_country_code FOREIGN KEY (product_code) REFERENCES bms_product (product_code) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家明细-重量段+运费挂号费+尺寸/尾程/时效'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产品国家明细-重量段+运费挂号费+尺寸/尾程/时效';
-- 已建表升级(幂等:列已存在时跳过报错可忽略) -- 已建表升级(幂等:列已存在时跳过报错可忽略)
@@ -59,6 +63,7 @@ CREATE TABLE IF NOT EXISTS bms_product_country (
-- ALTER TABLE bms_product_country ADD COLUMN expire_date date DEFAULT NULL COMMENT '失效日期' AFTER effective_date; -- ALTER TABLE bms_product_country ADD COLUMN expire_date date DEFAULT NULL COMMENT '失效日期' AFTER effective_date;
-- ALTER TABLE bms_product_country DROP INDEX uk_product_country_weight; -- ALTER TABLE bms_product_country DROP INDEX uk_product_country_weight;
-- ALTER TABLE bms_product_country ADD UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date); -- ALTER TABLE bms_product_country ADD UNIQUE KEY uk_product_country_weight_eff (product_code, country_name, weight_min, weight_max, effective_date);
-- product_id: run product_country_product_id_alter.sql
-- 菜单:挂在价格基础 2100 下(order_num=0 置顶) -- 菜单:挂在价格基础 2100 下(order_num=0 置顶)
INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
@@ -0,0 +1,20 @@
-- ----------------------------
-- bms_product:产品代码不单独唯一
-- 唯一键改为 (product_code, effective_date)
-- 导入:相同代码+相同生效日不覆盖;不同生效日新增一行
-- ----------------------------
SET NAMES utf8mb4;
-- 1) 国家明细外键依赖 uk_product_code,必须先删
ALTER TABLE bms_product_country DROP FOREIGN KEY fk_product_country_code;
-- 2) 删除产品代码单列唯一
ALTER TABLE bms_product DROP INDEX uk_product_code;
-- 3) 按代码+生效日期唯一
ALTER TABLE bms_product
ADD UNIQUE KEY uk_product_code_eff (product_code, effective_date);
-- 4) 保留产品代码查询索引
ALTER TABLE bms_product
ADD KEY idx_product_code (product_code);
@@ -0,0 +1,27 @@
-- bms_product_country:增加 bms_product.product_id,明细绑定到具体产品版本行
SET NAMES utf8mb4;
-- 1) 新增 product_id 列
ALTER TABLE bms_product_country
ADD COLUMN product_id bigint DEFAULT NULL COMMENT '产品ID(bms_product.product_id,绑定具体产品版本行)' AFTER detail_id;
-- 2) 按 product_code + effective_date 回填既有明细
UPDATE bms_product_country c
INNER JOIN bms_product p
ON p.product_code = c.product_code
AND (
(p.effective_date IS NULL AND c.effective_date IS NULL)
OR p.effective_date = c.effective_date
)
SET c.product_id = p.product_id
WHERE c.product_id IS NULL;
-- 3) 索引
ALTER TABLE bms_product_country
ADD KEY idx_product_country_pid (product_id);
-- 4) 外键(删除产品时级联删除明细;若已存在或有孤儿数据报错可忽略)
ALTER TABLE bms_product_country
ADD CONSTRAINT fk_product_country_pid
FOREIGN KEY (product_id) REFERENCES bms_product (product_id)
ON DELETE CASCADE;
@@ -0,0 +1,58 @@
-- bms_product 增加失效日期;产品/国家明细 status:0待生效 1已生效 2已失效
SET NAMES utf8mb4;
-- 1) 产品主表增加 expire_date
ALTER TABLE bms_product
ADD COLUMN expire_date date DEFAULT NULL COMMENT '产品失效日期(空=长期;下一版生效日=本版失效日)' AFTER effective_date;
ALTER TABLE bms_product
MODIFY COLUMN status char(1) NOT NULL DEFAULT '0' COMMENT '0待生效 1已生效 2已失效';
-- 2) 国家明细增加 status
ALTER TABLE bms_product_country
ADD COLUMN status char(1) NOT NULL DEFAULT '0' COMMENT '0待生效 1已生效 2已失效' AFTER expire_date;
-- 3) 按同代码下一版生效日回填产品失效日
UPDATE bms_product p
INNER JOIN (
SELECT p1.product_id,
(SELECT MIN(p2.effective_date)
FROM bms_product p2
WHERE p2.product_code = p1.product_code
AND p2.effective_date IS NOT NULL
AND p1.effective_date IS NOT NULL
AND p2.effective_date > p1.effective_date) AS next_eff
FROM bms_product p1
) t ON t.product_id = p.product_id
SET p.expire_date = t.next_eff
WHERE p.expire_date IS NULL AND t.next_eff IS NOT NULL;
-- 4) 按生效/失效日期回写 status
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;
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;
-- 5) 字典值 ACTIVE/PENDING/EXPIRED 改为 1/0/2
DELETE FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value IN ('ACTIVE', 'PENDING', 'EXPIRED');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 1, N'已生效', '1', 'bms_product_effect_status', '', 'success', 'Y', '0', 'admin', sysdate(), N'1 已生效'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = '1');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 2, N'待生效', '0', 'bms_product_effect_status', '', 'warning', 'N', '0', 'admin', sysdate(), N'0 待生效'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = '0');
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, create_time, remark)
SELECT 3, N'已失效', '2', 'bms_product_effect_status', '', 'info', 'N', '0', 'admin', sysdate(), N'2 已失效'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'bms_product_effect_status' AND dict_value = '2');
+7 -3
View File
@@ -17,11 +17,15 @@ export function getProduct(productId) {
}) })
} }
/** 查询产品国家明细 */ /** 查询产品国家明细(优先 productId,绑定到具体产品版本行) */
export function listProductCountries(productCode) { export function listProductCountries(productCode, effectiveDate, productId) {
const params = {}
if (effectiveDate) params.effectiveDate = effectiveDate
if (productId) params.productId = productId
return request({ return request({
url: '/price/product/' + productCode + '/countries', url: '/price/product/' + productCode + '/countries',
method: 'get' method: 'get',
params
}) })
} }
@@ -2,9 +2,6 @@
<el-drawer v-model="visible" size="920px" :title="title" append-to-body> <el-drawer v-model="visible" size="920px" :title="title" append-to-body>
<div class="mb8" style="display: flex; align-items: center; gap: 12px"> <div class="mb8" style="display: flex; align-items: center; gap: 12px">
<el-input v-model="keyword" placeholder="搜索国家(中文名 / ISO2)" clearable style="width: 260px" /> <el-input v-model="keyword" placeholder="搜索国家(中文名 / ISO2)" clearable style="width: 260px" />
<!-- <el-select v-model="status" placeholder="状态(全部)" clearable style="width: 160px">
<el-option v-for="d in bms_product_effect_status" :key="d.value" :label="d.label" :value="d.value" />
</el-select> -->
</div> </div>
<simple-table <simple-table
:data="filteredList" :data="filteredList"
@@ -29,8 +26,6 @@
import SimpleTable from '@/components/SimpleTable' import SimpleTable from '@/components/SimpleTable'
import { listProductCountries } from '@/api/price/product' import { listProductCountries } from '@/api/price/product'
const { bms_product_effect_status } = useDict('bms_product_effect_status')
const columns = [ const columns = [
{ prop: 'countryName', label: '国家', width: 90, showOverflowTooltip: true }, { prop: 'countryName', label: '国家', width: 90, showOverflowTooltip: true },
{ prop: 'countryCode', label: 'ISO2', width: 60, align: 'center' }, { prop: 'countryCode', label: 'ISO2', width: 60, align: 'center' },
@@ -48,7 +43,7 @@ const columns = [
{ prop: 'leadTime', label: '时效', width: 100, showOverflowTooltip: true }, { prop: 'leadTime', label: '时效', width: 100, showOverflowTooltip: true },
{ prop: 'effectiveDate', label: '生效日期', width: 110, align: 'center' }, { prop: 'effectiveDate', label: '生效日期', width: 110, align: 'center' },
{ prop: 'expireDate', label: '失效日期', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' }, { prop: 'expireDate', label: '失效日期', width: 110, align: 'center', formatter: (row) => row.expireDate || '长期' },
{ prop: 'effectStatus', label: '状态', width: 90, align: 'center', dict: 'bms_product_effect_status' } { prop: 'status', label: '状态', width: 90, align: 'center', dict: 'bms_product_effect_status' }
] ]
const visible = ref(false) const visible = ref(false)
@@ -64,19 +59,24 @@ const filteredList = computed(() => {
return list.value.filter(r => { return list.value.filter(r => {
if (kw && !(String(r.countryName || '').toLowerCase().includes(kw) || if (kw && !(String(r.countryName || '').toLowerCase().includes(kw) ||
String(r.countryCode || '').toLowerCase().includes(kw))) return false String(r.countryCode || '').toLowerCase().includes(kw))) return false
if (st && r.effectStatus !== st) return false if (st && String(r.status) !== String(st)) return false
return true return true
}) })
}) })
function formatEffDate(v) {
if (!v) return undefined
return String(v).slice(0, 10)
}
function open(row) { function open(row) {
title.value = row.productCode + ' \u00b7 ' + row.productName + ' \u00b7 \u56fd\u5bb6\u670d\u52a1\u660e\u7ec6' title.value = row.productCode + ' · ' + row.productName + ' · 国家服务明细'
visible.value = true visible.value = true
loading.value = true loading.value = true
list.value = [] list.value = []
keyword.value = '' keyword.value = ''
status.value = '' status.value = ''
listProductCountries(row.productCode).then(res => { listProductCountries(row.productCode, formatEffDate(row.effectiveDate), row.productId).then(res => {
list.value = (res.data || []).slice().sort((a, b) => { list.value = (res.data || []).slice().sort((a, b) => {
return String(a.countryCode || '').localeCompare(String(b.countryCode || '')) return String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
}) })
@@ -85,6 +85,15 @@ const schemas = computed(() => [
component: 'date', component: 'date',
placeholder: '选择日期', placeholder: '选择日期',
span: 12, span: 12,
width: '100%',
rules: [{ required: true, message: '生效日期不能为空', trigger: 'change' }]
},
{
label: '失效日期',
prop: 'expireDate',
component: 'date',
placeholder: '选择日期',
span: 12,
width: '100%' width: '100%'
}, },
{ {
@@ -116,6 +125,7 @@ function emptyForm() {
updateNote: undefined, updateNote: undefined,
taxMode: 'TAX_INCLUDED', taxMode: 'TAX_INCLUDED',
effectiveDate: undefined, effectiveDate: undefined,
expireDate: undefined,
status: '0', status: '0',
remark: undefined remark: undefined
} }
@@ -67,7 +67,7 @@ function open(row) {
surchargePool.value = res.rows || [] surchargePool.value = res.rows || []
}) })
if (row && row.productCode) { if (row && row.productCode) {
listProductCountries(row.productCode).then(res => { listProductCountries(row.productCode, undefined, row.productId).then(res => {
countryList.value = res.data || [] countryList.value = res.data || []
}) })
} }
@@ -5,14 +5,24 @@
:closable="false" :closable="false"
show-icon show-icon
class="mb16" class="mb16"
title="Excel 导入:上传威狮B2C小包价表.xlsx。入库产品目录 + 各产品 sheet 的国家×重量段(运费/挂号费/尺寸/尾程/时效);不导入退件重派、申报说明、赔偿标准。" title="Excel 导入:相同产品代码+生效日期不覆盖;不同生效日期新增一条产品。同时入库国家×重量段(运费/挂号费/尺寸/尾程/时效)。"
/> />
<el-tabs v-model="effectTab" class="mb8" @tab-change="onTabChange">
<el-tab-pane
v-for="t in statusTabs"
:key="t.name"
:label="t.label"
:name="t.name"
/>
</el-tabs>
<basic-table <basic-table
ref="tableRef" ref="tableRef"
:columns="columns" :columns="columns"
:search-columns="searchColumns" :search-columns="searchColumns"
:query="queryList" :query="queryList"
:extra-params="extraParams"
row-key="productId" row-key="productId"
> >
<template #toolbar="{ selection, ids }"> <template #toolbar="{ selection, ids }">
@@ -35,7 +45,7 @@
</template> </template>
<template #col-status="{ row }"> <template #col-status="{ row }">
<dict-tag :options="bms_product_effect_status" :value="resolveEffectStatus(row)" /> <dict-tag :options="bms_product_effect_status" :value="row.status" />
</template> </template>
<template #col-action="{ row }"> <template #col-action="{ row }">
@@ -54,9 +64,7 @@
title="产品 Excel 导入" title="产品 Excel 导入"
action="/price/product/importData" action="/price/product/importData"
width="560px" width="560px"
:default-update-support="true" :show-update-support="false"
update-support-label="是否更新已存在的产品主数据"
update-support-hint="国家明细:新生效日晚于上次则保留历史,上次失效日=本次生效日"
@success="() => tableRef.refresh()" @success="() => tableRef.refresh()"
/> />
</div> </div>
@@ -78,6 +86,41 @@ const countryRef = ref()
const surchargeRef = ref() const surchargeRef = ref()
const importRef = ref() const importRef = ref()
const TAB_ACTIVE = 'active'
const TAB_PENDING = 'pending'
const TAB_EXPIRED = 'expired'
const effectTab = ref(TAB_ACTIVE)
const statusTabs = computed(() => [
{ name: TAB_ACTIVE, code: '1', label: dictLabel('1', 'ACTIVE') },
{ name: TAB_PENDING, code: '0', label: dictLabel('0', 'PENDING') },
{ name: TAB_EXPIRED, code: '2', label: dictLabel('2', 'EXPIRED') }
])
const extraParams = computed(() => ({ status: tabToStatus(effectTab.value) }))
function dictLabel(code, legacy) {
const list = bms_product_effect_status.value || []
const hit = list.find(d => String(d.value) === code || String(d.value) === legacy)
return hit?.label || code
}
function tabToStatus(name) {
if (name === TAB_ACTIVE || name === 'ACTIVE' || name === '1') return '1'
if (name === TAB_PENDING || name === 'PENDING' || name === '0') return '0'
if (name === TAB_EXPIRED || name === 'EXPIRED' || name === '2') return '2'
return '1'
}
function onTabChange(name) {
effectTab.value = String(name || TAB_ACTIVE)
nextTick(() => {
tableRef.value?.clearSelection()
tableRef.value?.reload()
})
}
const columns = [ const columns = [
{ prop: 'productCode', label: '产品代码', minWidth: 110 }, { prop: 'productCode', label: '产品代码', minWidth: 110 },
{ prop: 'productName', label: '产品名称', minWidth: 140, showOverflowTooltip: true }, { prop: 'productName', label: '产品名称', minWidth: 140, showOverflowTooltip: true },
@@ -86,6 +129,7 @@ const columns = [
{ prop: 'destRegion', label: '目的区域', width: 90, align: 'center' }, { prop: 'destRegion', label: '目的区域', width: 90, align: 'center' },
{ prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' }, { prop: 'taxMode', label: '税模式', width: 90, align: 'center', dict: 'bms_tax_mode' },
{ prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' }, { prop: 'effectiveDate', label: '生效日期', width: 120, align: 'center' },
{ prop: 'expireDate', label: '失效日期', width: 120, align: 'center' },
{ prop: 'countryCount', label: '国家数', width: 80, align: 'center' }, { prop: 'countryCount', label: '国家数', width: 80, align: 'center' },
{ prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' }, { prop: 'surchargeCount', label: '附加费数', width: 90, align: 'center' },
{ prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' }, { prop: 'status', label: '状态', width: 90, align: 'center', slot: 'status' },
@@ -103,35 +147,15 @@ const searchColumns = [
dict: 'bms_tax_mode', dict: 'bms_tax_mode',
placeholder: '税模式', placeholder: '税模式',
clearable: true clearable: true
},
{
prop: 'effectStatus',
label: '状态',
component: 'select',
dict: 'bms_product_effect_status',
placeholder: '请选择状态',
clearable: true
} }
] ]
function queryList(params) { function queryList(params) {
return listProduct(params) return listProduct({ ...params, status: tabToStatus(effectTab.value) })
}
function todayStr() {
return new Date().toISOString().slice(0, 10)
}
/** ACTIVE已生效 / PENDING待生效 / EXPIRED已失效 */
function resolveEffectStatus(row) {
if (!row) return 'PENDING'
if (row.status === '1') return 'EXPIRED'
if (!row.effectiveDate) return 'PENDING'
return String(row.effectiveDate).slice(0, 10) <= todayStr() ? 'ACTIVE' : 'PENDING'
} }
function isProductActive(row) { function isProductActive(row) {
return resolveEffectStatus(row) === 'ACTIVE' return row && String(row.status) === '1'
} }
function handleAdd() { function handleAdd() {
+19 -4
View File
@@ -95,7 +95,9 @@
<el-descriptions-item label="套餐编号">{{ result.priceSetCode || '-' }}</el-descriptions-item> <el-descriptions-item label="套餐编号">{{ result.priceSetCode || '-' }}</el-descriptions-item>
<el-descriptions-item label="套餐名称">{{ result.priceSetName || '-' }}</el-descriptions-item> <el-descriptions-item label="套餐名称">{{ result.priceSetName || '-' }}</el-descriptions-item>
<el-descriptions-item label="套餐类型">{{ result.setType || '-' }}</el-descriptions-item> <el-descriptions-item label="套餐类型">{{ result.setType || '-' }}</el-descriptions-item>
<el-descriptions-item label="币种">{{ result.currency || 'RMB' }}</el-descriptions-item> <el-descriptions-item label="币种">
<dict-tag :options="bms_currency" :value="displayCurrency(result.currency)" />
</el-descriptions-item>
<el-descriptions-item label="运费单价">{{ formatMoney(result.freightRate) }}</el-descriptions-item> <el-descriptions-item label="运费单价">{{ formatMoney(result.freightRate) }}</el-descriptions-item>
<el-descriptions-item label="折扣前单价">{{ formatMoney(result.baseFreightRate) }}</el-descriptions-item> <el-descriptions-item label="折扣前单价">{{ formatMoney(result.baseFreightRate) }}</el-descriptions-item>
<el-descriptions-item label="折扣%">{{ result.discountPercent != null ? result.discountPercent : '-' }}</el-descriptions-item> <el-descriptions-item label="折扣%">{{ result.discountPercent != null ? result.discountPercent : '-' }}</el-descriptions-item>
@@ -130,7 +132,11 @@
<span>{{ formatMoney(row.amount) }}</span> <span>{{ formatMoney(row.amount) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="币种" width="80" align="center" prop="currency" /> <el-table-column label="币种" width="90" align="center">
<template #default="{ row }">
<dict-tag :options="bms_currency" :value="displayCurrency(row.currency)" />
</template>
</el-table-column>
<el-table-column label="取值方式" width="110" align="center"> <el-table-column label="取值方式" width="110" align="center">
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template> <template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
</el-table-column> </el-table-column>
@@ -209,7 +215,11 @@
<el-table-column label="金额" min-width="120" align="right"> <el-table-column label="金额" min-width="120" align="right">
<template #default="{ row }">{{ formatMoney(row.amount) }}</template> <template #default="{ row }">{{ formatMoney(row.amount) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="币种" width="80" align="center" prop="currency" /> <el-table-column label="币种" width="90" align="center">
<template #default="{ row }">
<dict-tag :options="bms_currency" :value="displayCurrency(row.currency)" />
</template>
</el-table-column>
<el-table-column label="说明" min-width="160" show-overflow-tooltip> <el-table-column label="说明" min-width="160" show-overflow-tooltip>
<template #default="{ row }">{{ row.remark || '-' }}</template> <template #default="{ row }">{{ row.remark || '-' }}</template>
</el-table-column> </el-table-column>
@@ -225,7 +235,7 @@ import { quoteBilling, listQuote, getQuote } from '@/api/price/billing'
import { listProduct, listProductCountries } from '@/api/price/product' import { listProduct, listProductCountries } from '@/api/price/product'
const { proxy } = getCurrentInstance() const { proxy } = getCurrentInstance()
const { bms_fee_type } = useDict('bms_fee_type') const { bms_fee_type, bms_currency } = useDict('bms_fee_type', 'bms_currency')
const formRef = ref() const formRef = ref()
const historyTableRef = ref() const historyTableRef = ref()
const loading = ref(false) const loading = ref(false)
@@ -411,6 +421,11 @@ function feeTypeLabel(type) {
return found ? found.label : '' return found ? found.label : ''
} }
function displayCurrency(v) {
if (!v || v === 'RMB' || v === 'rmb') return 'CNY'
return v
}
function displayFeeName(row) { function displayFeeName(row) {
const typeLabel = feeTypeLabel(row.feeType) const typeLabel = feeTypeLabel(row.feeType)
const name = String(row.feeName || row.fee_name || '').trim() const name = String(row.feeName || row.fee_name || '').trim()
+6 -1
View File
@@ -219,7 +219,7 @@
:closable="false" :closable="false"
show-icon show-icon
class="mb12" class="mb12"
title="套餐创建后处于草稿状态,可暂不绑定客户。优先级数字越小越优先(1 = 最高)。同一客户可绑定多个套餐。" title="套餐创建后处于草稿状态,可暂不绑定客户。一个客户只能绑定一个套餐。"
/> />
<el-table :data="form.customers" border empty-text="暂未绑定客户"> <el-table :data="form.customers" border empty-text="暂未绑定客户">
<el-table-column label="客户ID" min-width="120"> <el-table-column label="客户ID" min-width="120">
@@ -597,6 +597,11 @@ function saveAndSubmit() {
} }
function doSave(submit) { function doSave(submit) {
const ids = (form.customers || []).filter(c => c.customerId).map(c => String(c.customerId))
if (new Set(ids).size !== ids.length) {
proxy.$modal.msgError('该客户已绑定套餐,一个客户只能绑定一个套餐')
return
}
saving.value = true saving.value = true
savePriceSet(buildPayload(submit)).then(res => { savePriceSet(buildPayload(submit)).then(res => {
form.priceSetId = res.data form.priceSetId = res.data
+1 -1
View File
@@ -158,7 +158,7 @@
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="rel-hint">优先级数字越小越优先;同一客户绑定多个套餐时,按优先级 → 生效日期依次命中。</div> <div class="rel-hint">一个客户只能绑定一个套餐;解绑后可绑定其他套餐。</div>
</el-card> </el-card>
<el-dialog :title="detailTitle" v-model="detailOpen" width="560px" append-to-body> <el-dialog :title="detailTitle" v-model="detailOpen" width="560px" append-to-body>