修改基础价格版本功能

This commit is contained in:
wuweihua
2026-09-28 15:45:07 +08:00
parent 1083c2876f
commit 32ab61b51c
22 changed files with 831 additions and 132 deletions
@@ -15,6 +15,10 @@ public class BmsProductCountry implements Serializable
private Long detailId;
/** FK to bms_product.product_id (one country version belongs to one product row) */
private Long productId;
/** FK to bms_product_price_version.ppv_id */
private Long ppvId;
/** Display only: joined from bms_product_price_version.version_no */
private String versionNo;
private String productCode;
private String countryName;
private String countryCode;
@@ -0,0 +1,32 @@
package com.bms.price.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.bms.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** Product base price version bms_product_price_version */
@Data
@EqualsAndHashCode(callSuper = true)
public class BmsProductPriceVersion extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long ppvId;
private String productCode;
/** Linked bms_product.product_id for this version row */
private Long productId;
/** V1 / V2 */
private String versionNo;
private Integer versionSeq;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date effectiveDate;
/** null = open; set to next version effective_date when that version starts */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date expireDate;
/** 0 pending / 1 active / 2 expired */
private String status;
/** query only: product count for this effective_date option */
private Integer productCount;
}
@@ -24,6 +24,11 @@ public class PrcPriceSet extends BaseEntity
private BigDecimal defaultDiscount;
private String defaultType;
private String calculateSign;
/**
* Pull-only: product base price version_no (V1/V2 from bms_product_price_version).
* Not a DB column — used when pullFromLibrary=Y to choose library rows.
*/
private String baseVersionNo;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date effectiveDate;
@JsonFormat(pattern = "yyyy-MM-dd")
@@ -20,6 +20,12 @@ public interface BmsProductCountryMapper
/** 按明细行主键更新可编辑字段(重量段/运费/挂号费/尺寸/尾程/时效/生效失效日期) */
public int updateBmsProductCountry(BmsProductCountry country);
/** Import overlay: update price/zone/attrs on matched country row */
public int updateBmsProductCountryImport(BmsProductCountry country);
/** Bind legacy open row to a version */
public int updatePpvId(@Param("detailId") Long detailId, @Param("ppvId") Long ppvId);
public int deleteByProductId(Long productId);
public int deleteByProductCode(String productCode);
@@ -44,6 +50,29 @@ public interface BmsProductCountryMapper
@Param("productCode") String productCode,
@Param("effectiveDate") Date effectiveDate);
/** Open country rows (expire_date is null) for product */
public List<BmsProductCountry> selectOpenByProductCode(@Param("productCode") String productCode);
/** Country rows under a price version */
public List<BmsProductCountry> selectByPpvId(@Param("ppvId") Long ppvId);
/** Copy open/source version rows into a new version (new ppv_id / product_id / effective_date) */
public int copyCountriesFromVersion(
@Param("srcPpvId") Long srcPpvId,
@Param("newPpvId") Long newPpvId,
@Param("productId") Long productId,
@Param("productCode") String productCode,
@Param("effectiveDate") Date effectiveDate,
@Param("status") String status);
/** Copy currently open rows (legacy / no ppv_id) into a new version */
public int copyOpenCountriesToVersion(
@Param("productCode") String productCode,
@Param("newPpvId") Long newPpvId,
@Param("productId") Long productId,
@Param("effectiveDate") Date effectiveDate,
@Param("status") String status);
/**
* Match by product + country + weight + bizDate (+ optional zoneCode).
* When zoneCode is empty, prefer rows without zone.
@@ -0,0 +1,38 @@
package com.bms.price.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.BmsProductPriceVersion;
/**
* Product base price version
*/
public interface BmsProductPriceVersionMapper
{
public BmsProductPriceVersion selectBmsProductPriceVersionById(Long ppvId);
/** Open version (expire_date is null), prefer latest effective_date */
public BmsProductPriceVersion selectOpenByProductCode(@Param("productCode") String productCode);
public BmsProductPriceVersion selectByProductCodeAndEffectiveDate(
@Param("productCode") String productCode,
@Param("effectiveDate") Date effectiveDate);
public Integer selectMaxVersionSeq(@Param("productCode") String productCode);
/** Distinct effective_date options for package create dropdown (desc) */
public List<BmsProductPriceVersion> selectVersionOptions();
public int insertBmsProductPriceVersion(BmsProductPriceVersion version);
public int updateBmsProductPriceVersion(BmsProductPriceVersion version);
/** Close open versions: set expire_date = newEffectiveDate where effective_date &lt; newEffectiveDate */
public int expireOpenByProductCode(
@Param("productCode") String productCode,
@Param("newEffectiveDate") Date newEffectiveDate);
/** Recompute status from effective_date / expire_date vs today */
public int refreshStatusByDates();
}
@@ -42,19 +42,22 @@ public interface PrcPriceSetDetailMapper
public int copyDetailsFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId);
/**
* FIXED: insert details from active product + country library (asOf window or openOnly fallback).
* @return inserted row count
* FIXED: insert details from product country library.
* When baseVersionNo is set, pull that base-price version only;
* otherwise asOf window or openOnly fallback.
*/
public int insertFixedDetailsFromLibrary(
@Param("priceSetId") Long priceSetId,
@Param("psvId") Long psvId,
@Param("asOf") Date asOf,
@Param("openOnly") boolean openOnly);
@Param("openOnly") boolean openOnly,
@Param("baseVersionNo") String baseVersionNo);
/** DISCOUNT: one detail row per active product */
/** DISCOUNT: one detail row per product (optionally scoped to base version_no) */
public int insertDiscountDetailsFromLibrary(
@Param("priceSetId") Long priceSetId,
@Param("psvId") Long psvId);
@Param("psvId") Long psvId,
@Param("baseVersionNo") String baseVersionNo);
/** FIXED: product + country + zone + productCountryId / weight; optional psvId scopes to version */
public PrcPriceSetDetail selectFixedMatch(
@@ -5,6 +5,7 @@ import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.bms.price.domain.BmsProduct;
import com.bms.price.domain.BmsProductCountry;
import com.bms.price.domain.BmsProductPriceVersion;
/**
* 产品列表 服务层
@@ -54,4 +55,7 @@ public interface IBmsProductService
* @return HTML result message
*/
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception;
/** Distinct base-price version options (by effective_date desc) for package create */
public List<BmsProductPriceVersion> selectPriceVersionOptions();
}
@@ -34,9 +34,11 @@ import com.bms.common.utils.MessageUtils;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.BmsProduct;
import com.bms.price.domain.BmsProductCountry;
import com.bms.price.domain.BmsProductPriceVersion;
import com.bms.price.domain.PrcZone;
import com.bms.price.mapper.BmsProductCountryMapper;
import com.bms.price.mapper.BmsProductMapper;
import com.bms.price.mapper.BmsProductPriceVersionMapper;
import com.bms.price.mapper.PrcZoneMapper;
import com.bms.price.service.IBmsProductService;
import com.bms.price.service.IPrcSurchargeRelService;
@@ -74,6 +76,9 @@ public class BmsProductServiceImpl implements IBmsProductService
@Autowired
private BmsProductCountryMapper countryMapper;
@Autowired
private BmsProductPriceVersionMapper priceVersionMapper;
@Autowired
private BmsRegionMapper regionMapper;
@@ -97,6 +102,7 @@ public class BmsProductServiceImpl implements IBmsProductService
{
productMapper.refreshStatusByDates();
countryMapper.refreshStatusByDates();
priceVersionMapper.refreshStatusByDates();
if (product != null)
{
product.setStatus(normalizeStatusCode(product.getStatus()));
@@ -415,6 +421,13 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
@Override
public List<BmsProductPriceVersion> selectPriceVersionOptions()
{
List<BmsProductPriceVersion> list = priceVersionMapper.selectVersionOptions();
return list == null ? new ArrayList<>() : list;
}
/** @return int[]{added, skipped, countryRows} */
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
@@ -476,8 +489,10 @@ public class BmsProductServiceImpl implements IBmsProductService
/**
* Versioned country save:
* - newEff &gt; last open: close old (expire_date = newEff), insert new (keep history)
* - newEff equals last open: replace open rows
* - require effective_date
* - newEff &gt; last open: expire previous version (expire_date = newEff), create new version,
* copy previous country rows, then match import (hit=update, miss=insert)
* - newEff equals last open: reuse open version, match update/insert (no full replace)
* - newEff &lt; last open: reject
*/
private int saveCountriesVersioned(BmsProduct product, List<BmsProductCountry> countries)
@@ -488,40 +503,125 @@ public class BmsProductServiceImpl implements IBmsProductService
}
String productCode = product.getProductCode();
Long productId = product.getProductId();
Date newEff = resolveImportEffectiveDate(countries);
Date lastOpenEff = countryMapper.selectMaxOpenEffectiveDate(productCode);
Date newEff = toDateOnly(resolveImportEffectiveDate(countries));
if (newEff == null)
{
newEff = toDateOnly(product.getEffectiveDate());
}
if (newEff == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.effEmpty"));
}
if (lastOpenEff != null && newEff != null && newEff.before(lastOpenEff))
BmsProductPriceVersion openVer = priceVersionMapper.selectOpenByProductCode(productCode);
Date lastOpenEff = openVer != null ? toDateOnly(openVer.getEffectiveDate())
: toDateOnly(countryMapper.selectMaxOpenEffectiveDate(productCode));
if (lastOpenEff != null && newEff.before(lastOpenEff))
{
throw new ServiceException(MessageUtils.message("price.product.import.effTooEarly",
formatDate(newEff), formatDate(lastOpenEff)));
}
if (lastOpenEff != null && newEff != null && newEff.after(lastOpenEff))
BmsProductPriceVersion targetVer;
boolean newVersionDay = lastOpenEff == null || newEff.after(lastOpenEff);
if (newVersionDay)
{
// Previous open version expire_date = this effective_date; keep old rows
countryMapper.expireOpenByProductCode(productCode, newEff);
countryMapper.refreshStatusByDates();
List<BmsProductCountry> prevSnapshot = null;
Long srcPpvId = openVer != null ? openVer.getPpvId() : null;
if (lastOpenEff != null)
{
if (srcPpvId != null)
{
prevSnapshot = null; // copy via SQL by ppv_id after expire
}
else
{
prevSnapshot = countryMapper.selectOpenByProductCode(productCode);
}
priceVersionMapper.expireOpenByProductCode(productCode, newEff);
countryMapper.expireOpenByProductCode(productCode, newEff);
}
targetVer = createPriceVersion(product, newEff);
String newStatus = resolveStoredStatus(newEff, null);
if (srcPpvId != null)
{
countryMapper.copyCountriesFromVersion(srcPpvId, targetVer.getPpvId(), productId,
productCode, newEff, newStatus);
}
else if (prevSnapshot != null && !prevSnapshot.isEmpty())
{
insertCountryCopies(prevSnapshot, targetVer.getPpvId(), productId, productCode, newEff, newStatus);
}
}
else if (lastOpenEff != null)
else
{
// Same effective day: replace open rows so re-import can fill/fix details
countryMapper.deleteOpenByProductCodeAndEffectiveDate(productCode, newEff);
targetVer = priceVersionMapper.selectByProductCodeAndEffectiveDate(productCode, newEff);
if (targetVer == null)
{
targetVer = openVer != null ? openVer : createPriceVersion(product, newEff);
}
else if (targetVer.getProductId() == null && productId != null)
{
targetVer.setProductId(productId);
targetVer.setUpdateBy(product.getUpdateBy());
priceVersionMapper.updateBmsProductPriceVersion(targetVer);
}
assignOpenRowsToVersion(productCode, targetVer.getPpvId());
}
for (BmsProductCountry c : countries)
List<BmsProductCountry> baseRows = countryMapper.selectByPpvId(targetVer.getPpvId());
if (baseRows == null)
{
c.setProductId(productId);
c.setProductCode(productCode);
c.setExpireDate(null);
if (c.getEffectiveDate() == null)
{
c.setEffectiveDate(newEff);
}
c.setStatus(resolveStoredStatus(c.getEffectiveDate(), c.getExpireDate()));
baseRows = new ArrayList<>();
}
Map<String, BmsProductCountry> byKey = new HashMap<>();
for (BmsProductCountry row : baseRows)
{
byKey.put(countryMatchKey(row), row);
}
int touched = 0;
for (BmsProductCountry incoming : countries)
{
incoming.setProductId(productId);
incoming.setProductCode(productCode);
incoming.setPpvId(targetVer.getPpvId());
incoming.setEffectiveDate(newEff);
incoming.setExpireDate(null);
incoming.setStatus(resolveStoredStatus(newEff, null));
String key = countryMatchKey(incoming);
BmsProductCountry existing = byKey.get(key);
try
{
countryMapper.insertBmsProductCountry(c);
if (existing != null)
{
existing.setProductId(productId);
existing.setPpvId(targetVer.getPpvId());
existing.setCountryCode(incoming.getCountryCode());
existing.setZoneId(incoming.getZoneId());
existing.setZoneCode(incoming.getZoneCode());
existing.setZoneName(incoming.getZoneName());
existing.setWeightMin(incoming.getWeightMin());
existing.setWeightMax(incoming.getWeightMax());
existing.setFreightPrice(incoming.getFreightPrice());
existing.setRegistrationFee(incoming.getRegistrationFee());
existing.setSizeLimit(incoming.getSizeLimit());
existing.setLastMile(incoming.getLastMile());
existing.setLeadTime(incoming.getLeadTime());
existing.setEffectiveDate(newEff);
existing.setExpireDate(null);
existing.setStatus(resolveStoredStatus(newEff, null));
countryMapper.updateBmsProductCountryImport(existing);
}
else
{
countryMapper.insertBmsProductCountry(incoming);
byKey.put(key, incoming);
}
touched++;
}
catch (Exception e)
{
@@ -530,6 +630,10 @@ public class BmsProductServiceImpl implements IBmsProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.needFeeAlter"));
}
if (tip.contains("ppv_id") || tip.contains("bms_product_price_version"))
{
throw new ServiceException(MessageUtils.message("price.product.import.needVersionAlter"));
}
if (tip.contains("expire_date") || tip.contains("uk_product_country_weight")
|| tip.contains("uk_product_country_zone_weight"))
{
@@ -538,7 +642,94 @@ public class BmsProductServiceImpl implements IBmsProductService
throw e;
}
}
return countries.size();
countryMapper.refreshStatusByDates();
priceVersionMapper.refreshStatusByDates();
return touched;
}
private BmsProductPriceVersion createPriceVersion(BmsProduct product, Date effectiveDate)
{
Integer maxSeq = priceVersionMapper.selectMaxVersionSeq(product.getProductCode());
int seq = maxSeq == null ? 1 : maxSeq.intValue() + 1;
BmsProductPriceVersion ver = new BmsProductPriceVersion();
ver.setProductCode(product.getProductCode());
ver.setProductId(product.getProductId());
ver.setVersionSeq(Integer.valueOf(seq));
ver.setVersionNo("V" + seq);
ver.setEffectiveDate(effectiveDate);
ver.setExpireDate(null);
ver.setStatus(resolveStoredStatus(effectiveDate, null));
ver.setCreateBy(product.getCreateBy() != null ? product.getCreateBy() : product.getUpdateBy());
priceVersionMapper.insertBmsProductPriceVersion(ver);
return ver;
}
/** After expire, insert in-memory previous open rows into the new version */
private void insertCountryCopies(List<BmsProductCountry> prevSnapshot, Long newPpvId, Long productId,
String productCode, Date newEff, String status)
{
for (BmsProductCountry src : prevSnapshot)
{
BmsProductCountry copy = new BmsProductCountry();
copy.setProductId(productId);
copy.setPpvId(newPpvId);
copy.setProductCode(productCode);
copy.setCountryName(src.getCountryName());
copy.setCountryCode(src.getCountryCode());
copy.setZoneId(src.getZoneId());
copy.setZoneCode(src.getZoneCode());
copy.setZoneName(src.getZoneName());
copy.setWeightMin(src.getWeightMin());
copy.setWeightMax(src.getWeightMax());
copy.setFreightPrice(src.getFreightPrice());
copy.setRegistrationFee(src.getRegistrationFee());
copy.setSizeLimit(src.getSizeLimit());
copy.setLastMile(src.getLastMile());
copy.setLeadTime(src.getLeadTime());
copy.setEffectiveDate(newEff);
copy.setExpireDate(null);
copy.setStatus(status);
countryMapper.insertBmsProductCountry(copy);
}
}
private void assignOpenRowsToVersion(String productCode, Long ppvId)
{
if (ppvId == null)
{
return;
}
List<BmsProductCountry> openRows = countryMapper.selectOpenByProductCode(productCode);
if (openRows == null)
{
return;
}
for (BmsProductCountry row : openRows)
{
if (row.getPpvId() == null)
{
countryMapper.updatePpvId(row.getDetailId(), ppvId);
}
}
}
/** Match key: country + zone + weight band */
private String countryMatchKey(BmsProductCountry row)
{
String country = row.getCountryName() == null ? "" : row.getCountryName().trim();
String zone = row.getZoneCode() == null ? "" : row.getZoneCode().trim().toUpperCase(Locale.ROOT);
String wMin = normalizeDecimal(row.getWeightMin());
String wMax = normalizeDecimal(row.getWeightMax());
return country + "|" + zone + "|" + wMin + "|" + wMax;
}
private String normalizeDecimal(BigDecimal v)
{
if (v == null)
{
return "";
}
return v.stripTrailingZeros().toPlainString();
}
private Date resolveImportEffectiveDate(List<BmsProductCountry> countries)
@@ -1830,20 +1830,21 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
detailMapper.deletePrcPriceSetDetailByPsvId(version.getPsvId());
// clean legacy rows without psv_id under this set (would look like duplicates in UI)
detailMapper.deleteOrphanDetailsByPriceSetId(set.getPriceSetId());
String baseVersionNo = set.getBaseVersionNo();
if (StringUtils.isEmpty(baseVersionNo))
{
throw new ServiceException(MessageUtils.message("price.set.baseVersionNo.empty"));
}
int inserted;
if (TYPE_DISCOUNT.equals(set.getSetType()))
{
inserted = detailMapper.insertDiscountDetailsFromLibrary(set.getPriceSetId(), version.getPsvId());
inserted = detailMapper.insertDiscountDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId(), baseVersionNo);
}
else
{
inserted = detailMapper.insertFixedDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId(), version.getEffectiveTime(), false);
if (inserted <= 0)
{
inserted = detailMapper.insertFixedDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId(), version.getEffectiveTime(), true);
}
set.getPriceSetId(), version.getPsvId(), null, false, baseVersionNo);
}
if (inserted <= 0)
{