重构产品价和红线价功能

This commit is contained in:
wuweihua
2026-10-18 14:16:27 +08:00
parent db6f5e882a
commit 60d4614695
84 changed files with 6753 additions and 2843 deletions
@@ -7,14 +7,17 @@ import com.bms.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** BMS产品主表 product */
/** Catalog product view over prc_price_set_product (price_type=PRODUCT/REDLINE) */
@Data
@EqualsAndHashCode(callSuper = true)
public class BmsProduct extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** maps to prc_price_set_product.psp_id */
private Long productId;
private Long psvId;
private String priceType;
private String productCode;
private String productName;
private String serviceLevel;
@@ -38,8 +41,6 @@ public class BmsProduct extends BaseEntity
/** 0未删除 1已删除 */
private String isDel;
private String sourceSheet;
/** FK prc_price_excel_file.pef_id */
private Long pefId;
private Long tmsId;
private Integer countryCount;
private Integer surchargeCount;
@@ -12,7 +12,11 @@ public class BmsProductCountry implements Serializable
{
private static final long serialVersionUID = 1L;
/** maps to prc_price_set_detail.psd_id */
private Long detailId;
private Long psvId;
private Long pspId;
private String priceType;
/** FK to product.product_id */
private Long productId;
private String productCode;
@@ -19,6 +19,8 @@ public class BmsProductImportTask implements Serializable
private int percent;
private String message;
private String error;
/** Preview parse payload (products / details); not persisted to DB. */
private Object result;
public String getTaskId()
{
@@ -69,4 +71,14 @@ public class BmsProductImportTask implements Serializable
{
this.error = error;
}
public Object getResult()
{
return result;
}
public void setResult(Object result)
{
this.result = result;
}
}
@@ -7,7 +7,7 @@ import com.bms.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** Redline catalog main table redline_product */
/** Redline catalog product view over prc_price_set_product (price_type=REDLINE) */
@Data
@EqualsAndHashCode(callSuper = true)
public class BmsRedlineProduct extends BaseEntity
@@ -15,6 +15,8 @@ public class BmsRedlineProduct extends BaseEntity
private static final long serialVersionUID = 1L;
private Long productId;
private Long psvId;
private String priceType;
private String productCode;
private String productName;
private String serviceLevel;
@@ -38,8 +40,6 @@ public class BmsRedlineProduct extends BaseEntity
/** 0未删除 1已删除 */
private String isDel;
private String sourceSheet;
/** FK prc_price_excel_file.pef_id */
private Long pefId;
private Long tmsId;
private Integer countryCount;
private Integer surchargeCount;
@@ -6,16 +6,22 @@ import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** Redline country detail table redline_product_country */
/** Redline catalog country detail over prc_price_set_detail (price_type=REDLINE) */
@Data
public class BmsRedlineProductCountry implements Serializable
{
private static final long serialVersionUID = 1L;
/** maps to prc_price_set_detail.psd_id */
private Long detailId;
/** FK to product.product_id */
private Long psvId;
private Long pspId;
private String priceType;
/** FK to product.product_id / psp_id */
private Long productId;
private String productCode;
/** Parse/import helper only (not a DB column). */
private String productName;
private String countryName;
private String countryCode;
/** prc_zone.zone_id; null when product country has no zone */
@@ -19,6 +19,8 @@ public class BmsRedlineProductImportTask implements Serializable
private int percent;
private String message;
private String error;
/** Preview parse payload (products / details); not persisted to DB. */
private Object result;
public String getTaskId()
{
@@ -69,4 +71,14 @@ public class BmsRedlineProductImportTask implements Serializable
{
this.error = error;
}
public Object getResult()
{
return result;
}
public void setResult(Object result)
{
this.result = result;
}
}
@@ -0,0 +1,48 @@
package com.bms.price.domain;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* Apply create-page Excel preview into a draft/pending catalog version.
*/
public class CatalogPreviewApplyRequest implements Serializable
{
private static final long serialVersionUID = 1L;
private Long psvId;
private List<BmsProduct> products;
/** key = upper-case productCode */
private Map<String, List<BmsProductCountry>> detailsByProductCode;
public Long getPsvId()
{
return psvId;
}
public void setPsvId(Long psvId)
{
this.psvId = psvId;
}
public List<BmsProduct> getProducts()
{
return products;
}
public void setProducts(List<BmsProduct> products)
{
this.products = products;
}
public Map<String, List<BmsProductCountry>> getDetailsByProductCode()
{
return detailsByProductCode;
}
public void setDetailsByProductCode(Map<String, List<BmsProductCountry>> detailsByProductCode)
{
this.detailsByProductCode = detailsByProductCode;
}
}
@@ -22,6 +22,8 @@ public class PrcPriceExcelFile extends BaseEntity
private Long pefId;
/** PRODUCT / REDLINE / SALES */
private String excelType;
/** FK prc_price_set_version.psv_id */
private Long psvId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date effectiveTime;
private String fileName;
@@ -49,8 +49,10 @@ public class PrcPriceSet extends BaseEntity
/** Y: even if version already has details, delete+re-pull from library */
private String forcePullLibrary;
/**
* Request-only: stamp this Excel attachment id onto prc_price_set_product.pef_id
* when saving IMPORT details (not a column on prc_price_set).
* Request-only: bind this Excel attachment (pef_id) to the saved version.psv_id
* (not a column on prc_price_set).
*/
private Long pefId;
/** 请求专用:多个 Excel 附件 pef_id,保存时绑到版本(非表字段) */
private java.util.List<Long> pefIds;
}
@@ -7,63 +7,57 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 价格套餐明细 prc_price_set_detail */
/** Price detail prc_price_set_detail (PRODUCT / REDLINE / SALES) */
@Data
public class PrcPriceSetDetail implements Serializable
{
private static final long serialVersionUID = 1L;
private Long psdId;
private Long priceSetId;
/** prc_price_set_version.psv_id */
/** PRODUCT / REDLINE / SALES */
private String priceType;
private Long psvId;
/** prc_price_set_product.psp_id */
private Long pspId;
private String productCode;
private String productName;
private String countryCode;
private String countryName;
/** prc_zone.zone_id; null when country has no zone */
private Long zoneId;
/** e.g. Z1; null when no zone */
private String zoneCode;
/** zone display name; null when no zone */
private String zoneName;
private Long zdId;
private Long wsdId;
private String weightLabel;
/** KG inclusive, same as product_country */
private BigDecimal weightMin;
private BigDecimal weightMax;
/** product_country.detail_id */
/** legacy migrate source id */
private Long productCountryId;
private BigDecimal fvValue;
private BigDecimal fvRegistrationFee;
/** product base freight from product_country (display only) */
private String sizeLimit;
private String lastMile;
private String leadTime;
/** display-only base from PRODUCT type when comparing discounts */
private BigDecimal baseFreightPrice;
/** product base registration from product_country (display only) */
private BigDecimal baseRegistrationFee;
private BigDecimal discountValue;
/** UP / DOWN / HALF_UP */
private String roundMode;
private Integer prId;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date effectiveDate;
/** null = open */
@JsonFormat(pattern = "yyyy-MM-dd")
private Date expireDate;
/** SALES rels for this detail (detail_id=psd_id, product_id=psp_id) */
/** 0 not deleted / 1 deleted */
public static final String IS_DEL_NO = "0";
public static final String IS_DEL_YES = "1";
private String isDel;
private List<PrcSurchargeRel> surcharges;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** query only: product/country/zone/weight keyword */
private String keyword;
/** query only: product code/name like */
private String productKeyword;
/** query only: country code/name like */
private String countryKeyword;
/** query only: zone code/name like */
private String zoneKeyword;
}
@@ -6,39 +6,38 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 价格套餐产品主表 prc_price_set_product(与产品库快照独立)
* Price product row prc_price_set_product (PRODUCT / REDLINE / SALES)
*/
@Data
public class PrcPriceSetProduct implements Serializable
{
private static final long serialVersionUID = 1L;
/** 套餐产品主表id */
private Long pspId;
/** 价格套餐id */
private Long priceSetId;
/** 套餐版本id */
/** PRODUCT / REDLINE / SALES */
private String priceType;
private Long psvId;
/** 产品对接代码 */
private String productCode;
/** 产品名称 */
private String productName;
/** 时效等级 */
private String serviceLevel;
/** 走货属性 */
private String cargoAttr;
/** 目的区域 */
private String destRegion;
/** 更新说明 */
private String updateNote;
/** TAX_INCLUDED / TAX_EXCLUDED */
private String taxMode;
/** 来源产品库 product.product_id */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date effectiveTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expireTime;
/** 0 draft / 3 pending / 1 effect / 2 expired */
private String status;
/** legacy migrate source id */
private Long sourceProductId;
/** 来源Excel sheet */
private String sourceSheet;
/** 关联导入价表Excel附件 prc_price_excel_file.pef_id */
private Long pefId;
/** 0 not deleted / 1 deleted */
public static final String IS_DEL_NO = "0";
public static final String IS_DEL_YES = "1";
private String isDel;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@@ -16,6 +16,8 @@ public class PrcPriceSetVersion extends BaseEntity
private Long psvId;
private Long priceSetId;
/** PRODUCT / REDLINE / SALES */
private String priceType;
/** V1 / V2 */
private String versionNo;
private Integer versionSeq;
@@ -34,4 +36,10 @@ public class PrcPriceSetVersion extends BaseEntity
private List<PrcPriceSetDetail> details;
private List<PrcPriceSetProduct> products;
/** query display: active product rows */
private Integer productCount;
/** query display: active detail rows */
private Integer detailCount;
/** query only: pending / expired / effect */
private String statusGroup;
}
@@ -9,10 +9,10 @@ import lombok.EqualsAndHashCode;
* 附加费与产品/明细/套餐关联 prc_surcharge_rel
* 同时携带附加费展示字段(JOIN prc_surcharge)与明细展示字段(LEFT JOIN product_country,仅 PRODUCT/REDLINE)。
*
* price_type 语义:
* PRODUCT / REDLINE : product_id=product.product_id,detail_id=product_country.detail_id(空=产品全部明细)
* SALES : product_id=prc_price_set_product.psp_id,detail_id=prc_price_set_detail.psd_id
* price_type semantics:
* PRODUCT / REDLINE / SALES : product_id=psp_id, detail_id=psd_id (null=all details of product)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PrcSurchargeRel extends BaseEntity
@@ -28,10 +28,10 @@ public class PrcSurchargeRel extends BaseEntity
private Long relId;
private Long scId;
/** PRODUCT/REDLINE=product_id;SALES=psp_id */
/** psp_id for PRODUCT/REDLINE/SALES */
private Long productId;
private String productCode;
/** PRODUCT/REDLINE=product_country.detail_id;SALES=psd_id */
/** psd_id for PRODUCT/REDLINE/SALES (null=all) */
private Long detailId;
/** PRODUCT / SALES / REDLINE */
private String priceType;
@@ -0,0 +1,20 @@
package com.bms.price.domain;
/**
* Unified price type for prc_price_set_version / product / detail.
*/
public final class PriceType
{
public static final String PRODUCT = "PRODUCT";
public static final String REDLINE = "REDLINE";
public static final String SALES = "SALES";
private PriceType()
{
}
public static boolean isCatalog(String priceType)
{
return PRODUCT.equals(priceType) || REDLINE.equals(priceType);
}
}
@@ -0,0 +1,48 @@
package com.bms.price.domain;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
* Apply create-page Excel preview into a draft/pending redline version.
*/
public class RedlineCatalogPreviewApplyRequest implements Serializable
{
private static final long serialVersionUID = 1L;
private Long psvId;
private List<BmsRedlineProduct> products;
/** key = upper-case productCode */
private Map<String, List<BmsRedlineProductCountry>> detailsByProductCode;
public Long getPsvId()
{
return psvId;
}
public void setPsvId(Long psvId)
{
this.psvId = psvId;
}
public List<BmsRedlineProduct> getProducts()
{
return products;
}
public void setProducts(List<BmsRedlineProduct> products)
{
this.products = products;
}
public Map<String, List<BmsRedlineProductCountry>> getDetailsByProductCode()
{
return detailsByProductCode;
}
public void setDetailsByProductCode(Map<String, List<BmsRedlineProductCountry>> detailsByProductCode)
{
this.detailsByProductCode = detailsByProductCode;
}
}
@@ -17,6 +17,8 @@ public interface BmsProductCountryMapper
public int insertBmsProductCountry(BmsProductCountry country);
public int insertBmsProductCountryBatch(@Param("list") List<BmsProductCountry> list);
/** 按明细行主键更新可编辑字段(重量段/运费/挂号费/尺寸/尾程/时效/生效失效日期) */
public int updateBmsProductCountry(BmsProductCountry country);
@@ -65,9 +67,18 @@ public interface BmsProductCountryMapper
@Param("productCode") String productCode,
@Param("effectiveDate") Date effectiveDate);
/** Hard-delete all detail rows of one product (psp_id). */
public int physicalDeleteByPspId(@Param("productId") Long productId);
/** Hard-delete detail rows of one product + country (psp_id scope). */
public int physicalDeleteByPspIdAndCountry(
@Param("productId") Long productId,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName);
/**
* Delete open rows for one product + country on a given effective_date (same-day replace).
* Needed because unique key keeps expired same-day weight bands.
* Hard-delete draft/pending detail rows for one product + country (same-version replace).
* Only versions with status draft(0) or pending(3) are affected.
*/
public int deleteOpenByProductCodeAndCountryOnDate(
@Param("productCode") String productCode,
@@ -75,6 +86,13 @@ public interface BmsProductCountryMapper
@Param("countryName") String countryName,
@Param("effectiveDate") Date effectiveDate);
/** Hard-delete surcharge_rel rows linked to draft/pending details for product+country. */
public int deleteSurchargeRelByProductCodeAndCountryOnDate(
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("effectiveDate") Date effectiveDate);
/** Open country rows (expire_date is null) for product */
public List<BmsProductCountry> selectOpenByProductCode(@Param("productCode") String productCode);
@@ -21,6 +21,9 @@ public interface BmsProductMapper
public BmsProduct selectBmsProductByCodeAndDate(@Param("productCode") String productCode,
@Param("effectiveDate") Date effectiveDate);
/** Product row in a specific version */
public BmsProduct selectByCodeAndPsvId(@Param("productCode") String productCode, @Param("psvId") Long psvId);
/** Product version effective on bizDate */
public BmsProduct selectEffectiveBmsProductByCode(@Param("productCode") String productCode,
@Param("bizDate") Date bizDate);
@@ -17,6 +17,8 @@ public interface BmsRedlineProductCountryMapper
public int insertBmsRedlineProductCountry(BmsRedlineProductCountry country);
public int insertBmsRedlineProductCountryBatch(@Param("list") List<BmsRedlineProductCountry> list);
/** 按明细行主键更新可编辑字段(重量段/运费/挂号费/尺寸/尾程/时效/生效失效日期) */
public int updateBmsRedlineProductCountry(BmsRedlineProductCountry country);
@@ -62,9 +64,18 @@ public interface BmsRedlineProductCountryMapper
@Param("productCode") String productCode,
@Param("effectiveDate") Date effectiveDate);
/** Hard-delete all detail rows of one product (psp_id). */
public int physicalDeleteByPspId(@Param("productId") Long productId);
/** Hard-delete detail rows of one product + country (psp_id scope). */
public int physicalDeleteByPspIdAndCountry(
@Param("productId") Long productId,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName);
/**
* Delete open rows for one product + country on a given effective_date (same-day replace).
* Needed because unique key keeps expired same-day weight bands.
* Hard-delete draft/pending detail rows for one product + country (same-version replace).
* Only versions with status draft(0) or pending(3) are affected.
*/
public int deleteOpenByProductCodeAndCountryOnDate(
@Param("productCode") String productCode,
@@ -72,6 +83,13 @@ public interface BmsRedlineProductCountryMapper
@Param("countryName") String countryName,
@Param("effectiveDate") Date effectiveDate);
/** Hard-delete surcharge_rel rows linked to draft/pending details for product+country. */
public int deleteSurchargeRelByProductCodeAndCountryOnDate(
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("effectiveDate") Date effectiveDate);
/** Open country rows (expire_date is null) for product */
public List<BmsRedlineProductCountry> selectOpenByProductCode(@Param("productCode") String productCode);
@@ -21,6 +21,9 @@ public interface BmsRedlineProductMapper
public BmsRedlineProduct selectBmsRedlineProductByCodeAndDate(@Param("productCode") String productCode,
@Param("effectiveDate") Date effectiveDate);
/** Product row in a specific version */
public BmsRedlineProduct selectByCodeAndPsvId(@Param("productCode") String productCode, @Param("psvId") Long psvId);
/** Product version effective on bizDate */
public BmsRedlineProduct selectEffectiveBmsRedlineProductByCode(@Param("productCode") String productCode,
@Param("bizDate") Date bizDate);
@@ -13,14 +13,12 @@ public interface PrcPriceExcelFileMapper
public List<PrcPriceExcelFile> selectPrcPriceExcelFileList(PrcPriceExcelFile query);
/** Distinct PRODUCT excel files linked by product.pef_id for this product_code */
public List<PrcPriceExcelFile> selectByProductCode(@Param("productCode") String productCode);
public List<PrcPriceExcelFile> selectByRedlineProductCode(@Param("productCode") String productCode);
/** Distinct excel files linked by prc_price_set_product.pef_id for a version. */
/** Excel files bound to a version via psv_id */
public List<PrcPriceExcelFile> selectByPsvId(@Param("psvId") Long psvId);
/** Hard delete attachments of a version. */
public int physicalDeleteByPsvId(@Param("psvId") Long psvId);
public int insertPrcPriceExcelFile(PrcPriceExcelFile row);
public int updatePrcPriceExcelFile(PrcPriceExcelFile row);
@@ -7,9 +7,7 @@ import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PrcPriceSetDetail;
/**
* 价格套餐明细 数据层
*
* @author bms
* Price detail mapper (PRODUCT / REDLINE / SALES)
*/
public interface PrcPriceSetDetailMapper
{
@@ -17,23 +15,24 @@ public interface PrcPriceSetDetailMapper
public List<PrcPriceSetDetail> selectPrcPriceSetDetailList(PrcPriceSetDetail detail);
/** 版本内去重后的 产品/国家/维度组合行(用于明细搜索下拉选项) */
public List<PrcPriceSetDetail> selectDetailOptionRows(@Param("psvId") Long psvId);
public int insertPrcPriceSetDetail(PrcPriceSetDetail detail);
/** Batch insert details; fills generated psdId on each item. */
public int insertPrcPriceSetDetailBatch(@Param("list") List<PrcPriceSetDetail> list);
public int updatePrcPriceSetDetail(PrcPriceSetDetail detail);
public int deletePrcPriceSetDetailByIds(Long[] psdIds);
/** Delete SALES details for all versions of a price set */
public int deletePrcPriceSetDetailByPriceSetId(Long priceSetId);
public int deletePrcPriceSetDetailByPsvId(Long psvId);
/** legacy rows: price_set_id match but psv_id is null */
/** Hard delete all detail rows under a version. */
public int physicalDeleteByPsvId(@Param("psvId") Long psvId);
public int deleteOrphanDetailsByPriceSetId(Long priceSetId);
public int countProductByPriceSetId(Long priceSetId);
@@ -42,32 +41,23 @@ public interface PrcPriceSetDetailMapper
public int countByPsvId(Long psvId);
/** Copy all details from src version into new version (new psd_id, same price_set_id) */
public int copyDetailsFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId);
/** FIXED: insert details from product country library (asOf window or openOnly). */
/** Copy version effective time, expire time and status onto catalog detail rows. */
public int syncEffectByPriceType(@Param("priceType") String priceType);
public int insertFixedDetailsFromLibrary(
@Param("priceSetId") Long priceSetId,
@Param("psvId") Long psvId,
@Param("asOf") Date asOf,
@Param("openOnly") boolean openOnly);
/** Preview: FIXED details from product country library (no insert). */
public List<PrcPriceSetDetail> selectFixedDetailsFromLibrary(
@Param("asOf") Date asOf,
@Param("openOnly") boolean openOnly);
/** DISCOUNT: one detail row per effective product */
public int insertDiscountDetailsFromLibrary(
@Param("priceSetId") Long priceSetId,
@Param("psvId") Long psvId);
public int insertDiscountDetailsFromLibrary(@Param("psvId") Long psvId);
/**
* FIXED match on package snapshot: product + country + zone + weight band.
* Does not depend on live product_country.detail_id.
*/
public PrcPriceSetDetail selectFixedMatch(
@Param("priceSetId") Long priceSetId,
@Param("psvId") Long psvId,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@@ -75,22 +65,16 @@ public interface PrcPriceSetDetailMapper
@Param("zoneCode") String zoneCode,
@Param("weight") BigDecimal weight);
/** Count version details that have zone for product + country (postcode gate). */
public int countZonedByPsvProductCountry(
@Param("psvId") Long psvId,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName);
/** Count FIXED details by version + product (ignore country/zone/weight). */
public int countByPsvProduct(
@Param("psvId") Long psvId,
@Param("productCode") String productCode);
/**
* Count FIXED details by version + product + country + zone (ignore weight).
* Used to diagnose country miss vs weight miss.
*/
public int countByPsvProductCountryZone(
@Param("psvId") Long psvId,
@Param("productCode") String productCode,
@@ -98,15 +82,46 @@ public interface PrcPriceSetDetailMapper
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode);
/** DISCOUNT by product; optional psvId scopes to version */
public PrcPriceSetDetail selectDiscountMatch(
@Param("priceSetId") Long priceSetId,
@Param("psvId") Long psvId,
@Param("productCode") String productCode);
/** Update package freight / registration; null args keep existing columns. */
public int updateDetailPrices(
@Param("psdId") Long psdId,
@Param("fvValue") BigDecimal fvValue,
@Param("fvRegistrationFee") BigDecimal fvRegistrationFee);
public int updatePriceTypeByPsvId(@Param("psvId") Long psvId, @Param("priceType") String priceType);
/**
* Match PRODUCT/REDLINE detail by catalog type + bizDate window on version.
*/
public PrcPriceSetDetail selectCatalogMatch(
@Param("priceType") String priceType,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode,
@Param("weight") BigDecimal weight,
@Param("bizDate") Date bizDate);
public int countCatalogByProduct(
@Param("priceType") String priceType,
@Param("productCode") String productCode,
@Param("bizDate") Date bizDate);
public int countCatalogZonedByProductCountry(
@Param("priceType") String priceType,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("bizDate") Date bizDate);
public int countCatalogByProductCountryZone(
@Param("priceType") String priceType,
@Param("productCode") String productCode,
@Param("countryCode") String countryCode,
@Param("countryName") String countryName,
@Param("zoneCode") String zoneCode,
@Param("bizDate") Date bizDate);
}
@@ -5,7 +5,7 @@ import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PrcPriceSetProduct;
/**
* Package intermediate product mapper
* Price product mapper (PRODUCT / REDLINE / SALES)
*/
public interface PrcPriceSetProductMapper
{
@@ -23,19 +23,21 @@ public interface PrcPriceSetProductMapper
public int deleteByPsvIdAndCode(@Param("psvId") Long psvId, @Param("productCode") String productCode);
/** Copy active library products into this version (ignore duplicates). */
public int insertFromLibrary(@Param("priceSetId") Long priceSetId, @Param("psvId") Long psvId);
/** Hard delete all product rows under a version. */
public int physicalDeleteByPsvId(@Param("psvId") Long psvId);
/** Preview: active library products without writing set-product rows. */
/** Copy active PRODUCT catalog into this SALES version. */
public int insertFromLibrary(@Param("psvId") Long psvId);
/** Preview: active PRODUCT catalog products. */
public List<PrcPriceSetProduct> selectFromLibrary();
/** Copy set products from one version to another. */
public int copyFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId);
/** Bind detail.psp_id from set product by psv + product_code. */
public int bindDetailPspByPsvId(@Param("psvId") Long psvId);
/** Stamp Excel attachment id on set products under this version (optional code filter). */
public int updatePefIdByPsvId(@Param("psvId") Long psvId, @Param("pefId") Long pefId,
@Param("productCodes") List<String> productCodes);
public int updatePriceTypeByPsvId(@Param("psvId") Long psvId, @Param("priceType") String priceType);
/** Copy version effective_time and status onto catalog product rows. */
public int syncEffectByPriceType(@Param("priceType") String priceType);
}
@@ -6,7 +6,7 @@ import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PrcPriceSetVersion;
/**
* Price set version
* Price version (PRODUCT / REDLINE / SALES)
*/
public interface PrcPriceSetVersionMapper
{
@@ -14,8 +14,16 @@ public interface PrcPriceSetVersionMapper
public List<PrcPriceSetVersion> selectPrcPriceSetVersionList(PrcPriceSetVersion version);
/** Catalog versions (price_set_id null) with product/detail counts. */
public List<PrcPriceSetVersion> selectCatalogVersionList(PrcPriceSetVersion version);
/** Max seq for SALES by price_set_id */
public Integer selectMaxVersionSeq(@Param("priceSetId") Long priceSetId);
/** Max seq for catalog type (price_set_id null) or SALES when priceSetId set */
public Integer selectMaxVersionSeqByType(@Param("priceType") String priceType,
@Param("priceSetId") Long priceSetId);
public int insertPrcPriceSetVersion(PrcPriceSetVersion version);
public int updatePrcPriceSetVersion(PrcPriceSetVersion version);
@@ -27,10 +35,18 @@ public interface PrcPriceSetVersionMapper
public List<Long> selectPriceSetIds();
/**
* Version effective at asOf: effective_time &lt;= asOf and (expire_time null or &gt; asOf).
* Prefer latest effective_time.
* SALES version effective at asOf for a price set.
*/
public PrcPriceSetVersion selectActiveByPriceSetAt(
@Param("priceSetId") Long priceSetId,
@Param("asOf") Date asOf);
/**
* Catalog (PRODUCT/REDLINE) version effective at asOf.
*/
public PrcPriceSetVersion selectActiveByTypeAt(
@Param("priceType") String priceType,
@Param("asOf") Date asOf);
public int refreshCatalogStatusByDates(@Param("priceType") String priceType);
}
@@ -35,6 +35,9 @@ public interface PrcSurchargeRelMapper
@Param("productIds") List<Long> productIds,
@Param("priceType") String priceType);
/** Hard delete surcharge rows linked to products/details under a version. */
public int physicalDeleteByPsvId(@Param("psvId") Long psvId);
/** 附加费版本化编辑后,把产品关联从旧记录重指向新记录 */
public int rebindRelScId(@Param("oldScId") Long oldScId, @Param("newScId") Long newScId, @Param("operator") String operator);
}
@@ -6,6 +6,7 @@ import org.springframework.web.multipart.MultipartFile;
import com.bms.price.domain.BmsProduct;
import com.bms.price.domain.BmsProductCountry;
import com.bms.price.domain.BmsProductImportTask;
import com.bms.price.domain.CatalogPreviewApplyRequest;
/**
* Product list service
@@ -61,4 +62,42 @@ public interface IBmsProductService
*/
public java.util.Map<String, List<BmsProductCountry>> parseCountryQuoteExcelByProduct(byte[] bytes, String filename,
String countryCode) throws Exception;
/** Parse multi-country workbook; return product list only (no DB write). */
public java.util.Map<String, Object> previewImportCatalog(byte[] bytes, String filename) throws Exception;
/** Parse single-country workbook; return product list only (no DB write). */
public java.util.Map<String, Object> previewImportCountryCatalog(byte[] bytes, String filename, String countryCode)
throws Exception;
/** Submit async multi-country parse-only preview; poll getImportTask for result. */
public String submitPreviewImportAsync(org.springframework.web.multipart.MultipartFile file) throws Exception;
/** One task parses up to 5 workbooks and merges by product code. */
public String submitPreviewImportAsync(java.util.List<org.springframework.web.multipart.MultipartFile> files)
throws Exception;
/** Submit async single-country parse-only preview; poll getImportTask for result. */
public String submitPreviewImportCountryAsync(org.springframework.web.multipart.MultipartFile file, String countryCode)
throws Exception;
/** One task parses up to 5 single-country workbooks and merges by product code. */
public String submitPreviewImportCountryAsync(java.util.List<org.springframework.web.multipart.MultipartFile> files,
String countryCode) throws Exception;
/** Later workbook overwrites the same product code. */
public java.util.Map<String, Object> mergeCatalogPreview(java.util.Map<String, Object> base,
java.util.Map<String, Object> incoming);
/**
* Persist create-page preview into draft/pending version (no activate, no effectiveDate required).
* Replaces only countries present in detailsByProductCode for each product.
*/
public int applyCatalogPreview(CatalogPreviewApplyRequest request);
/** One product in its own transaction, for parallel apply. */
public int applyOneCatalogProduct(Long psvId, String code, BmsProduct meta, java.util.List<BmsProductCountry> details);
/** Returns taskId; poll import task. Products are saved on a worker pool. */
public String submitApplyCatalogPreviewAsync(CatalogPreviewApplyRequest request);
}
@@ -6,6 +6,7 @@ import org.springframework.web.multipart.MultipartFile;
import com.bms.price.domain.BmsRedlineProduct;
import com.bms.price.domain.BmsRedlineProductCountry;
import com.bms.price.domain.BmsRedlineProductImportTask;
import com.bms.price.domain.RedlineCatalogPreviewApplyRequest;
/**
* redline product list service
@@ -54,4 +55,43 @@ public interface IBmsRedlineProductService
public java.util.Map<String, List<BmsRedlineProductCountry>> parseQuoteExcelByProduct(MultipartFile file) throws Exception;
public java.util.Map<String, List<BmsRedlineProductCountry>> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception;
/** Parse multi-country workbook; return product list only (no DB write). */
public java.util.Map<String, Object> previewImportCatalog(byte[] bytes, String filename) throws Exception;
/** Parse single-country workbook; return product list only (no DB write). */
public java.util.Map<String, Object> previewImportCountryCatalog(byte[] bytes, String filename, String countryCode)
throws Exception;
/** Submit async multi-country parse-only preview; poll getImportTask for result. */
public String submitPreviewImportAsync(org.springframework.web.multipart.MultipartFile file) throws Exception;
/** One task parses up to 5 workbooks and merges by product code. */
public String submitPreviewImportAsync(java.util.List<org.springframework.web.multipart.MultipartFile> files)
throws Exception;
/** Submit async single-country parse-only preview; poll getImportTask for result. */
public String submitPreviewImportCountryAsync(org.springframework.web.multipart.MultipartFile file, String countryCode)
throws Exception;
/** One task parses up to 5 single-country workbooks and merges by product code. */
public String submitPreviewImportCountryAsync(java.util.List<org.springframework.web.multipart.MultipartFile> files,
String countryCode) throws Exception;
/** Later workbook overwrites the same product code. */
public java.util.Map<String, Object> mergeCatalogPreview(java.util.Map<String, Object> base,
java.util.Map<String, Object> incoming);
/**
* Persist create-page preview into draft/pending version (no activate, no effectiveDate required).
* Replaces only countries present in detailsByProductCode for each product.
*/
public int applyCatalogPreview(RedlineCatalogPreviewApplyRequest request);
/** One product in its own transaction, for parallel apply. */
public int applyOneCatalogProduct(Long psvId, String code, BmsRedlineProduct meta,
java.util.List<BmsRedlineProductCountry> details);
/** Returns taskId; poll import task. Products are saved on a worker pool. */
public String submitApplyCatalogPreviewAsync(RedlineCatalogPreviewApplyRequest request);
}
@@ -11,20 +11,19 @@ public interface IPrcPriceExcelFileService
{
public PrcPriceExcelFile selectPrcPriceExcelFileById(Long pefId);
/** Import Excel attachments linked to this product_code via product.pef_id */
public List<PrcPriceExcelFile> selectByProductCode(String productCode);
List<PrcPriceExcelFile> selectByRedlineProductCode(String productCode);
/** Import excel files for a price-set version via prc_price_set_product.psv_id. */
/** Excel files bound to a price version */
List<PrcPriceExcelFile> selectByPsvId(Long psvId);
/**
* Persist uploaded Excel under upload/price/excel and insert metadata row.
*
* @param excelType PRODUCT / REDLINE / SALES
* @param psvId version id (nullable for draft preview; bind later)
* @return inserted row with pefId
*/
public PrcPriceExcelFile saveImportExcel(String excelType, Date effectiveTime, byte[] bytes,
String filename, String operName) throws Exception;
String filename, String operName, Long psvId) throws Exception;
/** Bind an existing attachment row to a version */
public int bindPsvId(Long pefId, Long psvId);
}
@@ -128,6 +128,15 @@ public interface IPrcPriceSetService
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, MultipartFile file, String operName,
boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception;
/** One task imports up to 5 workbooks in order. */
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, List<MultipartFile> files, String operName,
boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception;
/** Parse up to 5 workbooks in one call. Later file wins on the same product+country scope. */
public Map<String, Object> previewImportDetailPricesMany(List<byte[]> bytesList, List<String> names,
boolean updateOnly, List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode)
throws Exception;
/**
* Sync worker: parse bytes and upsert FIXED detail prices matched by
* product + country + zone(if any) + weight band (insert when missing unless updateOnly).
@@ -37,6 +37,7 @@ import com.bms.common.utils.uuid.IdUtils;
import com.bms.price.domain.BmsProduct;
import com.bms.price.domain.BmsProductCountry;
import com.bms.price.domain.BmsProductImportTask;
import com.bms.price.domain.CatalogPreviewApplyRequest;
import com.bms.price.domain.PrcZone;
import com.bms.price.mapper.BmsProductCountryMapper;
import com.bms.price.mapper.BmsProductMapper;
@@ -45,6 +46,9 @@ import com.bms.price.domain.PrcPriceExcelFile;
import com.bms.price.service.IBmsProductService;
import com.bms.price.service.IPrcPriceExcelFileService;
import com.bms.price.service.IPrcSurchargeRelService;
import com.bms.price.domain.PriceType;
import com.bms.price.domain.PrcPriceSetVersion;
import com.bms.price.service.support.CatalogVersionSupport;
import com.bms.price.task.BmsProductImportAsyncRunner;
import com.bms.price.util.ZoneCodeAbbrevs;
import com.bms.system.domain.BmsRegion;
@@ -74,6 +78,9 @@ public class BmsProductServiceImpl implements IBmsProductService
"(\\d+(?:\\.\\d+)?)\\s*[<\\u2264]=?\\s*W\\s*[<\\u2264]=?\\s*(\\d+(?:\\.\\d+)?)",
Pattern.CASE_INSENSITIVE);
@Autowired
private CatalogVersionSupport catalogVersionSupport;
@Autowired
private BmsProductMapper productMapper;
@@ -131,9 +138,12 @@ public class BmsProductServiceImpl implements IBmsProductService
}
applyProductStatus(product);
assertCodeDateUnique(product, null);
product.setPriceType(PriceType.PRODUCT);
Long psvId = catalogVersionSupport.requirePsvId(PriceType.PRODUCT, product.getPsvId(), product.getEffectiveDate());
product.setPsvId(psvId);
int rows = productMapper.insertBmsProduct(product);
productMapper.expireOpenByProductCode(product.getProductCode(), product.getEffectiveDate(), product.getProductId());
productMapper.refreshStatusByDates();
catalogVersionSupport.activateVersion(psvId, product.getEffectiveDate());
return rows;
}
@@ -297,7 +307,7 @@ public class BmsProductServiceImpl implements IBmsProductService
Date effectiveDate) throws Exception
{
validateImportFile(file);
Date eff = requireImportEffectiveDate(effectiveDate);
Date eff = effectiveDate == null ? null : requireImportEffectiveDate(effectiveDate);
byte[] bytes = file.getBytes();
String filename = file.getOriginalFilename();
String taskId = IdUtils.fastSimpleUUID();
@@ -321,7 +331,7 @@ public class BmsProductServiceImpl implements IBmsProductService
String taskId, Date effectiveDate) throws Exception
{
validateImportBytes(bytes, filename);
Date forcedEff = requireImportEffectiveDate(effectiveDate);
Date forcedEff = effectiveDate == null ? null : requireImportEffectiveDate(effectiveDate);
reportImportProgress(taskId, 10, "parsing excel");
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
@@ -354,9 +364,11 @@ public class BmsProductServiceImpl implements IBmsProductService
assertImportProductsNoDupCountryWeight(workbook, products, regionByNameCn, euRegions,
zoneByCountryAndCode);
Long importPsvId = forcedEff == null
? catalogVersionSupport.ensureDraftVersion(PriceType.PRODUCT, null, false).getPsvId()
: catalogVersionSupport.requirePsvId(PriceType.PRODUCT, null, forcedEff);
PrcPriceExcelFile excelFile = priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_PRODUCT, forcedEff, bytes, filename, operName);
Long pefId = excelFile != null ? excelFile.getPefId() : null;
PrcPriceExcelFile.TYPE_PRODUCT, forcedEff, bytes, filename, operName, importPsvId);
int successNum = 0;
int skipNum = 0;
@@ -376,7 +388,10 @@ public class BmsProductServiceImpl implements IBmsProductService
try
{
product.setEffectiveDate(forcedEff);
product.setPefId(pefId);
if (importPsvId != null)
{
product.setPsvId(importPsvId);
}
int[] nums = importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
zoneByCountryAndCode, unmatchedCountries, forcedEff);
transactionManager.commit(tx);
@@ -432,7 +447,7 @@ public class BmsProductServiceImpl implements IBmsProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
}
Date eff = requireImportEffectiveDate(effectiveDate);
Date eff = effectiveDate == null ? null : requireImportEffectiveDate(effectiveDate);
byte[] bytes = file.getBytes();
String filename = file.getOriginalFilename();
String taskId = IdUtils.fastSimpleUUID();
@@ -446,7 +461,7 @@ public class BmsProductServiceImpl implements IBmsProductService
String taskId, Date effectiveDate) throws Exception
{
validateImportBytes(bytes, filename);
Date forcedEff = requireImportEffectiveDate(effectiveDate);
Date forcedEff = effectiveDate == null ? null : requireImportEffectiveDate(effectiveDate);
if (StringUtils.isEmpty(countryCode))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
@@ -477,9 +492,11 @@ public class BmsProductServiceImpl implements IBmsProductService
// Pre-check all products: reject whole import before any write if any is earlier than prev version
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
Long importPsvId = forcedEff == null
? catalogVersionSupport.ensureDraftVersion(PriceType.PRODUCT, null, false).getPsvId()
: catalogVersionSupport.requirePsvId(PriceType.PRODUCT, null, forcedEff);
PrcPriceExcelFile excelFile = priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_PRODUCT, forcedEff, bytes, filename, operName);
Long pefId = excelFile != null ? excelFile.getPefId() : null;
PrcPriceExcelFile.TYPE_PRODUCT, forcedEff, bytes, filename, operName, importPsvId);
int successNum = 0;
int skipNum = 0;
@@ -508,7 +525,10 @@ public class BmsProductServiceImpl implements IBmsProductService
product.setDestRegion(countryLabel);
product.setSourceSheet(block.sheetName);
product.setEffectiveDate(forcedEff);
product.setPefId(pefId);
if (importPsvId != null)
{
product.setPsvId(importPsvId);
}
if (StringUtils.isEmpty(product.getTaxMode()))
{
product.setTaxMode(resolveTaxMode(product.getProductName()));
@@ -608,6 +628,12 @@ public class BmsProductServiceImpl implements IBmsProductService
@Override
public Map<String, List<BmsProductCountry>> parseQuoteExcelByProduct(byte[] bytes, String filename) throws Exception
{
return readQuoteWorkbook(bytes, filename).byCode;
}
/** One workbook open; each price sheet is parsed once. */
private QuoteWorkbook readQuoteWorkbook(byte[] bytes, String filename) throws Exception
{
if (bytes == null || bytes.length == 0)
{
@@ -630,13 +656,7 @@ public class BmsProductServiceImpl implements IBmsProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
}
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
// Fail whole import when any price sheet has duplicate country+weight (incl. unmatched sheets).
assertAllPriceSheetsNoDupCountryWeight(workbook, regionByNameCn, euRegions, zoneByCountryAndCode);
Set<String> unmatchedCountries = new HashSet<>();
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<>();
Map<String, List<BmsProduct>> ownersBySheet = new LinkedHashMap<String, List<BmsProduct>>();
for (BmsProduct product : products)
{
if (product == null || StringUtils.isEmpty(product.getProductCode()))
@@ -648,24 +668,89 @@ public class BmsProductServiceImpl implements IBmsProductService
{
continue;
}
List<BmsProductCountry> rows = parseProductSheet(productSheet, product,
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
if (rows == null || rows.isEmpty())
List<BmsProduct> owners = ownersBySheet.get(productSheet.getSheetName());
if (owners == null)
{
owners = new ArrayList<BmsProduct>();
ownersBySheet.put(productSheet.getSheetName(), owners);
}
owners.add(product);
}
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
Set<String> unmatchedCountries = new HashSet<String>();
Map<String, List<BmsProductCountry>> out = new LinkedHashMap<String, List<BmsProductCountry>>();
List<String> tips = new ArrayList<String>();
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
String sheetName = workbook.getSheetName(i);
if (shouldSkipCountryPriceSheet(sheetName))
{
continue;
}
assertNoDuplicateCountryWeightRows(product, rows);
String code = product.getProductCode().trim().toUpperCase(Locale.ROOT);
out.put(code, rows);
Sheet sheet = workbook.getSheetAt(i);
if (sheet == null || findHeaderCell(sheet, "\u56fd\u5bb6") == null)
{
continue;
}
List<BmsProduct> owners = ownersBySheet.get(sheetName);
BmsProduct parseAs = (owners == null || owners.isEmpty()) ? null : owners.get(0);
if (parseAs == null)
{
parseAs = new BmsProduct();
parseAs.setProductCode(sheetName);
parseAs.setProductName(sheetName);
}
List<BmsProductCountry> rows = parseProductSheet(sheet, parseAs,
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
String tip = buildDuplicateCountryWeightTip(parseAs, rows);
if (StringUtils.isNotEmpty(tip))
{
tips.add(tip);
}
if (owners == null || owners.isEmpty() || rows == null || rows.isEmpty())
{
continue;
}
for (int p = 0; p < owners.size(); p++)
{
BmsProduct owner = owners.get(p);
List<BmsProductCountry> use = rows;
if (p > 0)
{
use = parseProductSheet(sheet, owner,
regionByNameCn, euRegions, zoneByCountryAndCode, unmatchedCountries);
}
if (use == null || use.isEmpty())
{
continue;
}
out.put(owner.getProductCode().trim().toUpperCase(Locale.ROOT), use);
}
}
if (!tips.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.dupCountry",
String.join("<br/>", tips)));
}
if (out.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
}
return out;
QuoteWorkbook parsed = new QuoteWorkbook();
parsed.products = products;
parsed.byCode = out;
return parsed;
}
}
private static final class QuoteWorkbook
{
private List<BmsProduct> products;
private Map<String, List<BmsProductCountry>> byCode;
}
@Override
public Map<String, List<BmsProductCountry>> parseCountryQuoteExcelByProduct(byte[] bytes, String filename,
String countryCode) throws Exception
@@ -727,6 +812,415 @@ public class BmsProductServiceImpl implements IBmsProductService
}
}
@Override
public Map<String, Object> previewImportCatalog(byte[] bytes, String filename) throws Exception
{
QuoteWorkbook parsed = readQuoteWorkbook(bytes, filename);
return buildCatalogPreview(parsed.products, parsed.byCode);
}
@Override
public Map<String, Object> previewImportCountryCatalog(byte[] bytes, String filename, String countryCode)
throws Exception
{
Map<String, List<BmsProductCountry>> byCode = parseCountryQuoteExcelByProduct(bytes, filename, countryCode);
String countryLabel = resolveCountryLabel(countryCode);
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
{
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
List<BmsProduct> catalogProducts = dedupeCatalogProducts(parseCountryCatalog(catalog));
if (StringUtils.isNotEmpty(countryLabel) && catalogProducts != null)
{
for (BmsProduct p : catalogProducts)
{
if (p != null)
{
p.setDestRegion(countryLabel);
}
}
}
return buildCatalogPreview(catalogProducts, byCode);
}
}
@Override
public String submitPreviewImportAsync(MultipartFile file) throws Exception
{
return submitPreviewImportAsync(java.util.Collections.singletonList(file));
}
@Override
public String submitPreviewImportAsync(List<MultipartFile> files) throws Exception
{
List<byte[]> bytesList = new ArrayList<byte[]>();
List<String> names = new ArrayList<String>();
readPreviewFiles(files, bytesList, names);
String taskId = IdUtils.fastSimpleUUID();
importAsyncRunner.put(taskId, BmsProductImportTask.STATUS_PENDING, 0, "queued", null);
importAsyncRunner.runPreviewAll(taskId, bytesList, names);
return taskId;
}
@Override
public String submitPreviewImportCountryAsync(MultipartFile file, String countryCode) throws Exception
{
return submitPreviewImportCountryAsync(java.util.Collections.singletonList(file), countryCode);
}
@Override
public String submitPreviewImportCountryAsync(List<MultipartFile> files, String countryCode) throws Exception
{
if (StringUtils.isEmpty(countryCode))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
}
List<byte[]> bytesList = new ArrayList<byte[]>();
List<String> names = new ArrayList<String>();
readPreviewFiles(files, bytesList, names);
String taskId = IdUtils.fastSimpleUUID();
importAsyncRunner.put(taskId, BmsProductImportTask.STATUS_PENDING, 0, "queued", null);
importAsyncRunner.runPreviewCountry(taskId, bytesList, names, countryCode.trim());
return taskId;
}
private void readPreviewFiles(List<MultipartFile> files, List<byte[]> bytesList, List<String> names) throws Exception
{
if (files == null || files.isEmpty())
{
throw new ServiceException("file required");
}
if (files.size() > com.bms.price.service.support.UploadFileBatch.MAX_FILES)
{
throw new ServiceException("\u5355\u6b21\u6700\u591a\u5bfc\u5165 5 \u4e2a\u6587\u4ef6");
}
for (MultipartFile file : files)
{
validateImportFile(file);
bytesList.add(file.getBytes());
names.add(file.getOriginalFilename());
}
}
@Override
public Map<String, Object> mergeCatalogPreview(Map<String, Object> base, Map<String, Object> incoming)
{
if (base == null || base.isEmpty())
{
return incoming;
}
if (incoming == null || incoming.isEmpty())
{
return base;
}
Map<String, BmsProduct> products = new LinkedHashMap<String, BmsProduct>();
putPreviewProducts(products, castProductList(base.get("products")));
putPreviewProducts(products, castProductList(incoming.get("products")));
Map<String, List<BmsProductCountry>> details = new LinkedHashMap<String, List<BmsProductCountry>>();
putPreviewDetails(details, castDetailMap(base.get("detailsByProductCode")));
putPreviewDetails(details, castDetailMap(incoming.get("detailsByProductCode")));
int detailCount = 0;
for (List<BmsProductCountry> rows : details.values())
{
if (rows != null)
{
detailCount += rows.size();
}
}
Map<String, Object> out = new HashMap<String, Object>();
out.put("products", new ArrayList<BmsProduct>(products.values()));
out.put("productCount", Integer.valueOf(products.size()));
out.put("detailCount", Integer.valueOf(detailCount));
out.put("detailsByProductCode", details);
return out;
}
@SuppressWarnings("unchecked")
private static List<BmsProduct> castProductList(Object value)
{
if (value instanceof List)
{
return (List<BmsProduct>) value;
}
return null;
}
@SuppressWarnings("unchecked")
private static Map<String, List<BmsProductCountry>> castDetailMap(Object value)
{
if (value instanceof Map)
{
return (Map<String, List<BmsProductCountry>>) value;
}
return null;
}
private static void putPreviewProducts(Map<String, BmsProduct> products, List<BmsProduct> list)
{
if (list == null)
{
return;
}
for (BmsProduct p : list)
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
products.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
}
}
private static void putPreviewDetails(Map<String, List<BmsProductCountry>> details,
Map<String, List<BmsProductCountry>> incoming)
{
if (incoming == null)
{
return;
}
for (Map.Entry<String, List<BmsProductCountry>> e : incoming.entrySet())
{
if (e == null || StringUtils.isEmpty(e.getKey()) || e.getValue() == null)
{
continue;
}
details.put(e.getKey().trim().toUpperCase(Locale.ROOT), e.getValue());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public int applyCatalogPreview(CatalogPreviewApplyRequest request)
{
if (request == null || request.getPsvId() == null)
{
throw new ServiceException("psvId required");
}
Long psvId = request.getPsvId();
catalogVersionSupport.requireEditableCatalogVersion(psvId, PriceType.PRODUCT);
Map<String, List<BmsProductCountry>> byCode = request.getDetailsByProductCode();
if (byCode == null || byCode.isEmpty())
{
return 0;
}
Map<String, BmsProduct> productMeta = new LinkedHashMap<String, BmsProduct>();
if (request.getProducts() != null)
{
for (BmsProduct p : request.getProducts())
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
productMeta.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
}
}
int detailRows = 0;
for (Map.Entry<String, List<BmsProductCountry>> e : byCode.entrySet())
{
if (e == null || StringUtils.isEmpty(e.getKey()) || e.getValue() == null || e.getValue().isEmpty())
{
continue;
}
String code = e.getKey().trim().toUpperCase(Locale.ROOT);
detailRows += applyOneCatalogProduct(psvId, code, productMeta.get(code), e.getValue());
}
return detailRows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int applyOneCatalogProduct(Long psvId, String code, BmsProduct meta, List<BmsProductCountry> details)
{
if (psvId == null || StringUtils.isEmpty(code) || details == null || details.isEmpty())
{
return 0;
}
String key = code.trim().toUpperCase(Locale.ROOT);
PrcPriceSetVersion ver = catalogVersionSupport.findVersion(psvId);
Date eff = ver == null ? null : ver.getEffectiveTime();
Date exp = ver == null ? null : ver.getExpireTime();
String st = ver == null || StringUtils.isEmpty(ver.getStatus()) ? "0" : ver.getStatus();
String destRegion = resolveDestRegion(meta == null ? null : meta.getDestRegion(), details);
BmsProduct existing = productMapper.selectByCodeAndPsvId(key, psvId);
if (existing == null)
{
BmsProduct row = new BmsProduct();
row.setPsvId(psvId);
row.setPriceType(PriceType.PRODUCT);
row.setProductCode(meta != null && StringUtils.isNotEmpty(meta.getProductCode())
? meta.getProductCode().trim() : key);
row.setProductName(meta == null ? null : meta.getProductName());
row.setServiceLevel(meta == null ? null : meta.getServiceLevel());
row.setCargoAttr(meta == null ? null : meta.getCargoAttr());
row.setDestRegion(destRegion);
row.setTaxMode(meta != null && StringUtils.isNotEmpty(meta.getTaxMode())
? meta.getTaxMode() : "TAX_INCLUDED");
row.setEffectiveDate(eff);
row.setExpireDate(exp);
row.setStatus(st);
row.setIsDel(BmsProduct.IS_DEL_NO);
productMapper.insertBmsProduct(row);
existing = row;
}
else
{
if (meta != null)
{
if (StringUtils.isNotEmpty(meta.getProductName()))
{
existing.setProductName(meta.getProductName());
}
if (StringUtils.isNotEmpty(meta.getServiceLevel()))
{
existing.setServiceLevel(meta.getServiceLevel());
}
if (StringUtils.isNotEmpty(meta.getCargoAttr()))
{
existing.setCargoAttr(meta.getCargoAttr());
}
}
if (StringUtils.isNotEmpty(destRegion))
{
existing.setDestRegion(destRegion);
}
existing.setEffectiveDate(eff);
existing.setExpireDate(exp);
existing.setStatus(st);
productMapper.updateBmsProduct(existing);
}
Set<String> replaced = new HashSet<String>();
List<BmsProductCountry> toInsert = new ArrayList<BmsProductCountry>();
for (BmsProductCountry d : details)
{
if (d == null)
{
continue;
}
String ckey = countryIdentityKey(d);
if (StringUtils.isNotEmpty(ckey) && !replaced.contains(ckey))
{
countryMapper.physicalDeleteByPspIdAndCountry(existing.getProductId(), d.getCountryCode(),
d.getCountryName());
replaced.add(ckey);
}
d.setDetailId(null);
d.setProductId(existing.getProductId());
d.setPsvId(psvId);
d.setProductCode(existing.getProductCode());
if (StringUtils.isEmpty(d.getProductName()))
{
d.setProductName(existing.getProductName());
}
d.setEffectiveDate(eff);
d.setExpireDate(exp);
d.setStatus(st);
d.setIsDel(BmsProduct.IS_DEL_NO);
toInsert.add(d);
}
insertProductCountryChunks(toInsert);
return toInsert.size();
}
@Override
public String submitApplyCatalogPreviewAsync(CatalogPreviewApplyRequest request)
{
if (request == null || request.getPsvId() == null)
{
throw new ServiceException("psvId required");
}
catalogVersionSupport.requireEditableCatalogVersion(request.getPsvId(), PriceType.PRODUCT);
String taskId = IdUtils.fastSimpleUUID();
importAsyncRunner.put(taskId, BmsProductImportTask.STATUS_PENDING, 0, "queued", null);
importAsyncRunner.runApplyPreview(taskId, request);
return taskId;
}
private void insertProductCountryChunks(List<BmsProductCountry> rows)
{
if (rows == null || rows.isEmpty())
{
return;
}
int size = 200;
for (int i = 0; i < rows.size(); i += size)
{
int end = Math.min(i + size, rows.size());
countryMapper.insertBmsProductCountryBatch(new ArrayList<BmsProductCountry>(rows.subList(i, end)));
}
}
private String resolveCountryLabel(String countryCode)
{
if (StringUtils.isEmpty(countryCode))
{
return null;
}
BmsRegion region = regionMapper.selectRegionByCode2(countryCode.trim().toUpperCase(Locale.ROOT));
if (region == null)
{
return countryCode.trim().toUpperCase(Locale.ROOT);
}
return StringUtils.isNotEmpty(region.getCountryNameCn()) ? region.getCountryNameCn()
: region.getCountryCode2();
}
private String resolveDestRegion(String preferred, List<BmsProductCountry> details)
{
if (StringUtils.isNotEmpty(preferred))
{
return preferred.trim();
}
if (details == null)
{
return null;
}
for (BmsProductCountry d : details)
{
if (d != null && StringUtils.isNotEmpty(d.getCountryName()))
{
return d.getCountryName().trim();
}
}
return null;
}
private Map<String, Object> buildCatalogPreview(List<BmsProduct> catalogProducts,
Map<String, List<BmsProductCountry>> byCode)
{
List<BmsProduct> products = new ArrayList<BmsProduct>();
int detailCount = 0;
if (catalogProducts != null)
{
for (BmsProduct src : catalogProducts)
{
if (src == null || StringUtils.isEmpty(src.getProductCode()))
{
continue;
}
String code = src.getProductCode().trim().toUpperCase(Locale.ROOT);
List<BmsProductCountry> details = byCode == null ? null : byCode.get(code);
if (details == null || details.isEmpty())
{
continue;
}
BmsProduct row = new BmsProduct();
row.setProductCode(src.getProductCode());
row.setProductName(src.getProductName());
row.setServiceLevel(src.getServiceLevel());
row.setCargoAttr(src.getCargoAttr());
row.setDestRegion(src.getDestRegion());
products.add(row);
detailCount += details.size();
}
}
Map<String, Object> out = new HashMap<String, Object>();
out.put("products", products);
out.put("productCount", Integer.valueOf(products.size()));
out.put("detailCount", Integer.valueOf(detailCount));
out.put("detailsByProductCode", byCode == null ? new LinkedHashMap<String, List<BmsProductCountry>>() : byCode);
return out;
}
/** @return int[]{added, skipped, countryRows} */
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
@@ -753,22 +1247,28 @@ public class BmsProductServiceImpl implements IBmsProductService
}
assertNoDuplicateCountryWeightRows(product, countries);
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
Date newEff = requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
: product.getEffectiveDate());
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
? null
: requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
: product.getEffectiveDate());
if (newEff != null)
{
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
}
BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(
product.getProductCode(), newEff);
if (existing != null)
{
product.setProductId(existing.getProductId());
patchProductPefId(product, operName);
int countryRows = saveCountries(product, countries);
return new int[] { 0, 1, countryRows };
}
applyProductStatus(product);
product.setPriceType(PriceType.PRODUCT);
product.setPsvId(catalogVersionSupport.requirePsvId(PriceType.PRODUCT, product.getPsvId(), newEff));
try
{
productMapper.insertBmsProduct(product);
@@ -781,26 +1281,22 @@ public class BmsProductServiceImpl implements IBmsProductService
}
throw e;
}
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
productMapper.refreshStatusByDates();
if (newEff != null)
{
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
if (product.getPsvId() != null)
{
catalogVersionSupport.activateVersion(product.getPsvId(), newEff);
}
}
else
{
productMapper.refreshStatusByDates();
}
int countryRows = saveCountries(product, countries);
return new int[] { 1, 0, countryRows };
}
private void patchProductPefId(BmsProduct product, String operName)
{
if (product == null || product.getProductId() == null || product.getPefId() == null)
{
return;
}
BmsProduct pefPatch = new BmsProduct();
pefPatch.setProductId(product.getProductId());
pefPatch.setPefId(product.getPefId());
pefPatch.setUpdateBy(operName);
productMapper.updateBmsProduct(pefPatch);
}
/**
* Save country rows by effective_date.
* For each product_code + country in the Excel:
@@ -842,6 +1338,8 @@ public class BmsProductServiceImpl implements IBmsProductService
{
countryMapper.expireOpenByProductCodeAndCountry(
productCode, incoming.getCountryCode(), incoming.getCountryName(), newEff);
countryMapper.deleteSurchargeRelByProductCodeAndCountryOnDate(
productCode, incoming.getCountryCode(), incoming.getCountryName(), newEff);
countryMapper.deleteOpenByProductCodeAndCountryOnDate(
productCode, incoming.getCountryCode(), incoming.getCountryName(), newEff);
replacedCountries.add(countryKey);
@@ -2127,6 +2625,10 @@ public class BmsProductServiceImpl implements IBmsProductService
/** Batch pre-check: fail whole import before any DB write. */
private void assertImportProductsEffectiveNotBeforePrev(List<BmsProduct> products, Date forcedEff)
{
if (forcedEff == null)
{
return;
}
Date newEff = requireImportEffectiveDate(forcedEff);
if (products == null || products.isEmpty())
{
@@ -2346,18 +2848,22 @@ public class BmsProductServiceImpl implements IBmsProductService
{
product.setSourceSheet(product.getProductName());
}
Date newEff = requireImportEffectiveDate(product.getEffectiveDate());
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
Date newEff = product.getEffectiveDate() == null ? null : requireImportEffectiveDate(product.getEffectiveDate());
if (newEff != null)
{
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
}
BmsProduct existing = productMapper.selectBmsProductByCodeAndDate(product.getProductCode(), newEff);
if (existing != null)
{
product.setProductId(existing.getProductId());
patchProductPefId(product, operName);
int countryRows = saveCountries(product, countries);
return new int[] { 0, 1, countryRows };
}
applyProductStatus(product);
product.setPriceType(PriceType.PRODUCT);
product.setPsvId(catalogVersionSupport.requirePsvId(PriceType.PRODUCT, product.getPsvId(), newEff));
try
{
productMapper.insertBmsProduct(product);
@@ -2370,8 +2876,18 @@ public class BmsProductServiceImpl implements IBmsProductService
}
throw e;
}
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
productMapper.refreshStatusByDates();
if (newEff != null)
{
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
if (product.getPsvId() != null)
{
catalogVersionSupport.activateVersion(product.getPsvId(), newEff);
}
}
else
{
productMapper.refreshStatusByDates();
}
int countryRows = saveCountries(product, countries);
return new int[] { 1, 0, countryRows };
}
@@ -37,6 +37,7 @@ import com.bms.common.utils.uuid.IdUtils;
import com.bms.price.domain.BmsRedlineProduct;
import com.bms.price.domain.BmsRedlineProductCountry;
import com.bms.price.domain.BmsRedlineProductImportTask;
import com.bms.price.domain.RedlineCatalogPreviewApplyRequest;
import com.bms.price.domain.PrcZone;
import com.bms.price.domain.PrcSurchargeRel;
import com.bms.price.mapper.BmsRedlineProductCountryMapper;
@@ -46,6 +47,9 @@ import com.bms.price.mapper.PrcZoneMapper;
import com.bms.price.domain.PrcPriceExcelFile;
import com.bms.price.service.IBmsRedlineProductService;
import com.bms.price.service.IPrcPriceExcelFileService;
import com.bms.price.domain.PriceType;
import com.bms.price.domain.PrcPriceSetVersion;
import com.bms.price.service.support.CatalogVersionSupport;
import com.bms.price.task.BmsRedlineProductImportAsyncRunner;
import com.bms.price.util.ZoneCodeAbbrevs;
import com.bms.system.domain.BmsRegion;
@@ -75,6 +79,9 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
"(\\d+(?:\\.\\d+)?)\\s*[<\\u2264]=?\\s*W\\s*[<\\u2264]=?\\s*(\\d+(?:\\.\\d+)?)",
Pattern.CASE_INSENSITIVE);
@Autowired
private CatalogVersionSupport catalogVersionSupport;
@Autowired
private BmsRedlineProductMapper productMapper;
@@ -132,9 +139,12 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
}
applyProductStatus(product);
assertCodeDateUnique(product, null);
product.setPriceType(PriceType.REDLINE);
Long psvId = catalogVersionSupport.requirePsvId(PriceType.REDLINE, product.getPsvId(), product.getEffectiveDate());
product.setPsvId(psvId);
int rows = productMapper.insertBmsRedlineProduct(product);
productMapper.expireOpenByProductCode(product.getProductCode(), product.getEffectiveDate(), product.getProductId());
productMapper.refreshStatusByDates();
catalogVersionSupport.activateVersion(psvId, product.getEffectiveDate());
return rows;
}
@@ -290,7 +300,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
Date effectiveDate) throws Exception
{
validateImportFile(file);
Date eff = requireImportEffectiveDate(effectiveDate);
Date eff = effectiveDate == null ? null : requireImportEffectiveDate(effectiveDate);
byte[] bytes = file.getBytes();
String filename = file.getOriginalFilename();
String taskId = IdUtils.fastSimpleUUID();
@@ -314,7 +324,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
String taskId, Date effectiveDate) throws Exception
{
validateImportBytes(bytes, filename);
Date forcedEff = requireImportEffectiveDate(effectiveDate);
Date forcedEff = effectiveDate == null ? null : requireImportEffectiveDate(effectiveDate);
reportImportProgress(taskId, 10, "parsing excel");
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
@@ -346,9 +356,11 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
assertImportProductsNoDupCountryWeight(workbook, products, regionByNameCn, euRegions,
zoneByCountryAndCode);
Long importPsvId = forcedEff == null
? catalogVersionSupport.ensureDraftVersion(PriceType.REDLINE, null, false).getPsvId()
: catalogVersionSupport.requirePsvId(PriceType.REDLINE, null, forcedEff);
PrcPriceExcelFile excelFile = priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_REDLINE, forcedEff, bytes, filename, operName);
Long pefId = excelFile != null ? excelFile.getPefId() : null;
PrcPriceExcelFile.TYPE_REDLINE, forcedEff, bytes, filename, operName, importPsvId);
int successNum = 0;
int skipNum = 0;
@@ -368,7 +380,10 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
try
{
product.setEffectiveDate(forcedEff);
product.setPefId(pefId);
if (importPsvId != null)
{
product.setPsvId(importPsvId);
}
int[] nums = importOneProduct(workbook, product, operName, regionByNameCn, euRegions,
zoneByCountryAndCode, unmatchedCountries, forcedEff);
transactionManager.commit(tx);
@@ -424,7 +439,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
}
Date eff = requireImportEffectiveDate(effectiveDate);
Date eff = effectiveDate == null ? null : requireImportEffectiveDate(effectiveDate);
byte[] bytes = file.getBytes();
String filename = file.getOriginalFilename();
String taskId = IdUtils.fastSimpleUUID();
@@ -438,7 +453,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
String taskId, Date effectiveDate) throws Exception
{
validateImportBytes(bytes, filename);
Date forcedEff = requireImportEffectiveDate(effectiveDate);
Date forcedEff = effectiveDate == null ? null : requireImportEffectiveDate(effectiveDate);
if (StringUtils.isEmpty(countryCode))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
@@ -469,9 +484,11 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
// Pre-check all products: reject whole import before any write if any is earlier than prev version
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
Long importPsvId = forcedEff == null
? catalogVersionSupport.ensureDraftVersion(PriceType.REDLINE, null, false).getPsvId()
: catalogVersionSupport.requirePsvId(PriceType.REDLINE, null, forcedEff);
PrcPriceExcelFile excelFile = priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_REDLINE, forcedEff, bytes, filename, operName);
Long pefId = excelFile != null ? excelFile.getPefId() : null;
PrcPriceExcelFile.TYPE_REDLINE, forcedEff, bytes, filename, operName, importPsvId);
int successNum = 0;
int skipNum = 0;
@@ -500,7 +517,10 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
product.setDestRegion(countryLabel);
product.setSourceSheet(block.sheetName);
product.setEffectiveDate(forcedEff);
product.setPefId(pefId);
if (importPsvId != null)
{
product.setPsvId(importPsvId);
}
if (StringUtils.isEmpty(product.getTaxMode()))
{
product.setTaxMode(resolveTaxMode(product.getProductName()));
@@ -656,6 +676,454 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
}
}
@Override
public Map<String, Object> previewImportCatalog(byte[] bytes, String filename) throws Exception
{
Map<String, List<BmsRedlineProductCountry>> byCode = parseQuoteExcelByProduct(bytes, filename);
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
{
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
List<BmsRedlineProduct> catalogProducts = dedupeCatalogProducts(parseCatalog(catalog));
return buildCatalogPreview(catalogProducts, byCode);
}
}
@Override
public String submitPreviewImportAsync(MultipartFile file) throws Exception
{
return submitPreviewImportAsync(java.util.Collections.singletonList(file));
}
@Override
public String submitPreviewImportAsync(List<MultipartFile> files) throws Exception
{
List<byte[]> bytesList = new ArrayList<byte[]>();
List<String> names = new ArrayList<String>();
readPreviewFiles(files, bytesList, names);
String taskId = IdUtils.fastSimpleUUID();
importAsyncRunner.put(taskId, BmsRedlineProductImportTask.STATUS_PENDING, 0, "queued", null);
importAsyncRunner.runPreviewAll(taskId, bytesList, names);
return taskId;
}
@Override
public String submitPreviewImportCountryAsync(MultipartFile file, String countryCode) throws Exception
{
return submitPreviewImportCountryAsync(java.util.Collections.singletonList(file), countryCode);
}
@Override
public String submitPreviewImportCountryAsync(List<MultipartFile> files, String countryCode) throws Exception
{
if (StringUtils.isEmpty(countryCode))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
}
List<byte[]> bytesList = new ArrayList<byte[]>();
List<String> names = new ArrayList<String>();
readPreviewFiles(files, bytesList, names);
String taskId = IdUtils.fastSimpleUUID();
importAsyncRunner.put(taskId, BmsRedlineProductImportTask.STATUS_PENDING, 0, "queued", null);
importAsyncRunner.runPreviewCountry(taskId, bytesList, names, countryCode.trim());
return taskId;
}
private void readPreviewFiles(List<MultipartFile> files, List<byte[]> bytesList, List<String> names) throws Exception
{
if (files == null || files.isEmpty())
{
throw new ServiceException("file required");
}
if (files.size() > com.bms.price.service.support.UploadFileBatch.MAX_FILES)
{
throw new ServiceException("\u5355\u6b21\u6700\u591a\u5bfc\u5165 5 \u4e2a\u6587\u4ef6");
}
for (MultipartFile file : files)
{
validateImportFile(file);
bytesList.add(file.getBytes());
names.add(file.getOriginalFilename());
}
}
@Override
public Map<String, Object> mergeCatalogPreview(Map<String, Object> base, Map<String, Object> incoming)
{
if (base == null || base.isEmpty())
{
return incoming;
}
if (incoming == null || incoming.isEmpty())
{
return base;
}
Map<String, BmsRedlineProduct> products = new LinkedHashMap<String, BmsRedlineProduct>();
putPreviewProducts(products, castProductList(base.get("products")));
putPreviewProducts(products, castProductList(incoming.get("products")));
Map<String, List<BmsRedlineProductCountry>> details = new LinkedHashMap<String, List<BmsRedlineProductCountry>>();
putPreviewDetails(details, castDetailMap(base.get("detailsByProductCode")));
putPreviewDetails(details, castDetailMap(incoming.get("detailsByProductCode")));
int detailCount = 0;
for (List<BmsRedlineProductCountry> rows : details.values())
{
if (rows != null)
{
detailCount += rows.size();
}
}
Map<String, Object> out = new HashMap<String, Object>();
out.put("products", new ArrayList<BmsRedlineProduct>(products.values()));
out.put("productCount", Integer.valueOf(products.size()));
out.put("detailCount", Integer.valueOf(detailCount));
out.put("detailsByProductCode", details);
return out;
}
@SuppressWarnings("unchecked")
private static List<BmsRedlineProduct> castProductList(Object value)
{
if (value instanceof List)
{
return (List<BmsRedlineProduct>) value;
}
return null;
}
@SuppressWarnings("unchecked")
private static Map<String, List<BmsRedlineProductCountry>> castDetailMap(Object value)
{
if (value instanceof Map)
{
return (Map<String, List<BmsRedlineProductCountry>>) value;
}
return null;
}
private static void putPreviewProducts(Map<String, BmsRedlineProduct> products, List<BmsRedlineProduct> list)
{
if (list == null)
{
return;
}
for (BmsRedlineProduct p : list)
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
products.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
}
}
private static void putPreviewDetails(Map<String, List<BmsRedlineProductCountry>> details,
Map<String, List<BmsRedlineProductCountry>> incoming)
{
if (incoming == null)
{
return;
}
for (Map.Entry<String, List<BmsRedlineProductCountry>> e : incoming.entrySet())
{
if (e == null || StringUtils.isEmpty(e.getKey()) || e.getValue() == null)
{
continue;
}
details.put(e.getKey().trim().toUpperCase(Locale.ROOT), e.getValue());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public int applyCatalogPreview(RedlineCatalogPreviewApplyRequest request)
{
if (request == null || request.getPsvId() == null)
{
throw new ServiceException("psvId required");
}
Long psvId = request.getPsvId();
catalogVersionSupport.requireEditableCatalogVersion(psvId, PriceType.REDLINE);
Map<String, List<BmsRedlineProductCountry>> byCode = request.getDetailsByProductCode();
if (byCode == null || byCode.isEmpty())
{
return 0;
}
Map<String, BmsRedlineProduct> productMeta = new LinkedHashMap<String, BmsRedlineProduct>();
if (request.getProducts() != null)
{
for (BmsRedlineProduct p : request.getProducts())
{
if (p == null || StringUtils.isEmpty(p.getProductCode()))
{
continue;
}
productMeta.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
}
}
int detailRows = 0;
for (Map.Entry<String, List<BmsRedlineProductCountry>> e : byCode.entrySet())
{
if (e == null || StringUtils.isEmpty(e.getKey()) || e.getValue() == null || e.getValue().isEmpty())
{
continue;
}
String code = e.getKey().trim().toUpperCase(Locale.ROOT);
detailRows += applyOneCatalogProduct(psvId, code, productMeta.get(code), e.getValue());
}
return detailRows;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int applyOneCatalogProduct(Long psvId, String code, BmsRedlineProduct meta, List<BmsRedlineProductCountry> details)
{
if (psvId == null || StringUtils.isEmpty(code) || details == null || details.isEmpty())
{
return 0;
}
String key = code.trim().toUpperCase(Locale.ROOT);
PrcPriceSetVersion ver = catalogVersionSupport.findVersion(psvId);
Date eff = ver == null ? null : ver.getEffectiveTime();
Date exp = ver == null ? null : ver.getExpireTime();
String st = ver == null || StringUtils.isEmpty(ver.getStatus()) ? "0" : ver.getStatus();
String destRegion = resolveDestRegion(meta == null ? null : meta.getDestRegion(), details);
BmsRedlineProduct existing = productMapper.selectByCodeAndPsvId(key, psvId);
if (existing == null)
{
BmsRedlineProduct row = new BmsRedlineProduct();
row.setPsvId(psvId);
row.setPriceType(PriceType.REDLINE);
row.setProductCode(meta != null && StringUtils.isNotEmpty(meta.getProductCode())
? meta.getProductCode().trim() : key);
row.setProductName(meta == null ? null : meta.getProductName());
row.setServiceLevel(meta == null ? null : meta.getServiceLevel());
row.setCargoAttr(meta == null ? null : meta.getCargoAttr());
row.setDestRegion(destRegion);
row.setTaxMode(meta != null && StringUtils.isNotEmpty(meta.getTaxMode())
? meta.getTaxMode() : "TAX_INCLUDED");
row.setEffectiveDate(eff);
row.setExpireDate(exp);
row.setStatus(st);
row.setIsDel(BmsRedlineProduct.IS_DEL_NO);
productMapper.insertBmsRedlineProduct(row);
existing = row;
}
else
{
if (meta != null)
{
if (StringUtils.isNotEmpty(meta.getProductName()))
{
existing.setProductName(meta.getProductName());
}
if (StringUtils.isNotEmpty(meta.getServiceLevel()))
{
existing.setServiceLevel(meta.getServiceLevel());
}
if (StringUtils.isNotEmpty(meta.getCargoAttr()))
{
existing.setCargoAttr(meta.getCargoAttr());
}
}
if (StringUtils.isNotEmpty(destRegion))
{
existing.setDestRegion(destRegion);
}
existing.setEffectiveDate(eff);
existing.setExpireDate(exp);
existing.setStatus(st);
productMapper.updateBmsRedlineProduct(existing);
}
Set<String> replaced = new HashSet<String>();
List<BmsRedlineProductCountry> toInsert = new ArrayList<BmsRedlineProductCountry>();
for (BmsRedlineProductCountry d : details)
{
if (d == null)
{
continue;
}
String ckey = countryIdentityKey(d);
if (StringUtils.isNotEmpty(ckey) && !replaced.contains(ckey))
{
countryMapper.physicalDeleteByPspIdAndCountry(existing.getProductId(), d.getCountryCode(),
d.getCountryName());
replaced.add(ckey);
}
d.setDetailId(null);
d.setProductId(existing.getProductId());
d.setPsvId(psvId);
d.setProductCode(existing.getProductCode());
if (StringUtils.isEmpty(d.getProductName()))
{
d.setProductName(existing.getProductName());
}
d.setEffectiveDate(eff);
d.setExpireDate(exp);
d.setStatus(st);
d.setIsDel(BmsRedlineProduct.IS_DEL_NO);
toInsert.add(d);
}
insertRedlineCountryChunks(toInsert);
return toInsert.size();
}
@Override
public String submitApplyCatalogPreviewAsync(RedlineCatalogPreviewApplyRequest request)
{
if (request == null || request.getPsvId() == null)
{
throw new ServiceException("psvId required");
}
catalogVersionSupport.requireEditableCatalogVersion(request.getPsvId(), PriceType.REDLINE);
String taskId = IdUtils.fastSimpleUUID();
importAsyncRunner.put(taskId, BmsRedlineProductImportTask.STATUS_PENDING, 0, "queued", null);
importAsyncRunner.runApplyPreview(taskId, request);
return taskId;
}
private void insertRedlineCountryChunks(List<BmsRedlineProductCountry> rows)
{
if (rows == null || rows.isEmpty())
{
return;
}
int size = 200;
for (int i = 0; i < rows.size(); i += size)
{
int end = Math.min(i + size, rows.size());
countryMapper.insertBmsRedlineProductCountryBatch(new ArrayList<BmsRedlineProductCountry>(rows.subList(i, end)));
}
}
@Override
public Map<String, Object> previewImportCountryCatalog(byte[] bytes, String filename, String countryCode)
throws Exception
{
validateImportBytes(bytes, filename);
if (StringUtils.isEmpty(countryCode))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryRequired"));
}
BmsRegion region = regionMapper.selectRegionByCode2(countryCode.trim().toUpperCase(Locale.ROOT));
if (region == null || "2".equals(region.getDelFlag())
|| (StringUtils.isNotEmpty(region.getStatus()) && !"0".equals(region.getStatus())))
{
throw new ServiceException(MessageUtils.message("price.product.import.countryNotFound", countryCode));
}
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
{
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
if (catalog == null)
{
throw new ServiceException(MessageUtils.message("price.product.import.countrySheetNotFound"));
}
List<BmsRedlineProduct> products = dedupeCatalogProducts(parseCountryCatalog(catalog));
if (products.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.noData"));
}
Map<String, PriceBlock> priceBlocks = parseCountryPriceBlocks(workbook);
Map<String, List<BmsRedlineProductCountry>> byCode = new LinkedHashMap<String, List<BmsRedlineProductCountry>>();
for (BmsRedlineProduct product : products)
{
if (product == null || StringUtils.isEmpty(product.getProductCode()))
{
continue;
}
PriceBlock block = findPriceBlock(product.getProductName(), priceBlocks);
if (block == null || block.rows == null || block.rows.isEmpty())
{
continue;
}
List<BmsRedlineProductCountry> countries = stampSelectedCountry(block.rows, region, null);
for (BmsRedlineProductCountry c : countries)
{
if (c == null)
{
continue;
}
c.setProductCode(product.getProductCode());
c.setProductName(product.getProductName());
}
assertNoDuplicateCountryWeightRows(product, countries);
byCode.put(product.getProductCode().trim().toUpperCase(Locale.ROOT), countries);
}
if (byCode.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.set.import.noPriceRows"));
}
String countryLabel = StringUtils.isNotEmpty(region.getCountryNameCn())
? region.getCountryNameCn() : region.getCountryCode2();
if (StringUtils.isNotEmpty(countryLabel))
{
for (BmsRedlineProduct p : products)
{
if (p != null)
{
p.setDestRegion(countryLabel);
}
}
}
return buildCatalogPreview(products, byCode);
}
}
private String resolveDestRegion(String preferred, List<BmsRedlineProductCountry> details)
{
if (StringUtils.isNotEmpty(preferred))
{
return preferred.trim();
}
if (details == null)
{
return null;
}
for (BmsRedlineProductCountry d : details)
{
if (d != null && StringUtils.isNotEmpty(d.getCountryName()))
{
return d.getCountryName().trim();
}
}
return null;
}
private Map<String, Object> buildCatalogPreview(List<BmsRedlineProduct> catalogProducts,
Map<String, List<BmsRedlineProductCountry>> byCode)
{
List<BmsRedlineProduct> products = new ArrayList<BmsRedlineProduct>();
int detailCount = 0;
if (catalogProducts != null)
{
for (BmsRedlineProduct src : catalogProducts)
{
if (src == null || StringUtils.isEmpty(src.getProductCode()))
{
continue;
}
String code = src.getProductCode().trim().toUpperCase(Locale.ROOT);
List<BmsRedlineProductCountry> details = byCode == null ? null : byCode.get(code);
if (details == null || details.isEmpty())
{
continue;
}
BmsRedlineProduct row = new BmsRedlineProduct();
row.setProductCode(src.getProductCode());
row.setProductName(src.getProductName());
row.setServiceLevel(src.getServiceLevel());
row.setCargoAttr(src.getCargoAttr());
row.setDestRegion(src.getDestRegion());
products.add(row);
detailCount += details.size();
}
}
Map<String, Object> out = new HashMap<String, Object>();
out.put("products", products);
out.put("productCount", Integer.valueOf(products.size()));
out.put("detailCount", Integer.valueOf(detailCount));
out.put("detailsByProductCode", byCode == null ? new LinkedHashMap<String, List<BmsRedlineProductCountry>>() : byCode);
return out;
}
/** @return int[]{added, skipped, countryRows} */
private int[] importOneProduct(Workbook workbook, BmsRedlineProduct product, String operName,
@@ -682,22 +1150,28 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
}
assertNoDuplicateCountryWeightRows(product, countries);
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
Date newEff = requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
: product.getEffectiveDate());
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
? null
: requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
: product.getEffectiveDate());
if (newEff != null)
{
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
}
BmsRedlineProduct existing = productMapper.selectBmsRedlineProductByCodeAndDate(
product.getProductCode(), newEff);
if (existing != null)
{
product.setProductId(existing.getProductId());
patchProductPefId(product, operName);
int countryRows = saveCountries(product, countries);
return new int[] { 0, 1, countryRows };
}
applyProductStatus(product);
product.setPriceType(PriceType.REDLINE);
product.setPsvId(catalogVersionSupport.requirePsvId(PriceType.REDLINE, product.getPsvId(), newEff));
try
{
productMapper.insertBmsRedlineProduct(product);
@@ -710,26 +1184,22 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
}
throw e;
}
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
productMapper.refreshStatusByDates();
if (newEff != null)
{
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
if (product.getPsvId() != null)
{
catalogVersionSupport.activateVersion(product.getPsvId(), newEff);
}
}
else
{
productMapper.refreshStatusByDates();
}
int countryRows = saveCountries(product, countries);
return new int[] { 1, 0, countryRows };
}
private void patchProductPefId(BmsRedlineProduct product, String operName)
{
if (product == null || product.getProductId() == null || product.getPefId() == null)
{
return;
}
BmsRedlineProduct pefPatch = new BmsRedlineProduct();
pefPatch.setProductId(product.getProductId());
pefPatch.setPefId(product.getPefId());
pefPatch.setUpdateBy(operName);
productMapper.updateBmsRedlineProduct(pefPatch);
}
/**
* Save country rows by effective_date.
* For each product_code + country in the Excel:
@@ -771,6 +1241,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
{
countryMapper.expireOpenByProductCodeAndCountry(
productCode, incoming.getCountryCode(), incoming.getCountryName(), newEff);
countryMapper.deleteSurchargeRelByProductCodeAndCountryOnDate(
productCode, incoming.getCountryCode(), incoming.getCountryName(), newEff);
countryMapper.deleteOpenByProductCodeAndCountryOnDate(
productCode, incoming.getCountryCode(), incoming.getCountryName(), newEff);
replacedCountries.add(countryKey);
@@ -2035,6 +2507,10 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
/** Batch pre-check: fail whole import before any DB write. */
private void assertImportProductsEffectiveNotBeforePrev(List<BmsRedlineProduct> products, Date forcedEff)
{
if (forcedEff == null)
{
return;
}
Date newEff = requireImportEffectiveDate(forcedEff);
if (products == null || products.isEmpty())
{
@@ -2209,18 +2685,22 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
{
product.setSourceSheet(product.getProductName());
}
Date newEff = requireImportEffectiveDate(product.getEffectiveDate());
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
Date newEff = product.getEffectiveDate() == null ? null : requireImportEffectiveDate(product.getEffectiveDate());
if (newEff != null)
{
applyForcedEffectiveDate(product, countries, newEff);
assertImportEffectiveNotBeforePrev(product, newEff);
}
BmsRedlineProduct existing = productMapper.selectBmsRedlineProductByCodeAndDate(product.getProductCode(), newEff);
if (existing != null)
{
product.setProductId(existing.getProductId());
patchProductPefId(product, operName);
int countryRows = saveCountries(product, countries);
return new int[] { 0, 1, countryRows };
}
applyProductStatus(product);
product.setPriceType(PriceType.REDLINE);
product.setPsvId(catalogVersionSupport.requirePsvId(PriceType.REDLINE, product.getPsvId(), newEff));
try
{
productMapper.insertBmsRedlineProduct(product);
@@ -2233,8 +2713,18 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
}
throw e;
}
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
productMapper.refreshStatusByDates();
if (newEff != null)
{
productMapper.expireOpenByProductCode(product.getProductCode(), newEff, product.getProductId());
if (product.getPsvId() != null)
{
catalogVersionSupport.activateVersion(product.getPsvId(), newEff);
}
}
else
{
productMapper.refreshStatusByDates();
}
int countryRows = saveCountries(product, countries);
return new int[] { 1, 0, countryRows };
}
@@ -37,28 +37,6 @@ public class PrcPriceExcelFileServiceImpl implements IPrcPriceExcelFileService
return excelFileMapper.selectPrcPriceExcelFileById(pefId);
}
@Override
public List<PrcPriceExcelFile> selectByProductCode(String productCode)
{
if (StringUtils.isEmpty(productCode))
{
return Collections.emptyList();
}
List<PrcPriceExcelFile> list = excelFileMapper.selectByProductCode(productCode.trim());
return list == null ? Collections.emptyList() : list;
}
@Override
public List<PrcPriceExcelFile> selectByRedlineProductCode(String productCode)
{
if (productCode == null || productCode.trim().isEmpty())
{
return java.util.Collections.emptyList();
}
List<PrcPriceExcelFile> list = excelFileMapper.selectByRedlineProductCode(productCode.trim());
return list == null ? java.util.Collections.emptyList() : list;
}
@Override
public List<PrcPriceExcelFile> selectByPsvId(Long psvId)
{
@@ -72,7 +50,7 @@ public class PrcPriceExcelFileServiceImpl implements IPrcPriceExcelFileService
@Override
public PrcPriceExcelFile saveImportExcel(String excelType, Date effectiveTime, byte[] bytes,
String filename, String operName) throws Exception
String filename, String operName, Long psvId) throws Exception
{
if (StringUtils.isEmpty(excelType))
{
@@ -111,6 +89,7 @@ public class PrcPriceExcelFileServiceImpl implements IPrcPriceExcelFileService
PrcPriceExcelFile row = new PrcPriceExcelFile();
row.setExcelType(excelType.trim().toUpperCase(Locale.ROOT));
row.setPsvId(psvId);
row.setEffectiveTime(effectiveTime);
row.setFileName(originalName);
row.setFilePath(storedPath);
@@ -122,6 +101,19 @@ public class PrcPriceExcelFileServiceImpl implements IPrcPriceExcelFileService
return row;
}
@Override
public int bindPsvId(Long pefId, Long psvId)
{
if (pefId == null || psvId == null)
{
return 0;
}
PrcPriceExcelFile row = new PrcPriceExcelFile();
row.setPefId(pefId);
row.setPsvId(psvId);
return excelFileMapper.updatePrcPriceExcelFile(row);
}
private static String resolveContentType(String ext)
{
if ("xls".equals(ext))
@@ -399,6 +399,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
int curSeq = version.getVersionSeq() == null ? 0 : version.getVersionSeq().intValue();
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(version.getPriceSetId());
q.setPriceType(com.bms.price.domain.PriceType.SALES);
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
if (list == null || list.isEmpty())
{
@@ -455,6 +456,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
int seq = maxSeq == null ? 1 : maxSeq.intValue() + 1;
PrcPriceSetVersion nv = new PrcPriceSetVersion();
nv.setPriceSetId(set.getPriceSetId());
nv.setPriceType(com.bms.price.domain.PriceType.SALES);
nv.setVersionSeq(Integer.valueOf(seq));
nv.setVersionNo("V" + seq);
nv.setStatus(VER_DRAFT);
@@ -492,6 +494,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
int seq = maxSeq == null ? 1 : maxSeq.intValue() + 1;
PrcPriceSetVersion nv = new PrcPriceSetVersion();
nv.setPriceSetId(set.getPriceSetId());
nv.setPriceType(com.bms.price.domain.PriceType.SALES);
nv.setVersionSeq(Integer.valueOf(seq));
nv.setVersionNo("V" + seq);
nv.setStatus(VER_DRAFT);
@@ -908,10 +911,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
@Override
public int insertDetail(PrcPriceSetDetail detail)
{
PrcPriceSet db = requireSet(detail.getPriceSetId());
Long psvId = detail.getPsvId();
Long priceSetId = resolvePriceSetIdByPsvId(psvId);
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
detail.setPriceType(com.bms.price.domain.PriceType.SALES);
detail.setPsvId(resolvePsvId(priceSetId, psvId));
validateDetail(db, detail);
detail.setPsvId(resolvePsvId(detail.getPriceSetId(), detail.getPsvId()));
detail.setPspId(lookupSetProductId(detail.getPsvId(), detail.getProductCode()));
int rows = detailMapper.insertPrcPriceSetDetail(detail);
if (detail.getSurcharges() != null)
@@ -955,14 +961,52 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
throw new ServiceException(MessageUtils.message("price.product.import.fileType"));
}
byte[] bytes = file.getBytes();
return submitImportDetailPricesAsync(priceSetId, psvId, java.util.Collections.singletonList(file), operName,
updateOnly, replaceByProductCountry, countryCode);
}
@Override
public String submitImportDetailPricesAsync(Long priceSetId, Long psvId, List<MultipartFile> files, String operName,
boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception
{
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
if (!TYPE_FIXED.equals(db.getSetType()))
{
throw new ServiceException(MessageUtils.message("price.set.import.fixedOnly"));
}
if (files == null || files.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty"));
}
if (files.size() > com.bms.price.service.support.UploadFileBatch.MAX_FILES)
{
throw new ServiceException("\u5355\u6b21\u6700\u591a\u5bfc\u5165 5 \u4e2a\u6587\u4ef6");
}
List<byte[]> bytesList = new ArrayList<byte[]>();
List<String> names = new ArrayList<String>();
for (MultipartFile file : files)
{
if (file == null || file.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.product.import.fileEmpty"));
}
String filename = file.getOriginalFilename();
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")
|| filename.toLowerCase(Locale.ROOT).endsWith(".xls")))
{
throw new ServiceException(MessageUtils.message("price.product.import.fileType"));
}
bytesList.add(file.getBytes());
names.add(filename);
}
if (psvId == null)
{
psvId = resolveCurrentPsvId(db);
}
String taskId = IdUtils.fastSimpleUUID();
saveAsyncRunner.put(taskId, PrcPriceSetSaveTask.STATUS_PENDING, 0, "queued", priceSetId, null);
saveAsyncRunner.runImportPrices(taskId, priceSetId, psvId, bytes, filename, operName, updateOnly,
saveAsyncRunner.runImportPricesFiles(taskId, priceSetId, psvId, bytesList, names, operName, updateOnly,
replaceByProductCountry, countryCode);
return taskId;
}
@@ -1093,7 +1137,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
operName = null;
}
PrcPriceExcelFile excelFile = priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_SALES, new Date(), bytes, filename, operName);
PrcPriceExcelFile.TYPE_SALES, new Date(), bytes, filename, operName, null);
Long pefId = excelFile != null ? excelFile.getPefId() : null;
Map<String, PrcPriceSetProduct> productMap = new LinkedHashMap<String, PrcPriceSetProduct>();
@@ -1210,22 +1254,6 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
throw new ServiceException(MessageUtils.message("price.set.import.noDetailWritten"));
}
if (pefId != null)
{
for (String code : byProduct.keySet())
{
if (StringUtils.isEmpty(code))
{
continue;
}
PrcPriceSetProduct sp = productMap.get(code.trim().toUpperCase(Locale.ROOT));
if (sp != null)
{
sp.setPefId(pefId);
}
}
}
if (replaceScope)
{
for (PrcPriceSetDetail d : details)
@@ -1253,6 +1281,46 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return result;
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> previewImportDetailPricesMany(List<byte[]> bytesList, List<String> names,
boolean updateOnly, List<PrcPriceSetDetail> existingDetails, boolean replaceByProductCountry, String countryCode)
throws Exception
{
if (bytesList == null || bytesList.isEmpty())
{
throw new ServiceException("file required");
}
if (bytesList.size() > com.bms.price.service.support.UploadFileBatch.MAX_FILES)
{
throw new ServiceException("\u5355\u6b21\u6700\u591a\u5bfc\u5165 5 \u4e2a\u6587\u4ef6");
}
List<PrcPriceSetDetail> existing = existingDetails;
Map<String, Object> last = null;
List<Long> pefIds = new ArrayList<Long>();
for (int i = 0; i < bytesList.size(); i++)
{
String filename = names != null && i < names.size() ? names.get(i) : null;
last = previewImportDetailPrices(bytesList.get(i), filename, updateOnly, existing, replaceByProductCountry,
countryCode);
Object details = last == null ? null : last.get("details");
if (details instanceof List)
{
existing = (List<PrcPriceSetDetail>) details;
}
Object pefId = last == null ? null : last.get("pefId");
if (pefId instanceof Number)
{
pefIds.add(Long.valueOf(((Number) pefId).longValue()));
}
}
if (last != null)
{
last.put("pefIds", pefIds);
}
return last;
}
private Map<String, List<BmsProductCountry>> parseSetImportExcel(byte[] bytes, String filename, String countryCode)
throws Exception
{
@@ -1385,7 +1453,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
PrcPriceSetDetail q = new PrcPriceSetDetail();
q.setPriceSetId(priceSetId);
q.setPriceType(com.bms.price.domain.PriceType.SALES);
q.setPsvId(psvId);
List<PrcPriceSetDetail> details = detailMapper.selectPrcPriceSetDetailList(q);
if (details == null)
@@ -1424,9 +1492,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
PrcPriceSetVersion verForFile = versionMapper.selectPrcPriceSetVersionById(psvId);
Date excelEff = verForFile != null && verForFile.getEffectiveTime() != null
? verForFile.getEffectiveTime() : new Date();
PrcPriceExcelFile excelFile = priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_SALES, excelEff, bytes, filename, operName);
Long pefId = excelFile != null ? excelFile.getPefId() : null;
priceExcelFileService.saveImportExcel(
PrcPriceExcelFile.TYPE_SALES, excelEff, bytes, filename, operName, psvId);
int updated = 0;
int inserted = 0;
@@ -1461,7 +1528,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
Long pspId = null;
if (!updateOnly)
{
pspId = ensureSetProduct(priceSetId, psvId, productCode, productName, pefId);
pspId = ensureSetProduct(priceSetId, psvId, productCode, productName);
}
for (BmsProductCountry row : e.getValue())
{
@@ -1525,11 +1592,6 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
setProductMapper.bindDetailPspByPsvId(psvId);
}
if (pefId != null && (inserted > 0 || updated > 0))
{
List<String> codes = new ArrayList<String>(byProduct.keySet());
setProductMapper.updatePefIdByPsvId(psvId, pefId, codes);
}
reportImportProgress(taskId, priceSetId, 95, "finishing");
if (inserted == 0 && updated == 0)
{
@@ -1576,7 +1638,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
String productName, BmsProductCountry row, BigDecimal freight, BigDecimal registration)
{
PrcPriceSetDetail d = new PrcPriceSetDetail();
d.setPriceSetId(priceSetId);
d.setPriceType(com.bms.price.domain.PriceType.SALES);
d.setPsvId(psvId);
d.setProductCode(trimToLen(productCode, 20));
d.setProductName(trimToLen(productName, 50));
@@ -1636,6 +1698,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(set.getPriceSetId());
q.setPriceType(com.bms.price.domain.PriceType.SALES);
versions = versionMapper.selectPrcPriceSetVersionList(q);
}
if (versions == null || versions.isEmpty())
@@ -2011,13 +2074,14 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
throw new ServiceException(MessageUtils.message("price.set.detail.notExists"));
}
PrcPriceSet db = requireSet(old.getPriceSetId());
Long priceSetId = resolvePriceSetIdByPsvId(old.getPsvId());
PrcPriceSet db = requireSet(priceSetId);
assertChildEditable(db);
detail.setPriceSetId(old.getPriceSetId());
if (detail.getPsvId() == null)
{
detail.setPsvId(old.getPsvId());
}
detail.setPriceType(com.bms.price.domain.PriceType.SALES);
validateDetail(db, detail);
Long pspId = lookupSetProductId(detail.getPsvId(), detail.getProductCode());
if (pspId == null)
@@ -2045,7 +2109,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
continue;
}
PrcPriceSet db = requireSet(old.getPriceSetId());
PrcPriceSet db = requireSet(resolvePriceSetIdByPsvId(old.getPsvId()));
assertChildEditable(db);
if (old.getPsvId() != null && StringUtils.isNotEmpty(old.getProductCode()))
{
@@ -2817,6 +2881,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
}
private Long resolvePriceSetIdByPsvId(Long psvId)
{
if (psvId == null)
{
return null;
}
PrcPriceSetVersion v = versionMapper.selectPrcPriceSetVersionById(psvId);
return v == null ? null : v.getPriceSetId();
}
private PrcPriceSet requireSet(Long priceSetId)
{
// Header only: avoid count(detail) waiting on long pull/import locks.
@@ -2850,6 +2924,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(priceSetId);
q.setPriceType(com.bms.price.domain.PriceType.SALES);
List<PrcPriceSetVersion> versions = versionMapper.selectPrcPriceSetVersionList(q);
PrcPriceSetVersion current = pickCurrentVersion(versions);
if (current == null)
@@ -2896,6 +2971,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
Long priceSetId = set.getPriceSetId();
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(priceSetId);
q.setPriceType(com.bms.price.domain.PriceType.SALES);
List<PrcPriceSetVersion> versions = versionMapper.selectPrcPriceSetVersionList(q);
if (versions == null)
{
@@ -2960,6 +3036,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
// Draft versions may omit effective_time; set on activate
version.setPriceSetId(set.getPriceSetId());
version.setPriceType(com.bms.price.domain.PriceType.SALES);
version.setCreateBy(set.getCreateBy());
version.setUpdateBy(set.getUpdateBy());
// Reuse existing version when client omitted psvId (avoids V2/V3 + leftover details)
@@ -3002,6 +3079,28 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
versionMapper.updatePrcPriceSetVersion(version);
}
set.setVersion(version);
if (version.getPsvId() != null)
{
java.util.LinkedHashSet<Long> excelIds = new java.util.LinkedHashSet<Long>();
if (set.getPefIds() != null)
{
for (Long excelId : set.getPefIds())
{
if (excelId != null)
{
excelIds.add(excelId);
}
}
}
if (set.getPefId() != null)
{
excelIds.add(set.getPefId());
}
for (Long excelId : excelIds)
{
priceExcelFileService.bindPsvId(excelId, version.getPsvId());
}
}
if (detailsProvided)
{
upsertVersionDetails(set, version, details);
@@ -3077,6 +3176,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(priceSetId);
q.setPriceType(com.bms.price.domain.PriceType.SALES);
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
if (list == null || list.isEmpty())
{
@@ -3122,6 +3222,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(priceSetId);
q.setPriceType(com.bms.price.domain.PriceType.SALES);
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
if (list == null || list.isEmpty())
{
@@ -3169,7 +3270,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
setProductMapper.deleteByPsvId(version.getPsvId());
// clean legacy rows without psv_id under this set (would look like duplicates in UI)
detailMapper.deleteOrphanDetailsByPriceSetId(set.getPriceSetId());
int productRows = setProductMapper.insertFromLibrary(set.getPriceSetId(), version.getPsvId());
int productRows = setProductMapper.insertFromLibrary(version.getPsvId());
if (productRows <= 0)
{
throw new ServiceException(MessageUtils.message("price.set.library.empty"));
@@ -3178,18 +3279,16 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
int inserted;
if (TYPE_DISCOUNT.equals(set.getSetType()))
{
inserted = detailMapper.insertDiscountDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId());
inserted = detailMapper.insertDiscountDetailsFromLibrary(version.getPsvId());
}
else
{
// Prefer currently effective open country rows; fallback asOf window if needed
inserted = detailMapper.insertFixedDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId(), null, true);
inserted = detailMapper.insertFixedDetailsFromLibrary(version.getPsvId(), null, true);
if (inserted <= 0)
{
inserted = detailMapper.insertFixedDetailsFromLibrary(
set.getPriceSetId(), version.getPsvId(), new Date(), false);
version.getPsvId(), new Date(), false);
}
}
if (inserted <= 0)
@@ -3267,10 +3366,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
for (PrcPriceSetDetail d : details)
{
validateDetail(set, d);
d.setPriceSetId(set.getPriceSetId());
d.setPsvId(version.getPsvId());
d.setPsvId(version.getPsvId());
d.setPriceType(com.bms.price.domain.PriceType.SALES);
Long pspId = ensureSetProduct(set.getPriceSetId(), version.getPsvId(),
d.getProductCode(), d.getProductName(), set.getPefId());
d.getProductCode(), d.getProductName());
if (pspId != null)
{
d.setPspId(pspId);
@@ -3391,12 +3490,6 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
* Uses request-scoped cache during bulk upsert to avoid N selects on same code.
*/
private Long ensureSetProduct(Long priceSetId, Long psvId, String productCode, String productNameHint)
{
return ensureSetProduct(priceSetId, psvId, productCode, productNameHint, null);
}
private Long ensureSetProduct(Long priceSetId, Long psvId, String productCode, String productNameHint,
Long pefId)
{
if (psvId == null || StringUtils.isEmpty(productCode))
{
@@ -3407,13 +3500,6 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
Map<String, Long> cache = setProductCacheTl.get();
if (cache != null && cache.containsKey(key))
{
if (pefId != null)
{
PrcPriceSetProduct pefPatch = new PrcPriceSetProduct();
pefPatch.setPspId(cache.get(key));
pefPatch.setPefId(pefId);
setProductMapper.updatePrcPriceSetProduct(pefPatch);
}
return cache.get(key);
}
PrcPriceSetProduct existing = setProductMapper.selectByPsvIdAndCode(psvId, code);
@@ -3426,11 +3512,6 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
existing.setProductName(productNameHint);
needUpdate = true;
}
if (pefId != null && (existing.getPefId() == null || !pefId.equals(existing.getPefId())))
{
existing.setPefId(pefId);
needUpdate = true;
}
if (needUpdate)
{
setProductMapper.updatePrcPriceSetProduct(existing);
@@ -3442,10 +3523,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
return existing.getPspId();
}
PrcPriceSetProduct row = new PrcPriceSetProduct();
row.setPriceSetId(priceSetId);
row.setPriceType(com.bms.price.domain.PriceType.SALES);
row.setPsvId(psvId);
row.setProductCode(code);
row.setPefId(pefId);
BmsProduct lib = null;
try
{
@@ -3792,6 +3872,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
}
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(priceSetId);
q.setPriceType(com.bms.price.domain.PriceType.SALES);
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
if (list == null)
{
@@ -3815,6 +3896,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
{
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceSetId(priceSetId);
q.setPriceType(com.bms.price.domain.PriceType.SALES);
List<PrcPriceSetVersion> versions = versionMapper.selectPrcPriceSetVersionList(q);
if (versions == null || versions.isEmpty())
{
@@ -3914,15 +3996,18 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
public int refreshAllVersionStatus()
{
List<Long> ids = versionMapper.selectPriceSetIds();
if (ids == null || ids.isEmpty())
int n = 0;
if (ids != null)
{
return 0;
for (Long id : ids)
{
refreshVersionStatus(id);
n++;
}
}
for (Long id : ids)
{
refreshVersionStatus(id);
}
return ids.size();
n += versionMapper.refreshCatalogStatusByDates(com.bms.price.domain.PriceType.PRODUCT);
n += versionMapper.refreshCatalogStatusByDates(com.bms.price.domain.PriceType.REDLINE);
return n;
}
private static boolean sameTime(Date a, Date b)
@@ -923,7 +923,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
}
PrcPriceSetDetail detail = detailMapper.selectFixedMatch(
set.getPriceSetId(), psvId, productCode,
psvId, productCode,
countryCode, countryName, zoneCode, weight);
if (detail == null)
{
@@ -954,7 +954,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
PrcPriceSetDetail detail = null;
if (detailCount > 0)
{
detail = detailMapper.selectDiscountMatch(set.getPriceSetId(), psvId, productCode);
detail = detailMapper.selectDiscountMatch(psvId, productCode);
if (detail == null)
{
return null;
@@ -0,0 +1,415 @@
package com.bms.price.service.support;
import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.PriceType;
import com.bms.price.domain.PrcPriceSetVersion;
import com.bms.price.mapper.PrcPriceExcelFileMapper;
import com.bms.price.mapper.PrcPriceSetProductMapper;
import com.bms.price.mapper.PrcPriceSetDetailMapper;
import com.bms.price.mapper.PrcPriceSetVersionMapper;
import com.bms.price.mapper.PrcSurchargeRelMapper;
/**
* Resolve / create global PRODUCT or REDLINE versions (price_set_id null).
*/
@Component
public class CatalogVersionSupport
{
public static final String STATUS_DRAFT = "0";
public static final String STATUS_EFFECT = "1";
public static final String STATUS_EXPIRED = "2";
public static final String STATUS_PENDING = "3";
@Autowired
private PrcPriceSetVersionMapper versionMapper;
@Autowired
private PrcPriceSetProductMapper productMapper;
@Autowired
private PrcPriceSetDetailMapper detailMapper;
@Autowired
private PrcSurchargeRelMapper surchargeRelMapper;
@Autowired
private PrcPriceExcelFileMapper excelFileMapper;
public void assertCatalogType(String priceType)
{
if (!PriceType.isCatalog(priceType))
{
throw new ServiceException("priceType must be PRODUCT or REDLINE");
}
}
/** Latest version for list: prefer EFFECT, else max version_seq. */
public PrcPriceSetVersion resolveLatestForList(String priceType)
{
assertCatalogType(priceType);
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(priceType);
q.setStatus(STATUS_EFFECT);
List<PrcPriceSetVersion> effect = versionMapper.selectPrcPriceSetVersionList(q);
if (effect != null && !effect.isEmpty())
{
return effect.get(0);
}
q.setStatus(null);
List<PrcPriceSetVersion> all = versionMapper.selectPrcPriceSetVersionList(q);
return (all == null || all.isEmpty()) ? null : all.get(0);
}
public PrcPriceSetVersion selectActiveAt(String priceType, Date asOf)
{
assertCatalogType(priceType);
return versionMapper.selectActiveByTypeAt(priceType, asOf);
}
/**
* Ensure a draft version for import/manual edit at effectiveTime.
* If an open draft exists, reuse it; else create next seq and optionally copy from latest.
*/
public PrcPriceSetVersion ensureDraftVersion(String priceType, Date effectiveTime, boolean copyFromLatest)
{
assertCatalogType(priceType);
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(priceType);
q.setStatus(STATUS_DRAFT);
List<PrcPriceSetVersion> drafts = versionMapper.selectPrcPriceSetVersionList(q);
if (drafts != null && !drafts.isEmpty())
{
PrcPriceSetVersion draft = drafts.get(0);
if (effectiveTime != null)
{
draft.setEffectiveTime(effectiveTime);
versionMapper.updatePrcPriceSetVersion(draft);
}
return draft;
}
Integer max = versionMapper.selectMaxVersionSeqByType(priceType, null);
int next = max == null ? 1 : max + 1;
PrcPriceSetVersion nv = new PrcPriceSetVersion();
nv.setPriceSetId(null);
nv.setPriceType(priceType);
nv.setVersionSeq(next);
nv.setVersionNo("V" + next);
nv.setEffectiveTime(effectiveTime);
nv.setStatus(STATUS_DRAFT);
nv.setPriceBuildMode("IMPORT");
nv.setCreateBy("system");
versionMapper.insertPrcPriceSetVersion(nv);
if (copyFromLatest)
{
PrcPriceSetVersion src = resolveLatestForList(priceType);
if (src != null && src.getPsvId() != null && !src.getPsvId().equals(nv.getPsvId()))
{
productMapper.copyFromVersion(src.getPsvId(), nv.getPsvId());
detailMapper.copyDetailsFromVersion(src.getPsvId(), nv.getPsvId());
productMapper.bindDetailPspByPsvId(nv.getPsvId());
// stamp price_type on copied rows
productMapper.updatePriceTypeByPsvId(nv.getPsvId(), priceType);
detailMapper.updatePriceTypeByPsvId(nv.getPsvId(), priceType);
}
}
return nv;
}
/** Activate draft: set EFFECT, chain expire on previous EFFECT versions of same type. */
public void activateVersion(Long psvId, Date effectiveTime)
{
PrcPriceSetVersion ver = versionMapper.selectPrcPriceSetVersionById(psvId);
if (ver == null)
{
throw new ServiceException("版本不存在");
}
assertCatalogType(ver.getPriceType());
if (!STATUS_DRAFT.equals(ver.getStatus()) && !STATUS_PENDING.equals(ver.getStatus()))
{
throw new ServiceException("仅草稿或待生效版本可以启动");
}
Date eff = effectiveTime != null ? effectiveTime : ver.getEffectiveTime();
if (eff == null)
{
throw new ServiceException("请选择生效时间");
}
Date prevEff = findPrevEffectiveTime(ver);
if (prevEff != null && eff.before(prevEff))
{
throw new ServiceException("生效时间不能早于上一版本");
}
boolean future = eff.after(new Date());
if (!future)
{
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(ver.getPriceType());
q.setStatus(STATUS_EFFECT);
List<PrcPriceSetVersion> prevList = versionMapper.selectPrcPriceSetVersionList(q);
if (prevList != null)
{
for (PrcPriceSetVersion prev : prevList)
{
if (prev.getPsvId().equals(psvId))
{
continue;
}
prev.setExpireTime(eff);
prev.setStatus(STATUS_EXPIRED);
versionMapper.updatePrcPriceSetVersion(prev);
}
}
}
ver.setEffectiveTime(eff);
ver.setExpireTime(null);
ver.setStatus(future ? STATUS_PENDING : STATUS_EFFECT);
versionMapper.updatePrcPriceSetVersion(ver);
refreshCatalogStatuses(ver.getPriceType());
}
public List<PrcPriceSetVersion> listVersions(String priceType, String status)
{
return listVersions(priceType, status, null);
}
public List<PrcPriceSetVersion> listVersions(String priceType, String status, String statusGroup)
{
assertCatalogType(priceType);
refreshCatalogStatuses(priceType);
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(priceType);
q.setStatus(status);
q.setStatusGroup(statusGroup);
List<PrcPriceSetVersion> list = versionMapper.selectCatalogVersionList(q);
return list == null ? java.util.Collections.emptyList() : list;
}
/**
* Hard-delete draft/pending catalog version with products, details,
* surcharge links and excel attachments.
*/
public int deleteDraftVersion(Long psvId)
{
PrcPriceSetVersion ver = versionMapper.selectPrcPriceSetVersionById(psvId);
if (ver == null)
{
throw new ServiceException("\u7248\u672c\u4e0d\u5b58\u5728");
}
assertCatalogType(ver.getPriceType());
if (!STATUS_DRAFT.equals(ver.getStatus()) && !STATUS_PENDING.equals(ver.getStatus()))
{
throw new ServiceException("\u4ec5\u8349\u7a3f\u6216\u5f85\u751f\u6548\u7248\u672c\u53ef\u4ee5\u5220\u9664");
}
surchargeRelMapper.physicalDeleteByPsvId(psvId);
detailMapper.physicalDeleteByPsvId(psvId);
productMapper.physicalDeleteByPsvId(psvId);
excelFileMapper.physicalDeleteByPsvId(psvId);
return versionMapper.deletePrcPriceSetVersionByIds(new Long[] { psvId });
}
/** Current draft version (status=0), or null. */
public PrcPriceSetVersion currentDraft(String priceType)
{
List<PrcPriceSetVersion> drafts = listVersions(priceType, STATUS_DRAFT);
return drafts.isEmpty() ? null : drafts.get(0);
}
/** Draft/pending catalog version that can accept preview apply / edit. */
public PrcPriceSetVersion requireEditableCatalogVersion(Long psvId, String priceType)
{
PrcPriceSetVersion ver = versionMapper.selectPrcPriceSetVersionById(psvId);
if (ver == null)
{
throw new ServiceException("\u7248\u672c\u4e0d\u5b58\u5728");
}
assertCatalogType(ver.getPriceType());
if (StringUtils.isNotEmpty(priceType) && !priceType.equals(ver.getPriceType()))
{
throw new ServiceException("\u7248\u672c\u7c7b\u578b\u4e0d\u5339\u914d");
}
if (!STATUS_DRAFT.equals(ver.getStatus()) && !STATUS_PENDING.equals(ver.getStatus()))
{
throw new ServiceException("\u4ec5\u8349\u7a3f\u6216\u5f85\u751f\u6548\u7248\u672c\u53ef\u4ee5\u7f16\u8f91");
}
return ver;
}
/**
* Prepare draft for catalog price create page.
* copyFromLatest=true: copy from current EFFECT version when draft is empty.
* copyFromLatest=false: empty draft (Excel import / rebuild).
*/
public PrcPriceSetVersion prepareDraftVersion(String priceType, boolean copyFromLatest)
{
assertCatalogType(priceType);
List<PrcPriceSetVersion> drafts = listVersions(priceType, STATUS_DRAFT);
PrcPriceSetVersion draft;
if (drafts.isEmpty())
{
if (copyFromLatest)
{
PrcPriceSetVersion src = findEffectVersion(priceType);
if (src == null)
{
throw new ServiceException("\u5f53\u524d\u6ca1\u6709\u5df2\u751f\u6548\u7248\u672c\u53ef\u4ee5\u6cbf\u7528");
}
}
draft = ensureDraftVersion(priceType, null, copyFromLatest);
draft.setPriceBuildMode(copyFromLatest ? "COPY" : "IMPORT");
versionMapper.updatePrcPriceSetVersion(draft);
return reloadCatalogVersion(draft.getPsvId(), priceType);
}
draft = drafts.get(0);
int count = draft.getProductCount() == null ? 0 : draft.getProductCount().intValue();
if (count == 0 && copyFromLatest)
{
PrcPriceSetVersion src = findEffectVersion(priceType);
if (src == null)
{
throw new ServiceException("\u5f53\u524d\u6ca1\u6709\u5df2\u751f\u6548\u7248\u672c\u53ef\u4ee5\u6cbf\u7528");
}
if (!src.getPsvId().equals(draft.getPsvId()))
{
productMapper.copyFromVersion(src.getPsvId(), draft.getPsvId());
detailMapper.copyDetailsFromVersion(src.getPsvId(), draft.getPsvId());
productMapper.bindDetailPspByPsvId(draft.getPsvId());
productMapper.updatePriceTypeByPsvId(draft.getPsvId(), priceType);
detailMapper.updatePriceTypeByPsvId(draft.getPsvId(), priceType);
}
draft.setPriceBuildMode("COPY");
versionMapper.updatePrcPriceSetVersion(draft);
}
else if (count == 0)
{
draft.setPriceBuildMode("IMPORT");
versionMapper.updatePrcPriceSetVersion(draft);
}
return reloadCatalogVersion(draft.getPsvId(), priceType);
}
private PrcPriceSetVersion findEffectVersion(String priceType)
{
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(priceType);
q.setStatus(STATUS_EFFECT);
List<PrcPriceSetVersion> effect = versionMapper.selectPrcPriceSetVersionList(q);
return (effect == null || effect.isEmpty()) ? null : effect.get(0);
}
private PrcPriceSetVersion reloadCatalogVersion(Long psvId, String priceType)
{
List<PrcPriceSetVersion> list = listVersions(priceType, null);
for (PrcPriceSetVersion v : list)
{
if (v != null && psvId.equals(v.getPsvId()))
{
return v;
}
}
return versionMapper.selectPrcPriceSetVersionById(psvId);
}
private Date findPrevEffectiveTime(PrcPriceSetVersion version)
{
PrcPriceSetVersion q = new PrcPriceSetVersion();
q.setPriceType(version.getPriceType());
List<PrcPriceSetVersion> list = versionMapper.selectPrcPriceSetVersionList(q);
if (list == null)
{
return null;
}
int cur = version.getVersionSeq() == null ? 0 : version.getVersionSeq().intValue();
Date prev = null;
for (PrcPriceSetVersion v : list)
{
if (v == null || v.getEffectiveTime() == null)
{
continue;
}
if (version.getPsvId() != null && version.getPsvId().equals(v.getPsvId()))
{
continue;
}
int seq = v.getVersionSeq() == null ? 0 : v.getVersionSeq().intValue();
if (seq >= cur)
{
continue;
}
if (prev == null || v.getEffectiveTime().after(prev))
{
prev = v.getEffectiveTime();
}
}
return prev;
}
public PrcPriceSetVersion findVersion(Long psvId)
{
if (psvId == null)
{
return null;
}
return versionMapper.selectPrcPriceSetVersionById(psvId);
}
public void refreshCatalogStatuses(String priceType)
{
versionMapper.refreshCatalogStatusByDates(priceType);
productMapper.syncEffectByPriceType(priceType);
detailMapper.syncEffectByPriceType(priceType);
}
public Long requirePsvId(String priceType, Long psvId, Date effectiveTime)
{
if (psvId != null)
{
return psvId;
}
PrcPriceSetVersion latest = resolveLatestForList(priceType);
if (latest != null && STATUS_DRAFT.equals(latest.getStatus()))
{
if (effectiveTime != null)
{
latest.setEffectiveTime(effectiveTime);
versionMapper.updatePrcPriceSetVersion(latest);
}
return latest.getPsvId();
}
if (latest != null && STATUS_EFFECT.equals(latest.getStatus()))
{
if (effectiveTime == null || sameTime(latest.getEffectiveTime(), effectiveTime))
{
return latest.getPsvId();
}
}
PrcPriceSetVersion draft = ensureDraftVersion(priceType, effectiveTime, true);
return draft.getPsvId();
}
private static boolean sameTime(Date a, Date b)
{
if (a == null && b == null)
{
return true;
}
if (a == null || b == null)
{
return false;
}
return a.getTime() == b.getTime();
}
public static String normalizeType(String priceType, String defaultType)
{
return StringUtils.isEmpty(priceType) ? defaultType : priceType;
}
}
@@ -0,0 +1,44 @@
package com.bms.price.service.support;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.bms.common.exception.ServiceException;
/** Collect multipart parts named files and/or file. At most 5. */
public final class UploadFileBatch
{
public static final int MAX_FILES = 5;
private UploadFileBatch()
{
}
public static List<MultipartFile> collect(MultipartFile[] files, MultipartFile file)
{
List<MultipartFile> list = new ArrayList<MultipartFile>();
if (files != null)
{
for (MultipartFile item : files)
{
if (item != null && !item.isEmpty())
{
list.add(item);
}
}
}
if (file != null && !file.isEmpty())
{
list.add(file);
}
if (list.isEmpty())
{
throw new ServiceException("file required");
}
if (list.size() > MAX_FILES)
{
throw new ServiceException("\u5355\u6b21\u6700\u591a\u5bfc\u5165 5 \u4e2a\u6587\u4ef6");
}
return list;
}
}
@@ -1,7 +1,17 @@
package com.bms.price.task;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -9,11 +19,14 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.bms.common.core.redis.RedisCache;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.BmsProduct;
import com.bms.price.domain.BmsProductCountry;
import com.bms.price.domain.BmsProductImportTask;
import com.bms.price.domain.CatalogPreviewApplyRequest;
import com.bms.price.service.IBmsProductService;
/**
* Async runner for product Excel import (avoids HTTP timeout on large workbooks).
* Async runner for product Excel import / preview (avoids HTTP timeout on large workbooks).
*/
@Component
public class BmsProductImportAsyncRunner
@@ -23,6 +36,8 @@ public class BmsProductImportAsyncRunner
public static final String CACHE_KEY_PREFIX = "price:product:import:task:";
public static final int TTL_MINUTES = 60;
private final ConcurrentHashMap<String, Object> previewResults = new ConcurrentHashMap<String, Object>();
@Autowired
private IBmsProductService productService;
@@ -69,6 +84,215 @@ public class BmsProductImportAsyncRunner
}
}
/** Background multi-country parse-only preview. One task merges up to 5 workbooks. */
@Async("threadPoolTaskExecutor")
public void runPreviewAll(String taskId, List<byte[]> bytesList, List<String> names)
{
try
{
Map<String, Object> merged = parseCatalogFiles(taskId, bytesList, names, false, null);
putSuccess(taskId, "preview ok", merged);
log.info("async product preview done, taskId={}, files={}", taskId, Integer.valueOf(bytesList == null ? 0 : bytesList.size()));
}
catch (Exception e)
{
log.error("async product preview failed, taskId={}", taskId, e);
previewResults.remove(taskId);
put(taskId, BmsProductImportTask.STATUS_FAIL, 100, "failed", errorMessage(e));
}
}
/** Background single-country parse-only preview. One task merges up to 5 workbooks. */
@Async("threadPoolTaskExecutor")
public void runPreviewCountry(String taskId, List<byte[]> bytesList, List<String> names, String countryCode)
{
try
{
Map<String, Object> merged = parseCatalogFiles(taskId, bytesList, names, true, countryCode);
putSuccess(taskId, "preview ok", merged);
log.info("async product country preview done, taskId={}, country={}, files={}", taskId, countryCode,
Integer.valueOf(bytesList == null ? 0 : bytesList.size()));
}
catch (Exception e)
{
log.error("async product country preview failed, taskId={}, country={}", taskId, countryCode, e);
previewResults.remove(taskId);
put(taskId, BmsProductImportTask.STATUS_FAIL, 100, "failed", errorMessage(e));
}
}
/** Parse workbooks together. Merge stays in upload order so a later file wins. */
private Map<String, Object> parseCatalogFiles(String taskId, List<byte[]> bytesList, List<String> names,
boolean country, String countryCode) throws Exception
{
int n = bytesList == null ? 0 : bytesList.size();
if (n == 0)
{
return null;
}
Map<String, Object> merged = null;
for (int i = 0; i < n; i++)
{
put(taskId, BmsProductImportTask.STATUS_RUNNING, 10 + (70 * i / n), "parsing " + (i + 1) + "/" + n, null);
String filename = names != null && i < names.size() ? names.get(i) : null;
byte[] bytes = bytesList.get(i);
bytesList.set(i, null);
Map<String, Object> one = country
? productService.previewImportCountryCatalog(bytes, filename, countryCode)
: productService.previewImportCatalog(bytes, filename);
merged = productService.mergeCatalogPreview(merged, one);
}
return merged;
}
/** Save preview products in parallel. Each product is its own transaction. */
@Async("threadPoolTaskExecutor")
public void runApplyPreview(String taskId, CatalogPreviewApplyRequest request)
{
ExecutorService pool = null;
try
{
Map<String, BmsProduct> meta = new HashMap<String, BmsProduct>();
if (request.getProducts() != null)
{
for (BmsProduct p : request.getProducts())
{
if (p != null && StringUtils.isNotEmpty(p.getProductCode()))
{
meta.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
}
}
}
List<Map.Entry<String, List<BmsProductCountry>>> jobs =
new ArrayList<Map.Entry<String, List<BmsProductCountry>>>();
Map<String, List<BmsProductCountry>> byCode = request.getDetailsByProductCode();
if (byCode != null)
{
for (Map.Entry<String, List<BmsProductCountry>> e : byCode.entrySet())
{
if (e != null && StringUtils.isNotEmpty(e.getKey()) && e.getValue() != null && !e.getValue().isEmpty())
{
jobs.add(e);
}
}
}
int n = jobs.size();
if (n == 0)
{
put(taskId, BmsProductImportTask.STATUS_SUCCESS, 100, "\u5df2\u4fdd\u5b58", null);
return;
}
put(taskId, BmsProductImportTask.STATUS_RUNNING, 5, "\u6b63\u5728\u4fdd\u5b58 0/" + n, null);
Long psvId = request.getPsvId();
AtomicInteger done = new AtomicInteger();
pool = Executors.newFixedThreadPool(Math.min(4, n));
List<Future<?>> futures = new ArrayList<Future<?>>();
for (Map.Entry<String, List<BmsProductCountry>> e : jobs)
{
final String code = e.getKey().trim().toUpperCase(Locale.ROOT);
final List<BmsProductCountry> details = e.getValue();
final BmsProduct product = meta.get(code);
futures.add(pool.submit(new Runnable()
{
@Override
public void run()
{
applyWithDeadlockRetry(new Runnable()
{
@Override
public void run()
{
productService.applyOneCatalogProduct(psvId, code, product, details);
}
});
int d = done.incrementAndGet();
put(taskId, BmsProductImportTask.STATUS_RUNNING, 5 + (90 * d / n),
"\u6b63\u5728\u4fdd\u5b58 " + d + "/" + n, null);
}
}));
}
for (Future<?> future : futures)
{
future.get();
}
put(taskId, BmsProductImportTask.STATUS_SUCCESS, 100, "\u5df2\u4fdd\u5b58\u5230\u5f85\u751f\u6548\u8349\u7a3f", null);
log.info("async product apply preview done, taskId={}, products={}", taskId, Integer.valueOf(n));
}
catch (Exception e)
{
log.error("async product apply preview failed, taskId={}", taskId, e);
Throwable cause = e.getCause() == null ? e : e.getCause();
String msg = cause.getMessage();
if (StringUtils.isEmpty(msg))
{
msg = cause.getClass().getSimpleName();
}
put(taskId, BmsProductImportTask.STATUS_FAIL, 100, "failed", msg);
}
finally
{
if (pool != null)
{
pool.shutdownNow();
}
}
}
/** InnoDB rolls the product transaction back on deadlock; start a new one. */
private void applyWithDeadlockRetry(Runnable action)
{
int max = 5;
for (int attempt = 1; ; attempt++)
{
try
{
action.run();
return;
}
catch (RuntimeException ex)
{
if (attempt >= max || !isDeadlock(ex))
{
throw ex;
}
log.warn("product catalog save deadlock, retry {}/{}", Integer.valueOf(attempt), Integer.valueOf(max));
try
{
Thread.sleep(50L * attempt);
}
catch (InterruptedException ie)
{
Thread.currentThread().interrupt();
throw ex;
}
}
}
}
private static boolean isDeadlock(Throwable ex)
{
Throwable cur = ex;
while (cur != null)
{
if (cur instanceof org.springframework.dao.PessimisticLockingFailureException)
{
return true;
}
String name = cur.getClass().getName();
if (name.indexOf("MySQLTransactionRollbackException") >= 0)
{
return true;
}
String msg = cur.getMessage();
if (msg != null && msg.indexOf("Deadlock found when trying to get lock") >= 0)
{
return true;
}
cur = cur.getCause();
}
return false;
}
public void put(String taskId, String status, int percent, String message, String error)
{
BmsProductImportTask task = new BmsProductImportTask();
@@ -80,9 +304,27 @@ public class BmsProductImportAsyncRunner
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES);
}
private void putSuccess(String taskId, String message, Object result)
{
if (result != null)
{
previewResults.put(taskId, result);
}
put(taskId, BmsProductImportTask.STATUS_SUCCESS, 100, message, null);
}
public BmsProductImportTask get(String taskId)
{
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
BmsProductImportTask task = redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
if (task != null)
{
Object result = previewResults.get(taskId);
if (result != null)
{
task.setResult(result);
}
}
return task;
}
private static String errorMessage(Exception e)
@@ -1,7 +1,17 @@
package com.bms.price.task;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -9,11 +19,14 @@ import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import com.bms.common.core.redis.RedisCache;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.BmsRedlineProduct;
import com.bms.price.domain.BmsRedlineProductCountry;
import com.bms.price.domain.BmsRedlineProductImportTask;
import com.bms.price.domain.RedlineCatalogPreviewApplyRequest;
import com.bms.price.service.IBmsRedlineProductService;
/**
* Async runner for product Excel import (avoids HTTP timeout on large workbooks).
* Async runner for redline Excel import / preview (avoids HTTP timeout on large workbooks).
*/
@Component
public class BmsRedlineProductImportAsyncRunner
@@ -23,6 +36,8 @@ public class BmsRedlineProductImportAsyncRunner
public static final String CACHE_KEY_PREFIX = "price:redline:import:task:";
public static final int TTL_MINUTES = 60;
private final ConcurrentHashMap<String, Object> previewResults = new ConcurrentHashMap<String, Object>();
@Autowired
private IBmsRedlineProductService productService;
@@ -40,11 +55,11 @@ public class BmsRedlineProductImportAsyncRunner
String message = productService.importProductExcel(bytes, filename, updateSupport, operName, taskId,
effectiveDate);
put(taskId, BmsRedlineProductImportTask.STATUS_SUCCESS, 100, message, null);
log.info("async product import done, taskId={}", taskId);
log.info("async redline import done, taskId={}", taskId);
}
catch (Exception e)
{
log.error("async product import failed, taskId={}", taskId, e);
log.error("async redline import failed, taskId={}", taskId, e);
put(taskId, BmsRedlineProductImportTask.STATUS_FAIL, 100, "failed", errorMessage(e));
}
}
@@ -60,15 +75,224 @@ public class BmsRedlineProductImportAsyncRunner
String message = productService.importCountryProductExcel(bytes, filename, countryCode, operName, taskId,
effectiveDate);
put(taskId, BmsRedlineProductImportTask.STATUS_SUCCESS, 100, message, null);
log.info("async product country import done, taskId={}, country={}", taskId, countryCode);
log.info("async redline country import done, taskId={}, country={}", taskId, countryCode);
}
catch (Exception e)
{
log.error("async product country import failed, taskId={}, country={}", taskId, countryCode, e);
log.error("async redline country import failed, taskId={}, country={}", taskId, countryCode, e);
put(taskId, BmsRedlineProductImportTask.STATUS_FAIL, 100, "failed", errorMessage(e));
}
}
/** Background multi-country parse-only preview. One task merges up to 5 workbooks. */
@Async("threadPoolTaskExecutor")
public void runPreviewAll(String taskId, List<byte[]> bytesList, List<String> names)
{
try
{
Map<String, Object> merged = parseCatalogFiles(taskId, bytesList, names, false, null);
putSuccess(taskId, "preview ok", merged);
log.info("async redline preview done, taskId={}, files={}", taskId, Integer.valueOf(bytesList == null ? 0 : bytesList.size()));
}
catch (Exception e)
{
log.error("async redline preview failed, taskId={}", taskId, e);
previewResults.remove(taskId);
put(taskId, BmsRedlineProductImportTask.STATUS_FAIL, 100, "failed", errorMessage(e));
}
}
/** Background single-country parse-only preview. One task merges up to 5 workbooks. */
@Async("threadPoolTaskExecutor")
public void runPreviewCountry(String taskId, List<byte[]> bytesList, List<String> names, String countryCode)
{
try
{
Map<String, Object> merged = parseCatalogFiles(taskId, bytesList, names, true, countryCode);
putSuccess(taskId, "preview ok", merged);
log.info("async redline country preview done, taskId={}, country={}, files={}", taskId, countryCode,
Integer.valueOf(bytesList == null ? 0 : bytesList.size()));
}
catch (Exception e)
{
log.error("async redline country preview failed, taskId={}, country={}", taskId, countryCode, e);
previewResults.remove(taskId);
put(taskId, BmsRedlineProductImportTask.STATUS_FAIL, 100, "failed", errorMessage(e));
}
}
/** Parse workbooks together. Merge stays in upload order so a later file wins. */
private Map<String, Object> parseCatalogFiles(String taskId, List<byte[]> bytesList, List<String> names,
boolean country, String countryCode) throws Exception
{
int n = bytesList == null ? 0 : bytesList.size();
if (n == 0)
{
return null;
}
Map<String, Object> merged = null;
for (int i = 0; i < n; i++)
{
put(taskId, BmsRedlineProductImportTask.STATUS_RUNNING, 10 + (70 * i / n), "parsing " + (i + 1) + "/" + n, null);
String filename = names != null && i < names.size() ? names.get(i) : null;
byte[] bytes = bytesList.get(i);
bytesList.set(i, null);
Map<String, Object> one = country
? productService.previewImportCountryCatalog(bytes, filename, countryCode)
: productService.previewImportCatalog(bytes, filename);
merged = productService.mergeCatalogPreview(merged, one);
}
return merged;
}
/** Save preview products in parallel. Each product is its own transaction. */
@Async("threadPoolTaskExecutor")
public void runApplyPreview(String taskId, RedlineCatalogPreviewApplyRequest request)
{
ExecutorService pool = null;
try
{
Map<String, BmsRedlineProduct> meta = new HashMap<String, BmsRedlineProduct>();
if (request.getProducts() != null)
{
for (BmsRedlineProduct p : request.getProducts())
{
if (p != null && StringUtils.isNotEmpty(p.getProductCode()))
{
meta.put(p.getProductCode().trim().toUpperCase(Locale.ROOT), p);
}
}
}
List<Map.Entry<String, List<BmsRedlineProductCountry>>> jobs =
new ArrayList<Map.Entry<String, List<BmsRedlineProductCountry>>>();
Map<String, List<BmsRedlineProductCountry>> byCode = request.getDetailsByProductCode();
if (byCode != null)
{
for (Map.Entry<String, List<BmsRedlineProductCountry>> e : byCode.entrySet())
{
if (e != null && StringUtils.isNotEmpty(e.getKey()) && e.getValue() != null && !e.getValue().isEmpty())
{
jobs.add(e);
}
}
}
int n = jobs.size();
if (n == 0)
{
put(taskId, BmsRedlineProductImportTask.STATUS_SUCCESS, 100, "\u5df2\u4fdd\u5b58", null);
return;
}
put(taskId, BmsRedlineProductImportTask.STATUS_RUNNING, 5, "\u6b63\u5728\u4fdd\u5b58 0/" + n, null);
Long psvId = request.getPsvId();
AtomicInteger done = new AtomicInteger();
pool = Executors.newFixedThreadPool(Math.min(4, n));
List<Future<?>> futures = new ArrayList<Future<?>>();
for (Map.Entry<String, List<BmsRedlineProductCountry>> e : jobs)
{
final String code = e.getKey().trim().toUpperCase(Locale.ROOT);
final List<BmsRedlineProductCountry> details = e.getValue();
final BmsRedlineProduct product = meta.get(code);
futures.add(pool.submit(new Runnable()
{
@Override
public void run()
{
applyWithDeadlockRetry(new Runnable()
{
@Override
public void run()
{
productService.applyOneCatalogProduct(psvId, code, product, details);
}
});
int d = done.incrementAndGet();
put(taskId, BmsRedlineProductImportTask.STATUS_RUNNING, 5 + (90 * d / n),
"\u6b63\u5728\u4fdd\u5b58 " + d + "/" + n, null);
}
}));
}
for (Future<?> future : futures)
{
future.get();
}
put(taskId, BmsRedlineProductImportTask.STATUS_SUCCESS, 100, "\u5df2\u4fdd\u5b58\u5230\u5f85\u751f\u6548\u8349\u7a3f", null);
log.info("async redline apply preview done, taskId={}, products={}", taskId, Integer.valueOf(n));
}
catch (Exception e)
{
log.error("async redline apply preview failed, taskId={}", taskId, e);
Throwable cause = e.getCause() == null ? e : e.getCause();
String msg = cause.getMessage();
if (StringUtils.isEmpty(msg))
{
msg = cause.getClass().getSimpleName();
}
put(taskId, BmsRedlineProductImportTask.STATUS_FAIL, 100, "failed", msg);
}
finally
{
if (pool != null)
{
pool.shutdownNow();
}
}
}
/** InnoDB rolls the product transaction back on deadlock; start a new one. */
private void applyWithDeadlockRetry(Runnable action)
{
int max = 5;
for (int attempt = 1; ; attempt++)
{
try
{
action.run();
return;
}
catch (RuntimeException ex)
{
if (attempt >= max || !isDeadlock(ex))
{
throw ex;
}
log.warn("redline catalog save deadlock, retry {}/{}", Integer.valueOf(attempt), Integer.valueOf(max));
try
{
Thread.sleep(50L * attempt);
}
catch (InterruptedException ie)
{
Thread.currentThread().interrupt();
throw ex;
}
}
}
}
private static boolean isDeadlock(Throwable ex)
{
Throwable cur = ex;
while (cur != null)
{
if (cur instanceof org.springframework.dao.PessimisticLockingFailureException)
{
return true;
}
String name = cur.getClass().getName();
if (name.indexOf("MySQLTransactionRollbackException") >= 0)
{
return true;
}
String msg = cur.getMessage();
if (msg != null && msg.indexOf("Deadlock found when trying to get lock") >= 0)
{
return true;
}
cur = cur.getCause();
}
return false;
}
public void put(String taskId, String status, int percent, String message, String error)
{
BmsRedlineProductImportTask task = new BmsRedlineProductImportTask();
@@ -80,9 +304,27 @@ public class BmsRedlineProductImportAsyncRunner
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES);
}
private void putSuccess(String taskId, String message, Object result)
{
if (result != null)
{
previewResults.put(taskId, result);
}
put(taskId, BmsRedlineProductImportTask.STATUS_SUCCESS, 100, message, null);
}
public BmsRedlineProductImportTask get(String taskId)
{
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
BmsRedlineProductImportTask task = redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
if (task != null)
{
Object result = previewResults.get(taskId);
if (result != null)
{
task.setResult(result);
}
}
return task;
}
private static String errorMessage(Exception e)
@@ -116,6 +116,38 @@ public class PrcPriceSetSaveAsyncRunner
}
}
/** Import up to 5 workbooks in one task. Each file commits before the next. */
@Async("threadPoolTaskExecutor")
public void runImportPricesFiles(String taskId, Long priceSetId, Long psvId, List<byte[]> bytesList, List<String> names,
String operName, boolean updateOnly, boolean replaceByProductCountry, String countryCode)
{
try
{
int n = bytesList == null ? 0 : bytesList.size();
String message = null;
for (int i = 0; i < n; i++)
{
int pct = 10 + (80 * i / Math.max(n, 1));
put(taskId, PrcPriceSetSaveTask.STATUS_RUNNING, pct, "parsing " + (i + 1) + "/" + n, priceSetId, null);
String filename = names != null && i < names.size() ? names.get(i) : null;
message = priceSetService.importDetailPrices(priceSetId, psvId, bytesList.get(i), filename, operName, taskId,
updateOnly, replaceByProductCountry, countryCode);
}
put(taskId, PrcPriceSetSaveTask.STATUS_SUCCESS, 100, message, priceSetId, null);
log.info("async import prices files done, taskId={}, priceSetId={}, files={}", taskId, priceSetId, Integer.valueOf(n));
}
catch (Exception e)
{
log.error("async import prices files failed, taskId={}, priceSetId={}", taskId, priceSetId, e);
String msg = e.getMessage();
if (StringUtils.isEmpty(msg))
{
msg = e.getClass().getSimpleName();
}
put(taskId, PrcPriceSetSaveTask.STATUS_FAIL, 100, "failed", priceSetId, msg);
}
}
/** Background pull of product library into package version details. */
@Async("threadPoolTaskExecutor")
public void runPullLibrary(String taskId, Long priceSetId, Long psvId, boolean force, String operName)