重构打单套餐相关逻辑,更新服务接口,优化保存和查询功能
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
/**
|
||||
* 打单重量段计费条件(字典 lbl_charge_mode)。
|
||||
*/
|
||||
public final class LblChargeMode
|
||||
{
|
||||
private LblChargeMode()
|
||||
{
|
||||
}
|
||||
|
||||
/** 按票 */
|
||||
public static final String ITEM = "ITEM";
|
||||
|
||||
/** 单价*公斤数 */
|
||||
public static final String PER_KG = "PER_KG";
|
||||
|
||||
/** 首重+续重 */
|
||||
public static final String EXTRA_KG = "EXTRA_KG";
|
||||
|
||||
/** 邮编 */
|
||||
public static final String POSTCODE = "POSTCODE";
|
||||
|
||||
/** @deprecated 映射为 ITEM */
|
||||
@Deprecated
|
||||
public static final String ALWAYS = "ALWAYS";
|
||||
|
||||
/** @deprecated 映射为 ITEM(分区用 zone 列过滤) */
|
||||
@Deprecated
|
||||
public static final String ZONE = "ZONE";
|
||||
|
||||
/** @deprecated 映射为 ITEM */
|
||||
@Deprecated
|
||||
public static final String FLAT = "FLAT";
|
||||
|
||||
/** @deprecated 映射为 PER_KG */
|
||||
@Deprecated
|
||||
public static final String UNIT_PLUS_HANDLE = "UNIT_PLUS_HANDLE";
|
||||
|
||||
/** @deprecated 映射为 EXTRA_KG */
|
||||
@Deprecated
|
||||
public static final String FIRST_EXTRA = "FIRST_EXTRA";
|
||||
|
||||
/** @deprecated */
|
||||
@Deprecated
|
||||
public static final String WEIGHT = "WEIGHT";
|
||||
|
||||
/** @deprecated */
|
||||
@Deprecated
|
||||
public static final String SIZE = "SIZE";
|
||||
|
||||
/** @deprecated */
|
||||
@Deprecated
|
||||
public static final String RETURN = "RETURN";
|
||||
|
||||
/** @deprecated */
|
||||
@Deprecated
|
||||
public static final String MANUAL = "MANUAL";
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 打单解析过程中的邮编/配置原文行(二期落库;当前仅预览内存态)。
|
||||
*/
|
||||
@Data
|
||||
public class LblExtraLine
|
||||
{
|
||||
private Long lineId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private Long versionId;
|
||||
|
||||
private String lineType;
|
||||
|
||||
private String lineText;
|
||||
|
||||
private Integer sortNo;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
/**
|
||||
* 打单重量段费用类型(字典 bms_fee_type)。
|
||||
*/
|
||||
public final class LblFeeType
|
||||
{
|
||||
private LblFeeType()
|
||||
{
|
||||
}
|
||||
|
||||
/** 运费 */
|
||||
public static final String FREIGHT = "FREIGHT";
|
||||
|
||||
/** 处理费/挂号 */
|
||||
public static final String REGISTRATION = "REGISTRATION";
|
||||
|
||||
/** 偏远费 */
|
||||
public static final String REMOTE = "REMOTE";
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 打单价卡导入预览。
|
||||
*/
|
||||
@Data
|
||||
public class LblPreview
|
||||
{
|
||||
private String sourceFile;
|
||||
|
||||
private String filePath;
|
||||
|
||||
private String fileMd5;
|
||||
|
||||
private String supplierName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveDate;
|
||||
|
||||
private List<LblProduct> products = new ArrayList<>();
|
||||
|
||||
private List<String> skippedSheets = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.bms.common.annotation.Excel;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 打单价卡产品主数据。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LblProduct extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long productId;
|
||||
|
||||
@Excel(name = "产品名称", width = 24)
|
||||
private String productName;
|
||||
|
||||
private String dictValue;
|
||||
|
||||
private Long versionId;
|
||||
|
||||
private Integer versionNo;
|
||||
|
||||
private String versionStatus;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date versionStart;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date versionEnd;
|
||||
|
||||
private String countryScope;
|
||||
|
||||
private String chargeType;
|
||||
|
||||
private String extraPerKg;
|
||||
|
||||
private String serviceType;
|
||||
|
||||
private String classification;
|
||||
|
||||
private String catalogRemark;
|
||||
|
||||
private String updateContent;
|
||||
|
||||
private String weightLimit;
|
||||
|
||||
private String sheetName;
|
||||
|
||||
private String relatedSheet;
|
||||
|
||||
private String sourceFile;
|
||||
|
||||
private String filePath;
|
||||
|
||||
private String fileMd5;
|
||||
|
||||
private String supplierName;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveDate;
|
||||
|
||||
private String currency;
|
||||
|
||||
private String serviceNote;
|
||||
|
||||
private String parseStatus;
|
||||
|
||||
private Integer weightCount;
|
||||
|
||||
private Integer surchargeCount;
|
||||
|
||||
private Integer extraCount;
|
||||
|
||||
private String boundChannelText;
|
||||
|
||||
private Integer boundChannelCount;
|
||||
|
||||
private Boolean duplicated;
|
||||
|
||||
private Long existsId;
|
||||
|
||||
private List<LblWeightBand> weightBands;
|
||||
|
||||
private List<LblSurcharge> surcharges;
|
||||
|
||||
/** 兼容旧前端 termId,等同 versionId */
|
||||
public Long getTermId()
|
||||
{
|
||||
return versionId;
|
||||
}
|
||||
|
||||
public void setTermId(Long termId)
|
||||
{
|
||||
this.versionId = termId;
|
||||
}
|
||||
|
||||
private List<LblExtraLine> extraLines;
|
||||
|
||||
/** 邮编→分区(保存时同步到 prc_zone,不落产品子表) */
|
||||
private Map<String, List<String>> zonePostcodes;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.Date;
|
||||
import com.bms.common.core.domain.BaseEntity;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 打单价卡产品版本周期。
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class LblProductVersion extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long versionId;
|
||||
|
||||
private Long priceSetId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private String dictValue;
|
||||
|
||||
private Integer versionNo;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveStart;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveEnd;
|
||||
|
||||
private String filePath;
|
||||
|
||||
/** 0待生效 1生效 2已失效 */
|
||||
private String status;
|
||||
|
||||
private String productName;
|
||||
|
||||
private String sourceFile;
|
||||
|
||||
private Integer weightCount;
|
||||
|
||||
private Integer surchargeCount;
|
||||
|
||||
private String currency;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 打单价卡预览确认后的保存请求。
|
||||
*/
|
||||
@Data
|
||||
public class LblSaveCommand
|
||||
{
|
||||
private boolean overwrite;
|
||||
|
||||
private Long priceSetId;
|
||||
|
||||
private String priceSetName;
|
||||
|
||||
private String priceSetNote;
|
||||
|
||||
private Integer versionNo;
|
||||
|
||||
private List<LblProduct> products = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 打单价卡异步保存进度(Redis)。
|
||||
*/
|
||||
public class LblSaveTask implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final String STATUS_PENDING = "PENDING";
|
||||
public static final String STATUS_RUNNING = "RUNNING";
|
||||
public static final String STATUS_SUCCESS = "SUCCESS";
|
||||
public static final String STATUS_FAIL = "FAIL";
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private int percent;
|
||||
private String message;
|
||||
private Long priceSetId;
|
||||
private String error;
|
||||
|
||||
public String getTaskId()
|
||||
{
|
||||
return taskId;
|
||||
}
|
||||
|
||||
public void setTaskId(String taskId)
|
||||
{
|
||||
this.taskId = taskId;
|
||||
}
|
||||
|
||||
public String getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status)
|
||||
{
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getPercent()
|
||||
{
|
||||
return percent;
|
||||
}
|
||||
|
||||
public void setPercent(int percent)
|
||||
{
|
||||
this.percent = percent;
|
||||
}
|
||||
|
||||
public String getMessage()
|
||||
{
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message)
|
||||
{
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Long getPriceSetId()
|
||||
{
|
||||
return priceSetId;
|
||||
}
|
||||
|
||||
public void setPriceSetId(Long priceSetId)
|
||||
{
|
||||
this.priceSetId = priceSetId;
|
||||
}
|
||||
|
||||
public String getError()
|
||||
{
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(String error)
|
||||
{
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 打单价卡附加费(超重/超尺寸/% 等;偏远重量价不进本表)。
|
||||
*/
|
||||
@Data
|
||||
public class LblSurcharge
|
||||
{
|
||||
private Long surchargeId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private Long versionId;
|
||||
|
||||
/** 解析中间态区块名,不落库 */
|
||||
private String sectionName;
|
||||
|
||||
private String feeName;
|
||||
|
||||
private Long scId;
|
||||
|
||||
private String zoneCode;
|
||||
|
||||
private String zoneName;
|
||||
|
||||
private String chargeText;
|
||||
|
||||
private BigDecimal amount;
|
||||
|
||||
private String currency;
|
||||
|
||||
private String priceUnit;
|
||||
|
||||
private String feeDesc;
|
||||
|
||||
private String periodText;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveStart;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
|
||||
private Date effectiveEnd;
|
||||
|
||||
private String chargeTiming;
|
||||
|
||||
private String calcRule;
|
||||
|
||||
private String calcParam;
|
||||
|
||||
private String postcodeParam;
|
||||
|
||||
private BigDecimal weightMin;
|
||||
|
||||
private BigDecimal weightMax;
|
||||
|
||||
private Integer sortNo;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.bms.price.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 打单价卡重量段明细。
|
||||
* 维度:产品+国家+分区+重量段+fee_type;无 handle 字段。
|
||||
*/
|
||||
@Data
|
||||
public class LblWeightBand
|
||||
{
|
||||
private Long bandId;
|
||||
|
||||
private Long productId;
|
||||
|
||||
private Long versionId;
|
||||
|
||||
/** 解析中间态区块名,不落库 */
|
||||
private String sectionName;
|
||||
|
||||
/** FREIGHT / REGISTRATION / REMOTE */
|
||||
private String feeType;
|
||||
|
||||
private String countryName;
|
||||
|
||||
private String countryCode;
|
||||
|
||||
private String zoneCode;
|
||||
|
||||
private String zoneName;
|
||||
|
||||
private String postcodeParam;
|
||||
|
||||
private String weightLabel;
|
||||
|
||||
private BigDecimal weightMin;
|
||||
|
||||
private BigDecimal weightMax;
|
||||
|
||||
private String volumeText;
|
||||
|
||||
private String chargeName;
|
||||
|
||||
/** 字典 lbl_charge_mode:ITEM/PER_KG/EXTRA_KG/POSTCODE */
|
||||
private String chargeMode;
|
||||
|
||||
/** 条件参数(如首重KG、尺寸表达) */
|
||||
private String calcParam;
|
||||
|
||||
private BigDecimal price;
|
||||
|
||||
private String priceUnit;
|
||||
|
||||
private BigDecimal extraPrice;
|
||||
|
||||
private String extraPriceUnit;
|
||||
|
||||
private Integer sortNo;
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
package com.bms.price.label;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import com.bms.price.domain.LblChargeMode;
|
||||
import com.bms.price.domain.LblFeeType;
|
||||
import com.bms.price.domain.LblPreview;
|
||||
import com.bms.price.domain.LblProduct;
|
||||
import com.bms.price.domain.LblWeightBand;
|
||||
|
||||
/**
|
||||
* 归并解析重量段:按 fee_type 分行;首重+续重合 EXTRA_KG;补 chargeMode/zoneCode。
|
||||
* 不再把处理费合并进运费行。
|
||||
*/
|
||||
public final class LblBandNormalizer
|
||||
{
|
||||
private LblBandNormalizer()
|
||||
{
|
||||
}
|
||||
|
||||
public static void normalize(LblPreview preview)
|
||||
{
|
||||
if (preview == null || preview.getProducts() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (LblProduct product : preview.getProducts())
|
||||
{
|
||||
normalizeProduct(product);
|
||||
}
|
||||
}
|
||||
|
||||
public static void normalizeProduct(LblProduct product)
|
||||
{
|
||||
if (product == null || product.getWeightBands() == null || product.getWeightBands().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<LblWeightBand> rows = splitByFeeType(product.getWeightBands());
|
||||
rows = mergeFirstExtra(rows);
|
||||
int sort = 0;
|
||||
for (LblWeightBand band : rows)
|
||||
{
|
||||
if (band.getFeeType() == null || band.getFeeType().isEmpty())
|
||||
{
|
||||
band.setFeeType(inferFeeType(band));
|
||||
}
|
||||
if (band.getChargeMode() == null || band.getChargeMode().isEmpty())
|
||||
{
|
||||
band.setChargeMode(inferMode(band));
|
||||
}
|
||||
band.setChargeMode(migrateMode(band.getChargeMode()));
|
||||
if (band.getZoneCode() == null || band.getZoneCode().isEmpty())
|
||||
{
|
||||
band.setZoneCode(toZoneCode(band.getZoneName()));
|
||||
}
|
||||
if (band.getPostcodeParam() == null || band.getPostcodeParam().isEmpty())
|
||||
{
|
||||
String prefixes = LblSurchargeRules.remotePrefixes(band.getZoneName());
|
||||
band.setPostcodeParam(prefixes == null ? "" : prefixes);
|
||||
}
|
||||
if (band.getCountryCode() == null)
|
||||
{
|
||||
band.setCountryCode("");
|
||||
}
|
||||
if (band.getZoneCode() == null)
|
||||
{
|
||||
band.setZoneCode("");
|
||||
}
|
||||
if (band.getCalcParam() == null)
|
||||
{
|
||||
band.setCalcParam("");
|
||||
}
|
||||
band.setSortNo(sort++);
|
||||
}
|
||||
product.setWeightBands(rows);
|
||||
product.setWeightCount(rows.size());
|
||||
product.setChargeType(summarizeChargeType(rows));
|
||||
}
|
||||
|
||||
/** 处理费/挂号落 REGISTRATION;偏远落 REMOTE;其余落 FREIGHT。 */
|
||||
private static List<LblWeightBand> splitByFeeType(List<LblWeightBand> source)
|
||||
{
|
||||
List<LblWeightBand> out = new ArrayList<LblWeightBand>();
|
||||
for (LblWeightBand band : source)
|
||||
{
|
||||
if (band == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String name = nz(band.getChargeName());
|
||||
String fee = nz(band.getFeeType());
|
||||
if (!fee.isEmpty())
|
||||
{
|
||||
LblWeightBand kept = copyBand(band);
|
||||
// 解析器若误标 FREIGHT,按费用名回正为 REGISTRATION
|
||||
if (name.contains("处理费") || name.contains("挂号"))
|
||||
{
|
||||
kept.setFeeType(LblFeeType.REGISTRATION);
|
||||
if (kept.getChargeMode() == null || kept.getChargeMode().isEmpty())
|
||||
{
|
||||
kept.setChargeMode(LblChargeMode.ITEM);
|
||||
}
|
||||
}
|
||||
out.add(kept);
|
||||
continue;
|
||||
}
|
||||
if (nameContainsRemote(name))
|
||||
{
|
||||
LblWeightBand row = copyBand(band);
|
||||
row.setFeeType(LblFeeType.REMOTE);
|
||||
if (row.getChargeMode() == null || row.getChargeMode().isEmpty())
|
||||
{
|
||||
row.setChargeMode(nz(row.getPostcodeParam()).isEmpty() ? LblChargeMode.ITEM : LblChargeMode.POSTCODE);
|
||||
}
|
||||
out.add(row);
|
||||
continue;
|
||||
}
|
||||
if (name.contains("处理费") || name.contains("挂号"))
|
||||
{
|
||||
// 处理费/挂号固定 REGISTRATION,不再改写为 FREIGHT(避免与运费同维度唯一键冲突)
|
||||
LblWeightBand row = copyBand(band);
|
||||
row.setFeeType(LblFeeType.REGISTRATION);
|
||||
row.setChargeMode(LblChargeMode.ITEM);
|
||||
out.add(row);
|
||||
continue;
|
||||
}
|
||||
LblWeightBand row = copyBand(band);
|
||||
row.setFeeType(LblFeeType.FREIGHT);
|
||||
out.add(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static boolean nameContainsRemote(String name)
|
||||
{
|
||||
String n = name.toLowerCase(Locale.ROOT);
|
||||
return name.contains("偏远") || n.contains("remote") || name.contains("岛屿") || n.contains("island");
|
||||
}
|
||||
|
||||
private static List<LblWeightBand> mergeFirstExtra(List<LblWeightBand> source)
|
||||
{
|
||||
Map<String, LblWeightBand> firsts = new LinkedHashMap<String, LblWeightBand>();
|
||||
List<LblWeightBand> extras = new ArrayList<LblWeightBand>();
|
||||
List<LblWeightBand> rest = new ArrayList<LblWeightBand>();
|
||||
for (LblWeightBand band : source)
|
||||
{
|
||||
String label = nz(band.getWeightLabel()).toLowerCase(Locale.ROOT);
|
||||
boolean first = label.contains("首重") || label.contains("first");
|
||||
boolean extra = label.contains("每增加") || label.contains("续重") || label.contains("additional");
|
||||
if (first)
|
||||
{
|
||||
String key = zoneFeeKey(band);
|
||||
LblWeightBand row = copyBand(band);
|
||||
row.setChargeMode(LblChargeMode.EXTRA_KG);
|
||||
if (row.getWeightMax() != null && (row.getCalcParam() == null || row.getCalcParam().isEmpty()))
|
||||
{
|
||||
row.setCalcParam(row.getWeightMax().stripTrailingZeros().toPlainString());
|
||||
}
|
||||
firsts.put(key, row);
|
||||
}
|
||||
else if (extra)
|
||||
{
|
||||
extras.add(band);
|
||||
}
|
||||
else
|
||||
{
|
||||
rest.add(band);
|
||||
}
|
||||
}
|
||||
for (LblWeightBand extra : extras)
|
||||
{
|
||||
String key = zoneFeeKey(extra);
|
||||
LblWeightBand row = firsts.get(key);
|
||||
if (row == null)
|
||||
{
|
||||
row = copyBand(extra);
|
||||
row.setExtraPrice(extra.getPrice());
|
||||
row.setExtraPriceUnit(extra.getPriceUnit());
|
||||
row.setPrice(null);
|
||||
row.setChargeMode(LblChargeMode.EXTRA_KG);
|
||||
firsts.put(key, row);
|
||||
}
|
||||
else
|
||||
{
|
||||
row.setExtraPrice(extra.getPrice());
|
||||
row.setExtraPriceUnit(extra.getPriceUnit());
|
||||
row.setChargeMode(LblChargeMode.EXTRA_KG);
|
||||
}
|
||||
}
|
||||
List<LblWeightBand> out = new ArrayList<LblWeightBand>();
|
||||
out.addAll(rest);
|
||||
out.addAll(firsts.values());
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String inferFeeType(LblWeightBand band)
|
||||
{
|
||||
String name = nz(band.getChargeName());
|
||||
if (nameContainsRemote(name))
|
||||
{
|
||||
return LblFeeType.REMOTE;
|
||||
}
|
||||
if (name.contains("处理费") || name.contains("挂号"))
|
||||
{
|
||||
return LblFeeType.REGISTRATION;
|
||||
}
|
||||
return LblFeeType.FREIGHT;
|
||||
}
|
||||
|
||||
private static String inferMode(LblWeightBand band)
|
||||
{
|
||||
if (band.getExtraPrice() != null)
|
||||
{
|
||||
return LblChargeMode.EXTRA_KG;
|
||||
}
|
||||
if (!nz(band.getPostcodeParam()).isEmpty())
|
||||
{
|
||||
return LblChargeMode.POSTCODE;
|
||||
}
|
||||
String unit = nz(band.getPriceUnit()).toUpperCase(Locale.ROOT);
|
||||
if (unit.contains("KG") || unit.contains("公斤") || unit.contains("千克"))
|
||||
{
|
||||
return LblChargeMode.PER_KG;
|
||||
}
|
||||
return LblChargeMode.ITEM;
|
||||
}
|
||||
|
||||
private static String migrateMode(String mode)
|
||||
{
|
||||
if (mode == null || mode.isEmpty())
|
||||
{
|
||||
return LblChargeMode.ITEM;
|
||||
}
|
||||
if (LblChargeMode.FIRST_EXTRA.equals(mode) || "EXTRA_KG".equals(mode))
|
||||
{
|
||||
return LblChargeMode.EXTRA_KG;
|
||||
}
|
||||
if (LblChargeMode.POSTCODE.equals(mode))
|
||||
{
|
||||
return LblChargeMode.POSTCODE;
|
||||
}
|
||||
if (LblChargeMode.PER_KG.equals(mode) || LblChargeMode.UNIT_PLUS_HANDLE.equals(mode))
|
||||
{
|
||||
return LblChargeMode.PER_KG;
|
||||
}
|
||||
if (LblChargeMode.ITEM.equals(mode) || LblChargeMode.FLAT.equals(mode) || LblChargeMode.ALWAYS.equals(mode)
|
||||
|| LblChargeMode.ZONE.equals(mode) || LblChargeMode.WEIGHT.equals(mode) || LblChargeMode.SIZE.equals(mode)
|
||||
|| LblChargeMode.RETURN.equals(mode) || LblChargeMode.MANUAL.equals(mode))
|
||||
{
|
||||
return LblChargeMode.ITEM;
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
private static String summarizeChargeType(List<LblWeightBand> bands)
|
||||
{
|
||||
boolean firstExtra = false;
|
||||
boolean unitKg = false;
|
||||
for (LblWeightBand band : bands)
|
||||
{
|
||||
if (LblChargeMode.EXTRA_KG.equals(band.getChargeMode()) || LblChargeMode.FIRST_EXTRA.equals(band.getChargeMode()))
|
||||
{
|
||||
firstExtra = true;
|
||||
}
|
||||
String unit = nz(band.getPriceUnit()).toUpperCase(Locale.ROOT);
|
||||
if (unit.contains("KG") || unit.contains("公斤"))
|
||||
{
|
||||
unitKg = true;
|
||||
}
|
||||
if (LblFeeType.REGISTRATION.equals(band.getFeeType()))
|
||||
{
|
||||
unitKg = true;
|
||||
}
|
||||
}
|
||||
if (firstExtra)
|
||||
{
|
||||
return "续重计费";
|
||||
}
|
||||
if (unitKg)
|
||||
{
|
||||
return "常规计重价";
|
||||
}
|
||||
return "单票价";
|
||||
}
|
||||
|
||||
private static String zoneFeeKey(LblWeightBand band)
|
||||
{
|
||||
return nz(band.getCountryCode()) + "|" + nz(band.getZoneName()) + "|" + nz(band.getPostcodeParam()) + "|"
|
||||
+ nz(band.getSectionName()) + "|" + nz(band.getFeeType());
|
||||
}
|
||||
|
||||
private static String toZoneCode(String zoneName)
|
||||
{
|
||||
if (zoneName == null || zoneName.trim().isEmpty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String s = zoneName.trim().toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]+", "_");
|
||||
if (s.length() > 64)
|
||||
{
|
||||
s = s.substring(0, 64);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static LblWeightBand copyBand(LblWeightBand src)
|
||||
{
|
||||
LblWeightBand b = new LblWeightBand();
|
||||
b.setProductId(src.getProductId());
|
||||
b.setVersionId(src.getVersionId());
|
||||
b.setSectionName(src.getSectionName());
|
||||
b.setFeeType(src.getFeeType());
|
||||
b.setCountryName(src.getCountryName());
|
||||
b.setCountryCode(src.getCountryCode());
|
||||
b.setZoneCode(src.getZoneCode());
|
||||
b.setZoneName(src.getZoneName());
|
||||
b.setPostcodeParam(src.getPostcodeParam());
|
||||
b.setWeightLabel(src.getWeightLabel());
|
||||
b.setWeightMin(src.getWeightMin());
|
||||
b.setWeightMax(src.getWeightMax());
|
||||
b.setVolumeText(src.getVolumeText());
|
||||
b.setChargeName(src.getChargeName());
|
||||
b.setChargeMode(src.getChargeMode());
|
||||
b.setCalcParam(src.getCalcParam());
|
||||
b.setPrice(src.getPrice());
|
||||
b.setPriceUnit(src.getPriceUnit());
|
||||
b.setExtraPrice(src.getExtraPrice());
|
||||
b.setExtraPriceUnit(src.getExtraPriceUnit());
|
||||
b.setSortNo(src.getSortNo());
|
||||
return b;
|
||||
}
|
||||
|
||||
private static String nz(String v)
|
||||
{
|
||||
return v == null ? "" : v.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
package com.bms.price.label;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import com.bms.common.enums.PaymentMesEnum;
|
||||
import com.bms.price.domain.LblChargeMode;
|
||||
import com.bms.price.domain.LblFeeType;
|
||||
import com.bms.price.domain.LblProduct;
|
||||
import com.bms.price.domain.LblWeightBand;
|
||||
import com.bms.price.domain.PayCostLine;
|
||||
import com.bms.price.domain.PayOrderCostResult;
|
||||
import com.bms.price.domain.PayProduct;
|
||||
import com.bms.price.pay.LabelArCostCalculator;
|
||||
import com.bms.price.pay.PaySurchargeRules;
|
||||
import com.bms.price.pay.PayZoneMatcher;
|
||||
|
||||
/**
|
||||
* 打单新计费:直读 {@link LblWeightBand},按 fee_type 分行 + charge_mode(=calc_rule)。
|
||||
* 附加费仍复用 {@link LabelArCostCalculator#appendSurcharges}。
|
||||
*/
|
||||
public final class LblCostCalculator
|
||||
{
|
||||
private static final Set<Integer> REFUND_STATUS_SET = Set.of(9, 10);
|
||||
|
||||
private LblCostCalculator()
|
||||
{
|
||||
}
|
||||
|
||||
public static PayOrderCostResult calculate(LblProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus)
|
||||
{
|
||||
PayOrderCostResult result = new PayOrderCostResult();
|
||||
result.setFees(new ArrayList<PayCostLine>());
|
||||
result.setSkipped(new ArrayList<String>());
|
||||
if (product == null)
|
||||
{
|
||||
result.setMessage(PaymentMesEnum.BMS_PAY_NO_PRODUCT.getCnMes());
|
||||
return result;
|
||||
}
|
||||
result.setProductId(product.getProductId());
|
||||
result.setProductName(product.getProductName());
|
||||
if (weight == null || weight.compareTo(BigDecimal.ZERO) <= 0)
|
||||
{
|
||||
result.setMessage(PaymentMesEnum.BMS_PAY_NO_WEIGHT.getCnMes());
|
||||
return result;
|
||||
}
|
||||
|
||||
List<LblWeightBand> all = product.getWeightBands() == null ? List.of() : product.getWeightBands();
|
||||
Set<String> zones = zoneNames(all);
|
||||
if (zones.size() > 1 && (zoneName == null || zoneName.isBlank()))
|
||||
{
|
||||
result.setMessage("产品「" + nz(product.getProductName()) + "」有多个地区("
|
||||
+ String.join("、", zones) + "),邮编没有对上其中的地区,无法确定运费");
|
||||
return result;
|
||||
}
|
||||
|
||||
String currency = nz(product.getCurrency());
|
||||
if (currency.isEmpty())
|
||||
{
|
||||
currency = "EUR";
|
||||
}
|
||||
|
||||
BigDecimal freight = BigDecimal.ZERO;
|
||||
String freightSection = "";
|
||||
freight = addFeeGroup(result, all, LblFeeType.FREIGHT, weight, length, width, height, postcode, zoneName,
|
||||
orderStatus, currency, true);
|
||||
if (result.getMessage() != null && !result.getMessage().isEmpty() && freight.compareTo(BigDecimal.ZERO) == 0
|
||||
&& result.getFees().isEmpty())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
for (PayCostLine fee : result.getFees())
|
||||
{
|
||||
if ("BASIC_FREIGHT".equals(fee.getFeeType()))
|
||||
{
|
||||
freightSection = "";
|
||||
break;
|
||||
}
|
||||
}
|
||||
addFeeGroup(result, all, LblFeeType.REGISTRATION, weight, length, width, height, postcode, zoneName,
|
||||
orderStatus, currency, false);
|
||||
addFeeGroup(result, all, LblFeeType.REMOTE, weight, length, width, height, postcode, zoneName,
|
||||
orderStatus, currency, false);
|
||||
|
||||
if (freight.compareTo(BigDecimal.ZERO) == 0)
|
||||
{
|
||||
boolean hasFreightLine = false;
|
||||
for (PayCostLine fee : result.getFees())
|
||||
{
|
||||
if ("BASIC_FREIGHT".equals(fee.getFeeType()))
|
||||
{
|
||||
hasFreightLine = true;
|
||||
freight = freight.add(fee.getAmount() == null ? BigDecimal.ZERO : fee.getAmount());
|
||||
}
|
||||
}
|
||||
if (!hasFreightLine)
|
||||
{
|
||||
result.setMessage(outOfRange(product, all, weight, zoneName));
|
||||
result.getFees().clear();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
result.setCurrency(currency);
|
||||
PayProduct pay = LblProductPayAdapter.toPayProductForSurcharge(product);
|
||||
LabelArCostCalculator.appendSurcharges(pay, weight, length, width, height, postcode, zoneName, bizDate,
|
||||
orderStatus, freight, currency, freightSection, null, result);
|
||||
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (PayCostLine fee : result.getFees())
|
||||
{
|
||||
if (fee != null && fee.getAmount() != null)
|
||||
{
|
||||
total = total.add(fee.getAmount());
|
||||
}
|
||||
}
|
||||
result.setPayTotal(total.setScale(2, RoundingMode.HALF_UP));
|
||||
result.setMessage(summary(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FREIGHT 组金额汇总;其它组返回 ZERO
|
||||
*/
|
||||
private static BigDecimal addFeeGroup(PayOrderCostResult result, List<LblWeightBand> all, String feeType,
|
||||
BigDecimal weight, BigDecimal length, BigDecimal width, BigDecimal height, String postcode, String zoneName,
|
||||
Integer orderStatus, String currency, boolean required)
|
||||
{
|
||||
List<LblWeightBand> candidates = filterCandidates(all, feeType, weight, zoneName);
|
||||
LblWeightBand best = pickBest(candidates, weight, postcode, zoneName, length, width, height, orderStatus);
|
||||
if (best == null)
|
||||
{
|
||||
if (required)
|
||||
{
|
||||
// message set by caller
|
||||
}
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
if (!modeApplies(best, weight, length, width, height, postcode, zoneName, orderStatus))
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
BigDecimal amount = amountOf(best, weight);
|
||||
if (amount == null)
|
||||
{
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
amount = amount.setScale(2, RoundingMode.HALF_UP);
|
||||
String quoteType = quoteFeeType(feeType);
|
||||
String feeName = blank(best.getChargeName(), defaultFeeName(feeType));
|
||||
String remark = nz(best.getWeightLabel()) + " " + plain(best.getPrice()) + " " + nz(best.getPriceUnit());
|
||||
if (zoneName != null && !zoneName.isBlank())
|
||||
{
|
||||
remark = remark + ",地区 " + zoneName.replace('\n', ' ');
|
||||
}
|
||||
result.getFees().add(line(quoteType, feeName, 0L, amount, currency, remark.trim()));
|
||||
return LblFeeType.FREIGHT.equals(feeType) ? amount : BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private static List<LblWeightBand> filterCandidates(List<LblWeightBand> all, String feeType, BigDecimal weight,
|
||||
String zoneName)
|
||||
{
|
||||
List<LblWeightBand> out = new ArrayList<LblWeightBand>();
|
||||
for (LblWeightBand band : all)
|
||||
{
|
||||
if (band == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String ft = nz(band.getFeeType());
|
||||
if (ft.isEmpty())
|
||||
{
|
||||
ft = LblFeeType.FREIGHT;
|
||||
}
|
||||
if (!feeType.equals(ft))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String mode = migrateMode(band.getChargeMode());
|
||||
boolean zoned = !nz(band.getZoneName()).isEmpty() || !nz(band.getZoneCode()).isEmpty();
|
||||
boolean postcodeDriven = LblChargeMode.POSTCODE.equals(mode)
|
||||
|| ((LblChargeMode.EXTRA_KG.equals(mode) || LblFeeType.REMOTE.equals(ft))
|
||||
&& !nz(band.getPostcodeParam()).isEmpty());
|
||||
if (zoned && zoneName != null && !zoneName.isBlank() && !postcodeDriven)
|
||||
{
|
||||
if (!PayZoneMatcher.sameZone(band.getZoneName(), zoneName)
|
||||
&& !PayZoneMatcher.sameZone(band.getZoneCode(), zoneName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (LblChargeMode.EXTRA_KG.equals(mode))
|
||||
{
|
||||
out.add(band);
|
||||
continue;
|
||||
}
|
||||
if (band.getWeightMin() != null && weight.compareTo(band.getWeightMin()) < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (band.getWeightMax() != null && weight.compareTo(band.getWeightMax()) > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
out.add(band);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static LblWeightBand pickBest(List<LblWeightBand> candidates, BigDecimal weight, String postcode,
|
||||
String zoneName, BigDecimal length, BigDecimal width, BigDecimal height, Integer orderStatus)
|
||||
{
|
||||
List<LblWeightBand> ok = new ArrayList<LblWeightBand>();
|
||||
for (LblWeightBand band : candidates)
|
||||
{
|
||||
if (modeApplies(band, weight, length, width, height, postcode, zoneName, orderStatus))
|
||||
{
|
||||
ok.add(band);
|
||||
}
|
||||
}
|
||||
if (ok.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
ok.sort(Comparator.comparingInt(LblCostCalculator::specificity).reversed());
|
||||
return ok.get(0);
|
||||
}
|
||||
|
||||
private static int specificity(LblWeightBand band)
|
||||
{
|
||||
int score = 0;
|
||||
if (!nz(band.getZoneName()).isEmpty() || !nz(band.getZoneCode()).isEmpty())
|
||||
{
|
||||
score += 4;
|
||||
}
|
||||
if (!nz(band.getPostcodeParam()).isEmpty())
|
||||
{
|
||||
score += 2;
|
||||
}
|
||||
if (band.getWeightMin() != null || band.getWeightMax() != null)
|
||||
{
|
||||
score += 1;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
private static boolean modeApplies(LblWeightBand band, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||
BigDecimal height, String postcode, String zoneName, Integer orderStatus)
|
||||
{
|
||||
String mode = migrateMode(band.getChargeMode());
|
||||
if (LblChargeMode.POSTCODE.equals(mode))
|
||||
{
|
||||
return LblSurchargeRules.matchesPostcode(band.getPostcodeParam(), postcode)
|
||||
|| PaySurchargeRules.matchesPostcode(band.getPostcodeParam(), postcode);
|
||||
}
|
||||
if (LblChargeMode.EXTRA_KG.equals(mode))
|
||||
{
|
||||
if (!nz(band.getPostcodeParam()).isEmpty()
|
||||
&& !PaySurchargeRules.matchesPostcode(band.getPostcodeParam(), postcode)
|
||||
&& !LblSurchargeRules.matchesPostcode(band.getPostcodeParam(), postcode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// ITEM / PER_KG:维度筛选后即适用
|
||||
return true;
|
||||
}
|
||||
|
||||
private static BigDecimal amountOf(LblWeightBand band, BigDecimal weight)
|
||||
{
|
||||
String mode = migrateMode(band.getChargeMode());
|
||||
if (LblChargeMode.EXTRA_KG.equals(mode))
|
||||
{
|
||||
BigDecimal first = firstKg(band);
|
||||
BigDecimal base = band.getPrice() == null ? BigDecimal.ZERO : band.getPrice();
|
||||
BigDecimal extra = band.getExtraPrice() == null ? BigDecimal.ZERO : band.getExtraPrice();
|
||||
if (weight == null)
|
||||
{
|
||||
return base;
|
||||
}
|
||||
BigDecimal over = weight.subtract(first);
|
||||
if (over.compareTo(BigDecimal.ZERO) < 0)
|
||||
{
|
||||
over = BigDecimal.ZERO;
|
||||
}
|
||||
return base.add(extra.multiply(over));
|
||||
}
|
||||
if (band.getPrice() == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (LblChargeMode.PER_KG.equals(mode))
|
||||
{
|
||||
return band.getPrice().multiply(weight);
|
||||
}
|
||||
// ITEM / POSTCODE:按票
|
||||
return band.getPrice();
|
||||
}
|
||||
|
||||
private static BigDecimal firstKg(LblWeightBand band)
|
||||
{
|
||||
if (band.getCalcParam() != null && !band.getCalcParam().isBlank())
|
||||
{
|
||||
try
|
||||
{
|
||||
return new BigDecimal(band.getCalcParam().trim());
|
||||
}
|
||||
catch (NumberFormatException ignored)
|
||||
{
|
||||
}
|
||||
}
|
||||
if (band.getWeightMax() != null)
|
||||
{
|
||||
return band.getWeightMax();
|
||||
}
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
private static boolean perKilogram(LblWeightBand band)
|
||||
{
|
||||
String unit = nz(band.getPriceUnit()).toUpperCase(Locale.ROOT);
|
||||
return unit.contains("KG") || unit.contains("公斤") || unit.contains("千克");
|
||||
}
|
||||
|
||||
private static String migrateMode(String mode)
|
||||
{
|
||||
if (mode == null || mode.isEmpty())
|
||||
{
|
||||
return LblChargeMode.ITEM;
|
||||
}
|
||||
if (LblChargeMode.FIRST_EXTRA.equals(mode))
|
||||
{
|
||||
return LblChargeMode.EXTRA_KG;
|
||||
}
|
||||
if (LblChargeMode.UNIT_PLUS_HANDLE.equals(mode))
|
||||
{
|
||||
return LblChargeMode.PER_KG;
|
||||
}
|
||||
if (LblChargeMode.FLAT.equals(mode) || LblChargeMode.ALWAYS.equals(mode) || LblChargeMode.ZONE.equals(mode)
|
||||
|| LblChargeMode.WEIGHT.equals(mode) || LblChargeMode.SIZE.equals(mode) || LblChargeMode.RETURN.equals(mode)
|
||||
|| LblChargeMode.MANUAL.equals(mode))
|
||||
{
|
||||
return LblChargeMode.ITEM;
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
private static String quoteFeeType(String feeType)
|
||||
{
|
||||
if (LblFeeType.REGISTRATION.equals(feeType))
|
||||
{
|
||||
return "REGISTRATION_FEE";
|
||||
}
|
||||
if (LblFeeType.REMOTE.equals(feeType))
|
||||
{
|
||||
return "SURCHARGE";
|
||||
}
|
||||
return "BASIC_FREIGHT";
|
||||
}
|
||||
|
||||
private static String defaultFeeName(String feeType)
|
||||
{
|
||||
if (LblFeeType.REGISTRATION.equals(feeType))
|
||||
{
|
||||
return "处理费";
|
||||
}
|
||||
if (LblFeeType.REMOTE.equals(feeType))
|
||||
{
|
||||
return "偏远费";
|
||||
}
|
||||
return "基础运费";
|
||||
}
|
||||
|
||||
private static Set<String> zoneNames(List<LblWeightBand> bands)
|
||||
{
|
||||
Set<String> zones = new LinkedHashSet<String>();
|
||||
for (LblWeightBand band : bands)
|
||||
{
|
||||
if (band != null && band.getZoneName() != null && !band.getZoneName().isBlank())
|
||||
{
|
||||
zones.add(band.getZoneName().trim());
|
||||
}
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
private static String outOfRange(LblProduct product, List<LblWeightBand> bands, BigDecimal weight, String zoneName)
|
||||
{
|
||||
String name = nz(product.getProductName());
|
||||
List<LblWeightBand> freight = filterCandidates(bands, LblFeeType.FREIGHT, weight, zoneName);
|
||||
if (freight.isEmpty())
|
||||
{
|
||||
// try without weight filter for message
|
||||
int n = 0;
|
||||
BigDecimal max = null;
|
||||
for (LblWeightBand band : bands)
|
||||
{
|
||||
if (band == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String ft = nz(band.getFeeType());
|
||||
if (!ft.isEmpty() && !LblFeeType.FREIGHT.equals(ft))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
n++;
|
||||
if (band.getWeightMax() != null && (max == null || band.getWeightMax().compareTo(max) > 0))
|
||||
{
|
||||
max = band.getWeightMax();
|
||||
}
|
||||
}
|
||||
if (n == 0)
|
||||
{
|
||||
return "产品「" + name + "」没有可匹配的重量段";
|
||||
}
|
||||
if (max != null && weight.compareTo(max) > 0)
|
||||
{
|
||||
return "计费重 " + plain(weight) + "kg 超出「" + name + "」的重量段,最大 " + plain(max) + "kg";
|
||||
}
|
||||
}
|
||||
return "计费重 " + plain(weight) + "kg 没有落在「" + name + "」的重量段里";
|
||||
}
|
||||
|
||||
private static String summary(PayOrderCostResult result)
|
||||
{
|
||||
if (result.getFees() == null || result.getFees().isEmpty())
|
||||
{
|
||||
return nz(result.getMessage());
|
||||
}
|
||||
return "计费成功,共 " + result.getFees().size() + " 项";
|
||||
}
|
||||
|
||||
private static PayCostLine line(String feeType, String feeName, long scId, BigDecimal amount, String currency,
|
||||
String remark)
|
||||
{
|
||||
PayCostLine line = new PayCostLine();
|
||||
line.setFeeType(feeType);
|
||||
line.setFeeName(feeName);
|
||||
line.setScId(scId);
|
||||
line.setAmount(amount);
|
||||
line.setCurrency(currency);
|
||||
line.setRemark(remark);
|
||||
return line;
|
||||
}
|
||||
|
||||
private static String blank(String v, String fallback)
|
||||
{
|
||||
return v == null || v.trim().isEmpty() ? fallback : v.trim();
|
||||
}
|
||||
|
||||
private static String nz(String v)
|
||||
{
|
||||
return v == null ? "" : v.trim();
|
||||
}
|
||||
|
||||
private static String plain(BigDecimal v)
|
||||
{
|
||||
return v == null ? "" : v.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.bms.price.label;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.bms.price.domain.LblProduct;
|
||||
import com.bms.price.domain.LblSurcharge;
|
||||
import com.bms.price.domain.LblWeightBand;
|
||||
import com.bms.price.domain.PayProduct;
|
||||
import com.bms.price.domain.PaySurcharge;
|
||||
import com.bms.price.domain.PayWeightBand;
|
||||
|
||||
/**
|
||||
* 兼容适配:打单产品转 PayProduct(附加费/旧对照)。
|
||||
* 计费主路请用 {@link LblCostCalculator}。
|
||||
*/
|
||||
public final class LblProductPayAdapter
|
||||
{
|
||||
private LblProductPayAdapter()
|
||||
{
|
||||
}
|
||||
|
||||
public static PayProduct toPayProduct(LblProduct src)
|
||||
{
|
||||
if (src == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
PayProduct p = new PayProduct();
|
||||
p.setProductId(src.getProductId());
|
||||
p.setProductName(src.getProductName());
|
||||
p.setDictValue(src.getDictValue());
|
||||
p.setChargeType(src.getChargeType());
|
||||
p.setExtraPerKg(src.getExtraPerKg());
|
||||
p.setCurrency(src.getCurrency());
|
||||
p.setCountryScope(src.getCountryScope());
|
||||
p.setTermId(src.getVersionId());
|
||||
p.setVersionNo(src.getVersionNo());
|
||||
p.setEffectiveDate(src.getEffectiveDate());
|
||||
p.setTermStart(src.getVersionStart());
|
||||
p.setTermEnd(src.getVersionEnd());
|
||||
List<PayWeightBand> bands = new ArrayList<PayWeightBand>();
|
||||
if (src.getWeightBands() != null)
|
||||
{
|
||||
for (LblWeightBand band : src.getWeightBands())
|
||||
{
|
||||
if (band == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bands.add(toBand(band, blank(band.getChargeName(), "派送费"), band.getPrice(), band.getPriceUnit()));
|
||||
}
|
||||
}
|
||||
p.setWeightBands(bands);
|
||||
p.setSurcharges(toSurcharges(src));
|
||||
return p;
|
||||
}
|
||||
|
||||
/** 仅转附加费,供新计费追加附加费。 */
|
||||
public static PayProduct toPayProductForSurcharge(LblProduct src)
|
||||
{
|
||||
PayProduct p = toPayProduct(src);
|
||||
if (p != null)
|
||||
{
|
||||
p.setWeightBands(new ArrayList<PayWeightBand>());
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
private static List<PaySurcharge> toSurcharges(LblProduct src)
|
||||
{
|
||||
List<PaySurcharge> fees = new ArrayList<PaySurcharge>();
|
||||
if (src.getSurcharges() == null)
|
||||
{
|
||||
return fees;
|
||||
}
|
||||
for (LblSurcharge fee : src.getSurcharges())
|
||||
{
|
||||
if (fee == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
PaySurcharge s = new PaySurcharge();
|
||||
s.setSurchargeId(fee.getSurchargeId());
|
||||
s.setProductId(fee.getProductId());
|
||||
s.setTermId(fee.getVersionId());
|
||||
s.setFeeName(fee.getFeeName());
|
||||
s.setScId(fee.getScId());
|
||||
s.setZoneName(fee.getZoneName());
|
||||
s.setChargeText(fee.getChargeText());
|
||||
s.setAmount(fee.getAmount());
|
||||
s.setCurrency(fee.getCurrency());
|
||||
s.setPriceUnit(fee.getPriceUnit());
|
||||
s.setFeeDesc(fee.getFeeDesc());
|
||||
s.setPeriodText(fee.getPeriodText());
|
||||
s.setEffectiveStart(fee.getEffectiveStart());
|
||||
s.setEffectiveEnd(fee.getEffectiveEnd());
|
||||
s.setChargeTiming(fee.getChargeTiming());
|
||||
s.setCalcRule(fee.getCalcRule());
|
||||
s.setCalcParam(fee.getCalcParam());
|
||||
s.setPostcodeParam(fee.getPostcodeParam());
|
||||
s.setWeightMin(fee.getWeightMin());
|
||||
s.setWeightMax(fee.getWeightMax());
|
||||
s.setSortNo(fee.getSortNo());
|
||||
fees.add(s);
|
||||
}
|
||||
return fees;
|
||||
}
|
||||
|
||||
private static PayWeightBand toBand(LblWeightBand src, String chargeName, java.math.BigDecimal price, String unit)
|
||||
{
|
||||
PayWeightBand b = new PayWeightBand();
|
||||
b.setProductId(src.getProductId());
|
||||
b.setTermId(src.getVersionId());
|
||||
b.setSectionName(src.getSectionName());
|
||||
b.setCountryName(src.getCountryName());
|
||||
b.setCountryCode(src.getCountryCode());
|
||||
b.setZoneName(blank(src.getZoneName(), src.getZoneCode()));
|
||||
b.setWeightLabel(src.getWeightLabel());
|
||||
b.setWeightMin(src.getWeightMin());
|
||||
b.setWeightMax(src.getWeightMax());
|
||||
b.setVolumeText(src.getVolumeText());
|
||||
b.setChargeName(chargeName);
|
||||
b.setPrice(price);
|
||||
b.setPriceUnit(unit);
|
||||
b.setSortNo(src.getSortNo());
|
||||
return b;
|
||||
}
|
||||
|
||||
private static String blank(String v, String fallback)
|
||||
{
|
||||
return v == null || v.trim().isEmpty() ? fallback : v.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
package com.bms.price.label;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import com.bms.price.domain.LblSurcharge;
|
||||
|
||||
/**
|
||||
* 把附加费说明收成可计算的条件。认不出的标为暂不计费,避免把签名、重复单号当成每票必收。
|
||||
*/
|
||||
public final class LblSurchargeRules
|
||||
{
|
||||
public static final String ALWAYS = "ALWAYS";
|
||||
|
||||
public static final String POSTCODE = "POSTCODE";
|
||||
|
||||
public static final String WEIGHT = "WEIGHT";
|
||||
|
||||
public static final String SIZE = "SIZE";
|
||||
|
||||
public static final String RETURN = "RETURN";
|
||||
|
||||
public static final String MANUAL = "MANUAL";
|
||||
|
||||
/** 超过重量段上限后,按超出的公斤数乘单价 */
|
||||
public static final String EXTRA_KG = "EXTRA_KG";
|
||||
|
||||
/** 邮编已经落到这个分区,再按重量区间收取,例如英国高地 */
|
||||
public static final String ZONE = "ZONE";
|
||||
|
||||
private static final Pattern REMOTE_PREFIX = Pattern.compile("(\\d{1,5}(?:\\s*[、,,]\\s*\\d{1,5})*)\\s*邮编");
|
||||
|
||||
private static final Pattern RANGE = Pattern.compile("([A-Z]{1,2})(\\d{1,2})\\s*-\\s*(\\d{1,2})");
|
||||
|
||||
private static final Pattern DISTRICT = Pattern.compile("([A-Z]{1,2})(\\d{1,2})");
|
||||
|
||||
private static final Pattern AREA = Pattern.compile("^[A-Z]{1,2}$");
|
||||
|
||||
private static final Pattern UK_FULL = Pattern.compile("^([A-Z]{1,2})(\\d{1,2})[A-Z]?\\d[A-Z]{2}$");
|
||||
|
||||
private static final Pattern UK_OUTWARD = Pattern.compile("^([A-Z]{1,2})(\\d{1,2})$");
|
||||
|
||||
private static final Pattern BOX = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*\\*\\s*(\\d+(?:\\.\\d+)?)\\s*\\*\\s*(\\d+(?:\\.\\d+)?)");
|
||||
|
||||
private static final Pattern VOLUME = Pattern.compile("(?i)(\\d+(?:\\.\\d+)?)\\s*(?:m3|m³|立方米)");
|
||||
|
||||
private static final Pattern CM = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*(?:cm|厘米)");
|
||||
|
||||
private static final Pattern KG = Pattern.compile("(?i)(\\d+(?:\\.\\d+)?)\\s*(?:kg|公斤|千克)");
|
||||
|
||||
private LblSurchargeRules()
|
||||
{
|
||||
}
|
||||
|
||||
public static void apply(LblSurcharge fee)
|
||||
{
|
||||
if (fee == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
String name = fee.getFeeName() == null ? "" : fee.getFeeName();
|
||||
String desc = fee.getFeeDesc() == null ? "" : fee.getFeeDesc();
|
||||
String charge = fee.getChargeText() == null ? "" : fee.getChargeText();
|
||||
String blob = name + " " + desc + " " + charge;
|
||||
if (name.contains("报价说明"))
|
||||
{
|
||||
fee.setCalcRule(MANUAL);
|
||||
return;
|
||||
}
|
||||
if (name.contains("退件"))
|
||||
{
|
||||
fee.setCalcRule(RETURN);
|
||||
return;
|
||||
}
|
||||
if (name.contains("签名") || name.contains("重复") || name.contains("未预报") || name.contains("重新贴"))
|
||||
{
|
||||
fee.setCalcRule(MANUAL);
|
||||
return;
|
||||
}
|
||||
String postcodes = postcodeParam(desc);
|
||||
if (name.contains("偏远") && !postcodes.isEmpty())
|
||||
{
|
||||
fee.setCalcRule(POSTCODE);
|
||||
fee.setCalcParam(postcodes);
|
||||
return;
|
||||
}
|
||||
Matcher kg = KG.matcher(blob);
|
||||
if ((name.contains("超重") || blob.contains("重量>") || blob.toLowerCase(Locale.ROOT).contains("weight")) && kg.find())
|
||||
{
|
||||
fee.setCalcRule(WEIGHT);
|
||||
fee.setCalcParam(kg.group(1));
|
||||
return;
|
||||
}
|
||||
String size = sizeParam(blob);
|
||||
if (!size.isEmpty() && (name.contains("超大") || name.contains("超尺寸") || name.contains("特大") || blob.contains("厘米") || blob.toLowerCase(Locale.ROOT).contains("cm")))
|
||||
{
|
||||
fee.setCalcRule(SIZE);
|
||||
fee.setCalcParam(size);
|
||||
return;
|
||||
}
|
||||
if (blob.contains("固定收取") || "固定收取".equals(fee.getChargeTiming()))
|
||||
{
|
||||
fee.setCalcRule(ALWAYS);
|
||||
return;
|
||||
}
|
||||
fee.setCalcRule(MANUAL);
|
||||
}
|
||||
|
||||
public static boolean matchesPostcode(String param, String postcode)
|
||||
{
|
||||
if (param == null || param.isBlank() || postcode == null || postcode.isBlank())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
UkPostcode parsed = parseUk(postcode);
|
||||
String digits = postcode.replaceAll("\\D", "");
|
||||
for (String token : param.split(","))
|
||||
{
|
||||
String value = token.trim().toUpperCase(Locale.ROOT);
|
||||
if (value.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String compact = value.replaceAll("[^A-Z0-9]", "");
|
||||
String whole = postcode.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", "");
|
||||
if (!compact.isEmpty() && compact.equals(whole))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (parsed != null && tokenMatches(value, parsed))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (parsed == null && value.matches("\\d{1,5}") && !digits.isEmpty())
|
||||
{
|
||||
boolean full = value.length() >= 4 ? digits.equals(value) : digits.startsWith(value);
|
||||
if (full)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 从“07邮编开头”“35、38邮编开头”取出数字前缀。没有邮编字样时不猜。 */
|
||||
public static String remotePrefixes(String text)
|
||||
{
|
||||
if (text == null || !text.contains("邮编"))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
List<String> tokens = new ArrayList<>();
|
||||
Matcher matcher = REMOTE_PREFIX.matcher(text);
|
||||
while (matcher.find())
|
||||
{
|
||||
for (String part : matcher.group(1).split("[、,,\\s]+"))
|
||||
{
|
||||
if (part.matches("\\d{1,5}") && !tokens.contains(part))
|
||||
{
|
||||
tokens.add(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
return String.join(",", tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按附加费配置里的超尺寸明细判断。上限表示超过这个值才命中。箱规按从大到小比,任一边超过对应边即命中。
|
||||
*/
|
||||
public static boolean matchesConfiguredSize(java.util.Map<String, Object> detail, BigDecimal length, BigDecimal width,
|
||||
BigDecimal height, BigDecimal weight)
|
||||
{
|
||||
if (detail == null || detail.isEmpty() || length == null || width == null || height == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
BigDecimal weightMin = number(detail.get("weightMin"));
|
||||
BigDecimal weightMax = number(detail.get("weightMax"));
|
||||
if (weightMin != null || weightMax != null)
|
||||
{
|
||||
if (weight == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (weightMin != null && weight.compareTo(weightMin) <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (weightMax != null && weight.compareTo(weightMax) > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
BigDecimal[] sides = desc(length, width, height);
|
||||
List<Boolean> checks = new ArrayList<>();
|
||||
addBound(checks, sides[0], detail.get("longestMin"), detail.get("longestMax"));
|
||||
addBound(checks, sides[1], detail.get("secondMin"), detail.get("secondMax"));
|
||||
addBound(checks, length.add(width).add(height), detail.get("sumMin"), detail.get("sumMax"));
|
||||
addBound(checks, sides[0].add(sides[1].add(sides[2]).multiply(new BigDecimal("2"))), detail.get("girthMin"), detail.get("girthMax"));
|
||||
BigDecimal volumeMin = number(detail.get("volumeMin"));
|
||||
BigDecimal volumeMax = number(detail.get("volumeMax"));
|
||||
if (volumeMin != null || volumeMax != null)
|
||||
{
|
||||
BigDecimal cubic = length.multiply(width).multiply(height).divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
|
||||
addBound(checks, cubic, volumeMin, volumeMax);
|
||||
}
|
||||
BigDecimal cubicMax = number(detail.get("cubicMax"));
|
||||
if (cubicMax != null)
|
||||
{
|
||||
checks.add(length.multiply(width).multiply(height).compareTo(cubicMax) > 0);
|
||||
}
|
||||
BigDecimal boxLength = number(detail.get("boxLength"));
|
||||
BigDecimal boxWidth = number(detail.get("boxWidth"));
|
||||
BigDecimal boxHeight = number(detail.get("boxHeight"));
|
||||
if (boxLength != null && boxWidth != null && boxHeight != null)
|
||||
{
|
||||
boolean under = "UNDER".equals(String.valueOf(detail.get("boxMode")));
|
||||
checks.add(under ? withinBox(sides, boxLength, boxWidth, boxHeight) : exceedsBox(sides, boxLength, boxWidth, boxHeight));
|
||||
}
|
||||
if (checks.isEmpty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if ("ALL".equals(String.valueOf(detail.get("matchMode"))))
|
||||
{
|
||||
return !checks.contains(Boolean.FALSE);
|
||||
}
|
||||
return checks.contains(Boolean.TRUE);
|
||||
}
|
||||
|
||||
/** 每票都收。写了费用名的规则只约束这一项;带生效月日的规则只在这段日期内收取。 */
|
||||
public static boolean allowsAlways(List<java.util.Map<String, Object>> details, String feeName, java.util.Date bizDate)
|
||||
{
|
||||
if (details == null || details.isEmpty())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
String name = feeName == null ? "" : feeName.trim();
|
||||
List<java.util.Map<String, Object>> named = new ArrayList<>();
|
||||
List<java.util.Map<String, Object>> shared = new ArrayList<>();
|
||||
for (java.util.Map<String, Object> detail : details)
|
||||
{
|
||||
if (detail == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String ruleName = detail.get("feeName") == null ? "" : String.valueOf(detail.get("feeName")).trim();
|
||||
if (ruleName.isEmpty())
|
||||
{
|
||||
shared.add(detail);
|
||||
}
|
||||
else if (ruleName.equals(name))
|
||||
{
|
||||
named.add(detail);
|
||||
}
|
||||
}
|
||||
List<java.util.Map<String, Object>> mine = named.isEmpty() ? shared : named;
|
||||
if (mine.isEmpty())
|
||||
{
|
||||
return shared.isEmpty();
|
||||
}
|
||||
java.time.LocalDate day = bizDate == null ? null : bizDate.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
|
||||
for (java.util.Map<String, Object> detail : mine)
|
||||
{
|
||||
if (!hasPeriod(detail))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (day != null && inPeriod(day, detail))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void addBound(List<Boolean> checks, BigDecimal actual, Object min, Object max)
|
||||
{
|
||||
BigDecimal lower = min instanceof BigDecimal ? (BigDecimal) min : number(min);
|
||||
BigDecimal upper = max instanceof BigDecimal ? (BigDecimal) max : number(max);
|
||||
if (lower == null && upper == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (lower != null && upper != null)
|
||||
{
|
||||
checks.add(actual.compareTo(lower) > 0 && actual.compareTo(upper) <= 0);
|
||||
}
|
||||
else if (upper != null)
|
||||
{
|
||||
checks.add(actual.compareTo(upper) > 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
checks.add(actual.compareTo(lower) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean withinBox(BigDecimal[] parcel, BigDecimal a, BigDecimal b, BigDecimal c)
|
||||
{
|
||||
BigDecimal[] limit = desc(a, b, c);
|
||||
return parcel[0].compareTo(limit[0]) < 0 && parcel[1].compareTo(limit[1]) < 0 && parcel[2].compareTo(limit[2]) < 0;
|
||||
}
|
||||
|
||||
private static boolean hasPeriod(java.util.Map<String, Object> detail)
|
||||
{
|
||||
return text(detail.get("periodStart")) != null || text(detail.get("periodEnd")) != null;
|
||||
}
|
||||
|
||||
private static boolean inPeriod(java.time.LocalDate day, java.util.Map<String, Object> detail)
|
||||
{
|
||||
int[] start = monthDay(text(detail.get("periodStart")));
|
||||
int[] end = monthDay(text(detail.get("periodEnd")));
|
||||
int current = day.getMonthValue() * 100 + day.getDayOfMonth();
|
||||
int from = start == null ? 101 : start[0] * 100 + start[1];
|
||||
int to = end == null ? 1231 : end[0] * 100 + end[1];
|
||||
if (from <= to)
|
||||
{
|
||||
return current >= from && current <= to;
|
||||
}
|
||||
return current >= from || current <= to;
|
||||
}
|
||||
|
||||
private static int[] monthDay(String text)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String[] parts = text.split("-");
|
||||
try
|
||||
{
|
||||
if (parts.length == 2)
|
||||
{
|
||||
return new int[] { Integer.parseInt(parts[0]), Integer.parseInt(parts[1]) };
|
||||
}
|
||||
if (parts.length == 3)
|
||||
{
|
||||
return new int[] { Integer.parseInt(parts[1]), Integer.parseInt(parts[2]) };
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException ex)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String text(Object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() ? null : text;
|
||||
}
|
||||
|
||||
private static BigDecimal number(Object value)
|
||||
{
|
||||
if (value == null || (value instanceof String text && text.isBlank()))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (value instanceof BigDecimal decimal)
|
||||
{
|
||||
return decimal;
|
||||
}
|
||||
try
|
||||
{
|
||||
return new BigDecimal(String.valueOf(value).trim());
|
||||
}
|
||||
catch (NumberFormatException ex)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean matchesWeight(String param, BigDecimal weight)
|
||||
{
|
||||
if (param == null || param.isBlank() || weight == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
return weight.compareTo(new BigDecimal(param.trim())) > 0;
|
||||
}
|
||||
catch (NumberFormatException ex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 长宽高单位厘米。箱规按从大到小比较,体积用立方米,总和是三边相加。 */
|
||||
public static boolean matchesSize(String param, BigDecimal length, BigDecimal width, BigDecimal height)
|
||||
{
|
||||
if (param == null || param.isBlank() || length == null || width == null || height == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
BigDecimal[] sides = desc(length, width, height);
|
||||
boolean sideHit = false;
|
||||
boolean sumHit = false;
|
||||
boolean sideSeen = false;
|
||||
boolean sumSeen = false;
|
||||
String join = "or";
|
||||
for (String part : param.split(";"))
|
||||
{
|
||||
String[] pair = part.split(":", 2);
|
||||
if (pair.length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String key = pair[0].trim();
|
||||
String value = pair[1].trim();
|
||||
if ("join".equals(key))
|
||||
{
|
||||
join = value;
|
||||
continue;
|
||||
}
|
||||
if ("box".equals(key))
|
||||
{
|
||||
String[] dims = value.split("\\*");
|
||||
if (dims.length == 3 && exceedsBox(sides, decimal(dims[0]), decimal(dims[1]), decimal(dims[2])))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if ("volume".equals(key))
|
||||
{
|
||||
BigDecimal cubic = length.multiply(width).multiply(height)
|
||||
.divide(new BigDecimal("1000000"), 6, RoundingMode.HALF_UP);
|
||||
if (cubic.compareTo(decimal(value)) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if ("side".equals(key))
|
||||
{
|
||||
sideSeen = true;
|
||||
sideHit = sides[0].compareTo(decimal(value)) > 0;
|
||||
}
|
||||
else if ("sum".equals(key))
|
||||
{
|
||||
sumSeen = true;
|
||||
sumHit = length.add(width).add(height).compareTo(decimal(value)) > 0;
|
||||
}
|
||||
}
|
||||
if (sideSeen || sumSeen)
|
||||
{
|
||||
if ("and".equals(join))
|
||||
{
|
||||
return (!sideSeen || sideHit) && (!sumSeen || sumHit);
|
||||
}
|
||||
return sideHit || sumHit;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String postcodeParam(String desc)
|
||||
{
|
||||
if (desc == null || desc.isBlank())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
int cut = Math.max(desc.lastIndexOf(';'), desc.lastIndexOf(';'));
|
||||
String source = cut >= 0 ? desc.substring(cut + 1) : desc;
|
||||
String upper = source.replace("邮编开头", " ").toUpperCase(Locale.ROOT).replace(',', ',').replace('、', ',').replace('.', ',');
|
||||
List<String> tokens = new ArrayList<>();
|
||||
for (String raw : upper.split("[,\\s]+"))
|
||||
{
|
||||
String token = raw.trim();
|
||||
if (token.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (RANGE.matcher(token).matches() || DISTRICT.matcher(token).matches() || AREA.matcher(token).matches())
|
||||
{
|
||||
tokens.add(token);
|
||||
}
|
||||
}
|
||||
return String.join(",", tokens);
|
||||
}
|
||||
|
||||
private static String sizeParam(String blob)
|
||||
{
|
||||
StringBuilder param = new StringBuilder();
|
||||
Matcher box = BOX.matcher(blob);
|
||||
if (box.find())
|
||||
{
|
||||
param.append("box:").append(box.group(1)).append('*').append(box.group(2)).append('*').append(box.group(3));
|
||||
}
|
||||
Matcher volume = VOLUME.matcher(blob);
|
||||
if (volume.find())
|
||||
{
|
||||
if (param.length() > 0)
|
||||
{
|
||||
param.append(';');
|
||||
}
|
||||
param.append("volume:").append(volume.group(1));
|
||||
}
|
||||
List<String> cms = new ArrayList<>();
|
||||
Matcher cm = CM.matcher(blob);
|
||||
while (cm.find())
|
||||
{
|
||||
cms.add(cm.group(1));
|
||||
}
|
||||
if (cms.size() >= 1 && (blob.contains("一边") || blob.toLowerCase(Locale.ROOT).contains("length") || blob.contains("最长")))
|
||||
{
|
||||
if (param.length() > 0)
|
||||
{
|
||||
param.append(';');
|
||||
}
|
||||
param.append("side:").append(cms.get(0));
|
||||
if (cms.size() >= 2 && (blob.contains("总和") || blob.toLowerCase(Locale.ROOT).contains("sum")))
|
||||
{
|
||||
param.append(";sum:").append(cms.get(1));
|
||||
param.append(";join:").append(join(blob));
|
||||
}
|
||||
}
|
||||
return param.toString();
|
||||
}
|
||||
|
||||
private static String join(String blob)
|
||||
{
|
||||
if (blob.contains("或") || Pattern.compile("(?i)\\bor\\b").matcher(blob).find())
|
||||
{
|
||||
return "or";
|
||||
}
|
||||
return "and";
|
||||
}
|
||||
|
||||
private static boolean tokenMatches(String token, UkPostcode postcode)
|
||||
{
|
||||
Matcher range = RANGE.matcher(token);
|
||||
if (range.matches())
|
||||
{
|
||||
int low = Integer.parseInt(range.group(2));
|
||||
int high = Integer.parseInt(range.group(3));
|
||||
return postcode.area.equals(range.group(1)) && postcode.district >= low && postcode.district <= high;
|
||||
}
|
||||
Matcher district = DISTRICT.matcher(token);
|
||||
if (district.matches())
|
||||
{
|
||||
return postcode.area.equals(district.group(1)) && postcode.district == Integer.parseInt(district.group(2));
|
||||
}
|
||||
return AREA.matcher(token).matches() && postcode.area.equals(token);
|
||||
}
|
||||
|
||||
private static UkPostcode parseUk(String postcode)
|
||||
{
|
||||
if (postcode == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String compact = postcode.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", "");
|
||||
Matcher full = UK_FULL.matcher(compact);
|
||||
if (full.matches())
|
||||
{
|
||||
return new UkPostcode(full.group(1), Integer.parseInt(full.group(2)));
|
||||
}
|
||||
Matcher outward = UK_OUTWARD.matcher(compact);
|
||||
if (outward.matches())
|
||||
{
|
||||
return new UkPostcode(outward.group(1), Integer.parseInt(outward.group(2)));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean exceedsBox(BigDecimal[] parcel, BigDecimal a, BigDecimal b, BigDecimal c)
|
||||
{
|
||||
BigDecimal[] limit = desc(a, b, c);
|
||||
return parcel[0].compareTo(limit[0]) > 0 || parcel[1].compareTo(limit[1]) > 0 || parcel[2].compareTo(limit[2]) > 0;
|
||||
}
|
||||
|
||||
private static BigDecimal[] desc(BigDecimal a, BigDecimal b, BigDecimal c)
|
||||
{
|
||||
BigDecimal[] values = new BigDecimal[] { a, b, c };
|
||||
for (int i = 0; i < values.length; i++)
|
||||
{
|
||||
for (int j = i + 1; j < values.length; j++)
|
||||
{
|
||||
if (values[j].compareTo(values[i]) > 0)
|
||||
{
|
||||
BigDecimal swap = values[i];
|
||||
values[i] = values[j];
|
||||
values[j] = swap;
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static BigDecimal decimal(String text)
|
||||
{
|
||||
return new BigDecimal(text.trim());
|
||||
}
|
||||
|
||||
private static final class UkPostcode
|
||||
{
|
||||
private final String area;
|
||||
|
||||
private final int district;
|
||||
|
||||
private UkPostcode(String area, int district)
|
||||
{
|
||||
this.area = area;
|
||||
this.district = district;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package com.bms.price.label;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 价卡单元格里的重量段、金额和时效原文解析。供导入预览和保存共用。
|
||||
*/
|
||||
public final class LblTexts
|
||||
{
|
||||
private static final Pattern WEIGHT_RANGE = Pattern.compile(
|
||||
"(?i)(\\d+(?:\\.\\d+)?)\\s*[<<]\\s*[wW]\\s*[≤<=<]\\s*(\\d+(?:\\.\\d+)?)");
|
||||
|
||||
private static final Pattern WEIGHT_DASH = Pattern.compile(
|
||||
"(?i)(\\d+(?:\\.\\d+)?)\\s*[-~~]\\s*(\\d+(?:\\.\\d+)?)\\s*(?:kg|公斤)?");
|
||||
|
||||
private static final Pattern FIRST_WEIGHT = Pattern.compile("(?i)首重\\s*(\\d+(?:\\.\\d+)?)\\s*(?:kg|公斤)?");
|
||||
|
||||
private static final Pattern STEP_WEIGHT = Pattern.compile("(?i)每增加\\s*(\\d+(?:\\.\\d+)?)");
|
||||
|
||||
private static final Pattern ONLY_KG = Pattern.compile("(?i)^\\s*(\\d+(?:\\.\\d+)?)\\s*(?:kg|公斤)\\s*$");
|
||||
|
||||
private static final Pattern CN_PERIOD = Pattern.compile(
|
||||
"(20\\d{2})\\s*年\\s*(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*[日号]?\\s*[至到\\-~~]+\\s*(?:(20\\d{2})\\s*年\\s*)?(\\d{1,2})\\s*月\\s*(\\d{1,2})\\s*[日号]?");
|
||||
|
||||
private static final Pattern CN_MONTH = Pattern.compile(
|
||||
"(20\\d{2})\\s*年\\s*(\\d{1,2})\\s*月\\s*[至到\\-~~]+\\s*(\\d{1,2})\\s*月");
|
||||
|
||||
private static final Pattern ISO_DATE = Pattern.compile("(20\\d{2})[./-](\\d{1,2})[./-](\\d{1,2})");
|
||||
|
||||
/** 20260928-20270117 这种连在一起的日期窗口 */
|
||||
private static final Pattern COMPACT_PERIOD = Pattern.compile("(20\\d{2})(\\d{2})(\\d{2})\\s*-\\s*(20\\d{2})(\\d{2})(\\d{2})");
|
||||
|
||||
private static final Pattern EN_PERIOD = Pattern.compile(
|
||||
"(?i)(January|February|March|April|May|June|July|August|September|October|November|December)"
|
||||
+ "\\s+(\\d{1,2})\\w*\\s+to\\s+"
|
||||
+ "(January|February|March|April|May|June|July|August|September|October|November|December)"
|
||||
+ "\\s+(\\d{1,2})\\w*[,\\s]+(20\\d{2})");
|
||||
|
||||
private static final Pattern MONEY = Pattern.compile(
|
||||
"(?i)(\\d+(?:\\.\\d+)?)\\s*(EUR|GBP|USD|RMB|CNY|欧元|英镑|人民币)?");
|
||||
|
||||
private static final Pattern PERCENT = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*%");
|
||||
|
||||
private LblTexts()
|
||||
{
|
||||
}
|
||||
|
||||
/** 从重量段原文取出上下限。每增加一公斤只保留原文,不编造上下限。 */
|
||||
public static BigDecimal[] weightBounds(String label)
|
||||
{
|
||||
BigDecimal[] bounds = new BigDecimal[] { null, null };
|
||||
if (label == null || label.isBlank())
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
Matcher range = WEIGHT_RANGE.matcher(label);
|
||||
if (range.find())
|
||||
{
|
||||
bounds[0] = decimal(range.group(1));
|
||||
bounds[1] = decimal(range.group(2));
|
||||
return bounds;
|
||||
}
|
||||
Matcher dash = WEIGHT_DASH.matcher(label);
|
||||
if (dash.find())
|
||||
{
|
||||
bounds[0] = decimal(dash.group(1));
|
||||
bounds[1] = decimal(dash.group(2));
|
||||
return bounds;
|
||||
}
|
||||
Matcher first = FIRST_WEIGHT.matcher(label);
|
||||
if (first.find())
|
||||
{
|
||||
bounds[0] = BigDecimal.ZERO;
|
||||
bounds[1] = decimal(first.group(1));
|
||||
return bounds;
|
||||
}
|
||||
if (STEP_WEIGHT.matcher(label).find())
|
||||
{
|
||||
return bounds;
|
||||
}
|
||||
Matcher only = ONLY_KG.matcher(label.trim());
|
||||
if (only.find())
|
||||
{
|
||||
bounds[0] = BigDecimal.ZERO;
|
||||
bounds[1] = decimal(only.group(1));
|
||||
}
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public static boolean looksLikeWeight(String text)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return WEIGHT_RANGE.matcher(text).find()
|
||||
|| WEIGHT_DASH.matcher(text).find()
|
||||
|| FIRST_WEIGHT.matcher(text).find()
|
||||
|| STEP_WEIGHT.matcher(text).find()
|
||||
|| ONLY_KG.matcher(text.trim()).find();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从说明里取第一段时效。defaultYear 用于只有月日、没有年份的句子。
|
||||
*/
|
||||
public static Period period(String text, int defaultYear)
|
||||
{
|
||||
if (text == null || text.isBlank())
|
||||
{
|
||||
return Period.NONE;
|
||||
}
|
||||
Matcher full = CN_PERIOD.matcher(text);
|
||||
if (full.find())
|
||||
{
|
||||
int y1 = Integer.parseInt(full.group(1));
|
||||
int y2 = full.group(4) == null ? y1 : Integer.parseInt(full.group(4));
|
||||
LocalDate start = date(y1, full.group(2), full.group(3));
|
||||
LocalDate end = date(y2, full.group(5), full.group(6));
|
||||
return new Period(start, end, full.group());
|
||||
}
|
||||
Matcher month = CN_MONTH.matcher(text);
|
||||
if (month.find())
|
||||
{
|
||||
int year = Integer.parseInt(month.group(1));
|
||||
int m1 = Integer.parseInt(month.group(2));
|
||||
int m2 = Integer.parseInt(month.group(3));
|
||||
LocalDate start = LocalDate.of(year, m1, 1);
|
||||
LocalDate end = YearMonth.of(year, m2).atEndOfMonth();
|
||||
return new Period(start, end, month.group());
|
||||
}
|
||||
Matcher english = EN_PERIOD.matcher(text);
|
||||
if (english.find())
|
||||
{
|
||||
int year = Integer.parseInt(english.group(5));
|
||||
LocalDate start = date(year, monthNumber(english.group(1)), english.group(2));
|
||||
LocalDate end = date(year, monthNumber(english.group(3)), english.group(4));
|
||||
return new Period(start, end, english.group());
|
||||
}
|
||||
Matcher compact = COMPACT_PERIOD.matcher(text);
|
||||
if (compact.find())
|
||||
{
|
||||
LocalDate start = date(compact.group(1), compact.group(2), compact.group(3));
|
||||
LocalDate end = date(compact.group(4), compact.group(5), compact.group(6));
|
||||
return new Period(start, end, compact.group());
|
||||
}
|
||||
Matcher iso = ISO_DATE.matcher(text);
|
||||
if (iso.find())
|
||||
{
|
||||
LocalDate day = date(iso.group(1), iso.group(2), iso.group(3));
|
||||
return new Period(day, day, iso.group());
|
||||
}
|
||||
return Period.NONE;
|
||||
}
|
||||
|
||||
/** 去掉时效句子后再取金额,避免把 2026 当成费用。 */
|
||||
public static Money money(String text, Period period)
|
||||
{
|
||||
if (text == null || text.isBlank())
|
||||
{
|
||||
return Money.NONE;
|
||||
}
|
||||
String source = text;
|
||||
if (period != null && period.text != null)
|
||||
{
|
||||
source = source.replace(period.text, " ");
|
||||
}
|
||||
Matcher percent = PERCENT.matcher(source);
|
||||
if (percent.find() && !source.matches(".*(?i)(EUR|GBP|USD|RMB|CNY|欧元|英镑).*"))
|
||||
{
|
||||
return new Money(decimal(percent.group(1)), "", "%");
|
||||
}
|
||||
Matcher matcher = MONEY.matcher(source);
|
||||
Money found = Money.NONE;
|
||||
while (matcher.find())
|
||||
{
|
||||
String raw = matcher.group(1);
|
||||
if (raw.startsWith("20") && raw.length() == 4)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String currency = currency(matcher.group(2));
|
||||
String unit = unit(source);
|
||||
found = new Money(decimal(raw), currency, unit);
|
||||
if (!currency.isEmpty())
|
||||
{
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
public static String timing(String text, Period period)
|
||||
{
|
||||
String value = text == null ? "" : text;
|
||||
if (value.contains("产生时") || value.contains("实报") || value.contains("实付"))
|
||||
{
|
||||
return "产生时收取";
|
||||
}
|
||||
if (value.contains("旺季") || (period != null && period.start != null && value.contains("期间")))
|
||||
{
|
||||
return "旺季期间";
|
||||
}
|
||||
if (value.contains("固定收取") || value.contains("每票固定"))
|
||||
{
|
||||
return "固定收取";
|
||||
}
|
||||
if (period != null && period.start != null)
|
||||
{
|
||||
return "旺季期间";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public static String currencies(String text)
|
||||
{
|
||||
String source = text == null ? "" : text.toUpperCase(Locale.ROOT);
|
||||
StringBuilder joined = new StringBuilder();
|
||||
appendCurrency(joined, source, "GBP", "英镑");
|
||||
appendCurrency(joined, source, "EUR", "欧元");
|
||||
appendCurrency(joined, source, "USD", "美元");
|
||||
if (source.contains("RMB") || source.contains("CNY") || source.contains("人民币"))
|
||||
{
|
||||
appendCurrency(joined, "CNY", "CNY", "");
|
||||
}
|
||||
return joined.toString();
|
||||
}
|
||||
|
||||
public static Date toDate(LocalDate day)
|
||||
{
|
||||
if (day == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// 用普通日期,避免 java.sql.Date 在接口序列化时抛出不支持的时间类型
|
||||
return Date.from(day.atStartOfDay(ZoneId.of("GMT+8")).toInstant());
|
||||
}
|
||||
|
||||
private static void appendCurrency(StringBuilder joined, String source, String code, String alias)
|
||||
{
|
||||
if (source.contains(code) || (!alias.isEmpty() && source.contains(alias)))
|
||||
{
|
||||
if (joined.length() > 0)
|
||||
{
|
||||
joined.append(',');
|
||||
}
|
||||
joined.append(code);
|
||||
}
|
||||
}
|
||||
|
||||
private static String currency(String token)
|
||||
{
|
||||
if (token == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String value = token.toUpperCase(Locale.ROOT);
|
||||
if (value.contains("GBP") || token.contains("英镑"))
|
||||
{
|
||||
return "GBP";
|
||||
}
|
||||
if (value.contains("EUR") || token.contains("欧元"))
|
||||
{
|
||||
return "EUR";
|
||||
}
|
||||
if (value.contains("USD") || token.contains("美元"))
|
||||
{
|
||||
return "USD";
|
||||
}
|
||||
if (value.contains("RMB") || value.contains("CNY") || token.contains("人民币"))
|
||||
{
|
||||
return "CNY";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String unit(String text)
|
||||
{
|
||||
String value = text.toUpperCase(Locale.ROOT);
|
||||
if (value.contains("/ITEM") || value.contains("每票") || text.contains("/票") || value.contains("/PARCEL"))
|
||||
{
|
||||
return "ITEM";
|
||||
}
|
||||
if (value.contains("/KG") || text.contains("/公斤") || text.contains("每公斤"))
|
||||
{
|
||||
return "KG";
|
||||
}
|
||||
if (text.contains("/次"))
|
||||
{
|
||||
return "次";
|
||||
}
|
||||
if (text.contains("%"))
|
||||
{
|
||||
return "%";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static int monthNumber(String name)
|
||||
{
|
||||
String[] months = { "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST",
|
||||
"SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" };
|
||||
String key = name.toUpperCase(Locale.ROOT);
|
||||
for (int i = 0; i < months.length; i++)
|
||||
{
|
||||
if (months[i].equals(key))
|
||||
{
|
||||
return i + 1;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static LocalDate date(String year, String month, String day)
|
||||
{
|
||||
return date(Integer.parseInt(year), month, day);
|
||||
}
|
||||
|
||||
private static LocalDate date(int year, String month, String day)
|
||||
{
|
||||
return date(year, Integer.parseInt(month), day);
|
||||
}
|
||||
|
||||
private static LocalDate date(int year, int month, String day)
|
||||
{
|
||||
return LocalDate.of(year, month, Integer.parseInt(day));
|
||||
}
|
||||
|
||||
private static BigDecimal decimal(String raw)
|
||||
{
|
||||
return new BigDecimal(raw);
|
||||
}
|
||||
|
||||
/** 一段可以落库的生效窗口。text 是原文片段。 */
|
||||
public static final class Period
|
||||
{
|
||||
public static final Period NONE = new Period(null, null, null);
|
||||
|
||||
public final LocalDate start;
|
||||
|
||||
public final LocalDate end;
|
||||
|
||||
public final String text;
|
||||
|
||||
public Period(LocalDate start, LocalDate end, String text)
|
||||
{
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
/** 收费标准里的金额。unit 为 % 时 amount 是百分数本身。 */
|
||||
public static final class Money
|
||||
{
|
||||
public static final Money NONE = new Money(null, "", "");
|
||||
|
||||
public final BigDecimal amount;
|
||||
|
||||
public final String currency;
|
||||
|
||||
public final String unit;
|
||||
|
||||
public Money(BigDecimal amount, String currency, String unit)
|
||||
{
|
||||
this.amount = amount;
|
||||
this.currency = currency == null ? "" : currency;
|
||||
this.unit = unit == null ? "" : unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,703 @@
|
||||
package com.bms.price.label;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import com.bms.price.domain.LblProduct;
|
||||
import com.bms.price.domain.LblWeightBand;
|
||||
|
||||
/**
|
||||
* 打单价卡「邮编分区」表抽取为 zone→postcodes,保存时同步到分区管理 prc_zone。
|
||||
* 支持 Corres/双列、Colissimo Densite、CTT Density、PosteItaliane 三列、iMile IT/DE。
|
||||
*/
|
||||
public final class LblZonePostcodeAttach
|
||||
{
|
||||
private static final DataFormatter FMT = new DataFormatter(Locale.ROOT);
|
||||
private static final Pattern COUNTRY_CODE = Pattern.compile("\\(([A-Z]{2})\\)");
|
||||
private static final int MAX_ROWS = 30000;
|
||||
|
||||
private LblZonePostcodeAttach()
|
||||
{
|
||||
}
|
||||
|
||||
public static void attach(Workbook workbook, List<LblProduct> products)
|
||||
{
|
||||
if (workbook == null || products == null || products.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
|
||||
{
|
||||
Sheet sheet = workbook.getSheetAt(i);
|
||||
if (sheet == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String sheetName = sheet.getSheetName() == null ? "" : sheet.getSheetName().trim();
|
||||
if (!isZonePostcodeSheet(sheetName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (sheetName.contains("iMile"))
|
||||
{
|
||||
attachImile(sheet, sheetName, products);
|
||||
continue;
|
||||
}
|
||||
Map<String, List<String>> zoneMap = parseSheet(sheet);
|
||||
if (zoneMap.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
LblProduct target = matchProduct(products, sheetName);
|
||||
if (target == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
mergeZonePostcodes(target, zoneMap);
|
||||
linkSheet(target, sheetName);
|
||||
}
|
||||
}
|
||||
|
||||
/** 从产品国家/重量段推导 ISO2,供同步 prc_zone 使用。 */
|
||||
public static String countryCodeOf(LblProduct product)
|
||||
{
|
||||
if (product == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
if (product.getWeightBands() != null)
|
||||
{
|
||||
for (LblWeightBand band : product.getWeightBands())
|
||||
{
|
||||
if (band == null || band.getCountryCode() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String code = band.getCountryCode().trim().toUpperCase(Locale.ROOT);
|
||||
if (code.matches("[A-Z]{2}"))
|
||||
{
|
||||
return code;
|
||||
}
|
||||
}
|
||||
}
|
||||
String scope = product.getCountryScope() == null ? "" : product.getCountryScope();
|
||||
Matcher m = COUNTRY_CODE.matcher(scope.toUpperCase(Locale.ROOT));
|
||||
if (m.find())
|
||||
{
|
||||
return m.group(1);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static boolean isZonePostcodeSheet(String sheetName)
|
||||
{
|
||||
if (sheetName == null || sheetName.isBlank())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return sheetName.contains("邮编") && sheetName.contains("分区");
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parseSheet(Sheet sheet)
|
||||
{
|
||||
Map<String, List<String>> poste = parsePosteItaliane(sheet);
|
||||
if (!poste.isEmpty())
|
||||
{
|
||||
return poste;
|
||||
}
|
||||
Map<String, List<String>> ctt = parseCtt(sheet);
|
||||
if (!ctt.isEmpty())
|
||||
{
|
||||
return ctt;
|
||||
}
|
||||
Map<String, List<String>> coli = parseColissimo(sheet);
|
||||
if (!coli.isEmpty())
|
||||
{
|
||||
return coli;
|
||||
}
|
||||
return parseTwoCol(sheet);
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parsePosteItaliane(Sheet sheet)
|
||||
{
|
||||
Map<String, List<String>> map = new LinkedHashMap<String, List<String>>();
|
||||
int cityCol = -1;
|
||||
int provinceCol = -1;
|
||||
int ruralCol = -1;
|
||||
int headerRow = -1;
|
||||
int lastRow = Math.min(sheet.getLastRowNum(), MAX_ROWS);
|
||||
for (int r = 0; r <= Math.min(lastRow, 10); r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int c = 0; c < row.getLastCellNum(); c++)
|
||||
{
|
||||
String val = cellText(row.getCell(c)).toLowerCase(Locale.ROOT);
|
||||
if (val.contains("city") && cityCol < 0)
|
||||
{
|
||||
cityCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
else if (val.contains("province") && provinceCol < 0)
|
||||
{
|
||||
provinceCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
else if (val.contains("rural") && ruralCol < 0)
|
||||
{
|
||||
ruralCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
}
|
||||
if (cityCol >= 0 && provinceCol >= 0 && ruralCol >= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cityCol < 0 || provinceCol < 0 || ruralCol < 0 || headerRow < 0)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
int dataStart = headerRow + 1;
|
||||
Row sub = sheet.getRow(headerRow + 1);
|
||||
if (sub != null)
|
||||
{
|
||||
String subLine = cellText(sub.getCell(cityCol)) + cellText(sub.getCell(provinceCol))
|
||||
+ cellText(sub.getCell(ruralCol));
|
||||
if (subLine.contains("邮编"))
|
||||
{
|
||||
dataStart = headerRow + 2;
|
||||
}
|
||||
}
|
||||
for (int r = dataStart; r <= lastRow; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
addPrefix(map, "City", cellText(row.getCell(cityCol)), 3);
|
||||
addPrefix(map, "Province", cellText(row.getCell(provinceCol)), 3);
|
||||
addPrefix(map, "Rural", cellText(row.getCell(ruralCol)), 5);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void addPrefix(Map<String, List<String>> map, String zone, String raw, int pad)
|
||||
{
|
||||
String postcode = normalizePostcode(raw, pad);
|
||||
if (postcode.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<String> list = map.computeIfAbsent(zone, k -> new ArrayList<String>());
|
||||
if (!list.contains(postcode))
|
||||
{
|
||||
list.add(postcode);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parseCtt(Sheet sheet)
|
||||
{
|
||||
Map<String, List<String>> map = new LinkedHashMap<String, List<String>>();
|
||||
int postcodeCol = -1;
|
||||
int zoneCol = -1;
|
||||
int headerRow = -1;
|
||||
int lastRow = Math.min(sheet.getLastRowNum(), MAX_ROWS);
|
||||
for (int r = 0; r <= Math.min(lastRow, 10); r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int c = 0; c < row.getLastCellNum(); c++)
|
||||
{
|
||||
String val = cellText(row.getCell(c));
|
||||
String low = val.toLowerCase(Locale.ROOT);
|
||||
if ((low.contains("codigo postal") || low.contains("código postal") || low.equals("postcode")
|
||||
|| low.equals("postal code")) && postcodeCol < 0)
|
||||
{
|
||||
postcodeCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
if ((low.contains("ctt density") || (low.contains("density") && !low.contains("densité")))
|
||||
&& zoneCol < 0)
|
||||
{
|
||||
zoneCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
}
|
||||
if (postcodeCol >= 0 && zoneCol >= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (postcodeCol < 0 || zoneCol < 0 || headerRow < 0)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
for (int r = headerRow + 1; r <= lastRow; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)), 5);
|
||||
String zone = mapCttZone(cellText(row.getCell(zoneCol)));
|
||||
if (postcode.isEmpty() || zone.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<String> list = map.computeIfAbsent(zone, k -> new ArrayList<String>());
|
||||
if (!list.contains(postcode))
|
||||
{
|
||||
list.add(postcode);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** 价表密度分区名与邮编表 CTT Density 对齐。 */
|
||||
public static String normalizeDensityZone(String raw)
|
||||
{
|
||||
return mapCttZone(raw);
|
||||
}
|
||||
|
||||
private static String mapCttZone(String raw)
|
||||
{
|
||||
String zone = normalizeZone(raw);
|
||||
if (zone.isEmpty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String low = zone.toLowerCase(Locale.ROOT);
|
||||
if ((low.contains("madrid") || low.contains("mad ")) && low.contains("high"))
|
||||
{
|
||||
return "Mad High Density";
|
||||
}
|
||||
if (low.equals("high") || low.startsWith("high "))
|
||||
{
|
||||
return "High";
|
||||
}
|
||||
if (low.equals("medium") || low.startsWith("medium "))
|
||||
{
|
||||
return "Medium";
|
||||
}
|
||||
if (low.equals("low") || low.startsWith("low "))
|
||||
{
|
||||
return "Low";
|
||||
}
|
||||
if (low.contains("mainland") || low.contains("主岛"))
|
||||
{
|
||||
return "Mainland";
|
||||
}
|
||||
return zone;
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parseColissimo(Sheet sheet)
|
||||
{
|
||||
Map<String, List<String>> map = new LinkedHashMap<String, List<String>>();
|
||||
int postcodeCol = -1;
|
||||
int zoneCol = -1;
|
||||
int headerRow = -1;
|
||||
int lastRow = Math.min(sheet.getLastRowNum(), MAX_ROWS);
|
||||
for (int r = 0; r <= Math.min(lastRow, 10); r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int c = 0; c < row.getLastCellNum(); c++)
|
||||
{
|
||||
String val = cellText(row.getCell(c));
|
||||
String low = val.toLowerCase(Locale.ROOT);
|
||||
if ((val.contains("邮编") || low.contains("postal")) && postcodeCol < 0)
|
||||
{
|
||||
postcodeCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
if ((low.contains("densit") || val.contains("中文名称")) && zoneCol < 0)
|
||||
{
|
||||
zoneCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
}
|
||||
if (postcodeCol >= 0 && zoneCol >= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (postcodeCol < 0 || zoneCol < 0 || headerRow < 0)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
for (int r = headerRow + 1; r <= lastRow; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)), 5);
|
||||
String zone = normalizeZone(cellText(row.getCell(zoneCol)));
|
||||
if (postcode.isEmpty() || zone.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<String> list = map.computeIfAbsent(zone, k -> new ArrayList<String>());
|
||||
if (!list.contains(postcode))
|
||||
{
|
||||
list.add(postcode);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> parseTwoCol(Sheet sheet)
|
||||
{
|
||||
Map<String, List<String>> map = new LinkedHashMap<String, List<String>>();
|
||||
int postcodeCol = -1;
|
||||
int zoneCol = -1;
|
||||
int headerRow = -1;
|
||||
int lastRow = Math.min(sheet.getLastRowNum(), MAX_ROWS);
|
||||
for (int r = 0; r <= Math.min(lastRow, 20); r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (int c = 0; c < row.getLastCellNum(); c++)
|
||||
{
|
||||
String val = cellText(row.getCell(c));
|
||||
if (val.contains("邮编") && postcodeCol < 0)
|
||||
{
|
||||
postcodeCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
if (val.contains("分区") && !val.contains("附加") && zoneCol < 0)
|
||||
{
|
||||
zoneCol = c;
|
||||
headerRow = r;
|
||||
}
|
||||
}
|
||||
if (postcodeCol >= 0 && zoneCol >= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (postcodeCol < 0 || zoneCol < 0 || headerRow < 0)
|
||||
{
|
||||
return map;
|
||||
}
|
||||
for (int r = headerRow + 1; r <= lastRow; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)), 5);
|
||||
String zone = normalizeZone(cellText(row.getCell(zoneCol)));
|
||||
if (postcode.isEmpty() || zone.isEmpty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<String> list = map.computeIfAbsent(zone, k -> new ArrayList<String>());
|
||||
if (!list.contains(postcode))
|
||||
{
|
||||
list.add(postcode);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static void attachImile(Sheet sheet, String sheetName, List<LblProduct> products)
|
||||
{
|
||||
LblProduct italy = productOf(products, "IT-iMile");
|
||||
LblProduct germany = productOf(products, "DE-iMile");
|
||||
int lastRow = Math.min(sheet.getLastRowNum(), MAX_ROWS);
|
||||
int header = -1;
|
||||
for (int r = 0; r <= Math.min(lastRow, 6); r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
StringBuilder line = new StringBuilder();
|
||||
for (int c = 0; c < row.getLastCellNum(); c++)
|
||||
{
|
||||
line.append(cellText(row.getCell(c)));
|
||||
}
|
||||
String text = line.toString();
|
||||
if (text.contains("邮编") || text.toLowerCase(Locale.ROOT).contains("zip"))
|
||||
{
|
||||
header = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (header < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Row head = sheet.getRow(header);
|
||||
int postcodeCol = -1;
|
||||
int zoneCol = -1;
|
||||
int zipCol = -1;
|
||||
int statusCol = -1;
|
||||
for (int c = 0; c < head.getLastCellNum(); c++)
|
||||
{
|
||||
String val = cellText(head.getCell(c));
|
||||
String low = val.toLowerCase(Locale.ROOT);
|
||||
if (val.contains("邮编") && postcodeCol < 0)
|
||||
{
|
||||
postcodeCol = c;
|
||||
}
|
||||
if (("分区".equals(val) || val.startsWith("分区")) && zoneCol < 0)
|
||||
{
|
||||
zoneCol = c;
|
||||
}
|
||||
if (low.contains("zip") && zipCol < 0)
|
||||
{
|
||||
zipCol = c;
|
||||
}
|
||||
if (low.contains("status") && statusCol < 0)
|
||||
{
|
||||
statusCol = c;
|
||||
}
|
||||
}
|
||||
Map<String, List<String>> itMap = new LinkedHashMap<String, List<String>>();
|
||||
Map<String, List<String>> deMap = new LinkedHashMap<String, List<String>>();
|
||||
for (int r = header + 1; r <= lastRow; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (italy != null && postcodeCol >= 0 && zoneCol >= 0)
|
||||
{
|
||||
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)), 5);
|
||||
String zone = imileZone(cellText(row.getCell(zoneCol)));
|
||||
if (!postcode.isEmpty() && !zone.isEmpty())
|
||||
{
|
||||
List<String> list = itMap.computeIfAbsent(zone, k -> new ArrayList<String>());
|
||||
if (!list.contains(postcode))
|
||||
{
|
||||
list.add(postcode);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (germany != null && zipCol >= 0)
|
||||
{
|
||||
String postcode = normalizePostcode(cellText(row.getCell(zipCol)), 5);
|
||||
String status = statusCol >= 0 ? cellText(row.getCell(statusCol)) : "Active";
|
||||
if (!postcode.isEmpty() && (statusCol < 0 || "Active".equalsIgnoreCase(status)))
|
||||
{
|
||||
List<String> list = deMap.computeIfAbsent("Phase 1", k -> new ArrayList<String>());
|
||||
if (!list.contains(postcode))
|
||||
{
|
||||
list.add(postcode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (italy != null && !itMap.isEmpty())
|
||||
{
|
||||
mergeZonePostcodes(italy, itMap);
|
||||
linkSheet(italy, sheetName);
|
||||
}
|
||||
if (germany != null && !deMap.isEmpty())
|
||||
{
|
||||
mergeZonePostcodes(germany, deMap);
|
||||
linkSheet(germany, sheetName);
|
||||
}
|
||||
}
|
||||
|
||||
private static String imileZone(String text)
|
||||
{
|
||||
String value = text == null ? "" : text.trim().toLowerCase(Locale.ROOT);
|
||||
if (value.startsWith("city"))
|
||||
{
|
||||
return "City";
|
||||
}
|
||||
if (value.startsWith("province"))
|
||||
{
|
||||
return "Province";
|
||||
}
|
||||
if (value.startsWith("rural"))
|
||||
{
|
||||
return "Rural";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static LblProduct productOf(List<LblProduct> products, String name)
|
||||
{
|
||||
for (LblProduct product : products)
|
||||
{
|
||||
if (name.equals(product.getSheetName()) || name.equals(product.getProductName())
|
||||
|| name.equals(product.getDictValue()))
|
||||
{
|
||||
return product;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static LblProduct matchProduct(List<LblProduct> products, String sheetName)
|
||||
{
|
||||
for (LblProduct product : products)
|
||||
{
|
||||
String related = product.getRelatedSheet();
|
||||
if (related != null && related.contains(sheetName))
|
||||
{
|
||||
return product;
|
||||
}
|
||||
}
|
||||
String token = sheetName.replace("邮编分区", "").replace("分区", "").trim();
|
||||
String tokenNorm = compact(token);
|
||||
LblProduct fuzzy = null;
|
||||
int hits = 0;
|
||||
for (LblProduct product : products)
|
||||
{
|
||||
String blob = compact(nz(product.getProductName()) + nz(product.getSheetName()) + nz(product.getDictValue()));
|
||||
if (!tokenNorm.isEmpty() && blob.contains(tokenNorm))
|
||||
{
|
||||
fuzzy = product;
|
||||
hits++;
|
||||
}
|
||||
}
|
||||
if (hits == 1)
|
||||
{
|
||||
return fuzzy;
|
||||
}
|
||||
for (LblProduct product : products)
|
||||
{
|
||||
String blob = compact(nz(product.getProductName()) + nz(product.getSheetName()) + nz(product.getDictValue()));
|
||||
if (tokenNorm.startsWith("ctt") && blob.contains("ctt"))
|
||||
{
|
||||
return product;
|
||||
}
|
||||
if (tokenNorm.startsWith("colissimo") && blob.contains("colissimo"))
|
||||
{
|
||||
return product;
|
||||
}
|
||||
if (tokenNorm.startsWith("corres") && blob.contains("corres"))
|
||||
{
|
||||
return product;
|
||||
}
|
||||
if (tokenNorm.startsWith("posteitaliane") && blob.contains("posteitaliane"))
|
||||
{
|
||||
return product;
|
||||
}
|
||||
}
|
||||
if (products.size() == 1)
|
||||
{
|
||||
return products.get(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void mergeZonePostcodes(LblProduct product, Map<String, List<String>> incoming)
|
||||
{
|
||||
Map<String, List<String>> current = product.getZonePostcodes();
|
||||
if (current == null)
|
||||
{
|
||||
current = new LinkedHashMap<String, List<String>>();
|
||||
product.setZonePostcodes(current);
|
||||
}
|
||||
for (Map.Entry<String, List<String>> e : incoming.entrySet())
|
||||
{
|
||||
List<String> list = current.computeIfAbsent(e.getKey(), k -> new ArrayList<String>());
|
||||
for (String pc : e.getValue())
|
||||
{
|
||||
if (!list.contains(pc))
|
||||
{
|
||||
list.add(pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void linkSheet(LblProduct product, String sheetName)
|
||||
{
|
||||
String current = product.getRelatedSheet();
|
||||
if (current == null || current.isBlank())
|
||||
{
|
||||
product.setRelatedSheet(sheetName);
|
||||
return;
|
||||
}
|
||||
if (!current.contains(sheetName))
|
||||
{
|
||||
product.setRelatedSheet(current + "、" + sheetName);
|
||||
}
|
||||
}
|
||||
|
||||
private static String cellText(Cell cell)
|
||||
{
|
||||
if (cell == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return FMT.formatCellValue(cell).replace('\r', ' ').replace('\n', ' ').replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
private static String normalizePostcode(String raw, int pad)
|
||||
{
|
||||
if (raw == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String text = raw.trim().replace(" ", "");
|
||||
if (text.isEmpty())
|
||||
{
|
||||
return "";
|
||||
}
|
||||
if (text.matches("\\d+"))
|
||||
{
|
||||
if (pad > 0)
|
||||
{
|
||||
long value = Long.parseLong(text);
|
||||
return String.format("%0" + pad + "d", value);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private static String normalizeZone(String raw)
|
||||
{
|
||||
if (raw == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return raw.replace('\r', ' ').replace('\n', ' ').replaceAll("\\s+", " ").trim();
|
||||
}
|
||||
|
||||
private static String compact(String text)
|
||||
{
|
||||
return text == null ? "" : text.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9\\u4e00-\\u9fa5]", "");
|
||||
}
|
||||
|
||||
private static String nz(String text)
|
||||
{
|
||||
return text == null ? "" : text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.bms.price.mapper;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import com.bms.price.domain.LblProduct;
|
||||
import com.bms.price.domain.LblProductVersion;
|
||||
import com.bms.price.domain.LblSurcharge;
|
||||
import com.bms.price.domain.LblWeightBand;
|
||||
import com.bms.price.domain.LabelPackageVersion;
|
||||
|
||||
/**
|
||||
* 打单价卡四表 Mapper。
|
||||
*/
|
||||
public interface LblProductMapper
|
||||
{
|
||||
int insertProduct(LblProduct product);
|
||||
|
||||
int updateProduct(LblProduct product);
|
||||
|
||||
LblProduct selectProductById(@Param("productId") Long productId);
|
||||
|
||||
LblProduct selectProductByDict(@Param("dictValue") String dictValue);
|
||||
|
||||
Long selectProductIdBySetAndDict(@Param("priceSetId") Long priceSetId, @Param("dictValue") String dictValue);
|
||||
|
||||
int insertVersion(LblProductVersion version);
|
||||
|
||||
int updateVersion(LblProductVersion version);
|
||||
|
||||
int closePendingBySetProduct(@Param("priceSetId") Long priceSetId, @Param("productId") Long productId,
|
||||
@Param("endDate") java.util.Date endDate, @Param("operator") String operator);
|
||||
|
||||
List<Long> selectVersionIdsBySetAndNo(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo);
|
||||
|
||||
int deletePendingVersionsBySetAndNo(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo);
|
||||
|
||||
int deleteAllVersionsBySetAndNo(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo);
|
||||
|
||||
List<LblProductVersion> selectVersionsBySet(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
List<LblProductVersion> selectVersionsBySetAndNo(@Param("priceSetId") Long priceSetId,
|
||||
@Param("versionNo") Integer versionNo);
|
||||
|
||||
LblProductVersion selectVersionById(@Param("versionId") Long versionId);
|
||||
|
||||
Integer selectMaxVersionNo(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
List<LabelPackageVersion> selectLabelVersionGroups(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
int insertWeightBand(LblWeightBand band);
|
||||
|
||||
/** 按 band_id 更新可编辑字段(不动维度/单位) */
|
||||
int updateWeightBand(LblWeightBand band);
|
||||
|
||||
LblWeightBand selectWeightBandById(@Param("bandId") Long bandId);
|
||||
|
||||
int deleteWeightBandsByVersion(@Param("versionId") Long versionId);
|
||||
|
||||
List<LblWeightBand> selectWeightBandsByVersion(@Param("versionId") Long versionId);
|
||||
|
||||
int insertSurcharge(LblSurcharge surcharge);
|
||||
|
||||
int updateSurcharge(LblSurcharge surcharge);
|
||||
|
||||
LblSurcharge selectSurchargeById(@Param("surchargeId") Long surchargeId);
|
||||
|
||||
int deleteSurchargesByVersion(@Param("versionId") Long versionId);
|
||||
|
||||
List<LblSurcharge> selectSurchargesByVersion(@Param("versionId") Long versionId);
|
||||
|
||||
List<Long> selectVersionIdsBySetAndProducts(@Param("priceSetId") Long priceSetId, @Param("productIds") Long[] productIds);
|
||||
|
||||
int deleteVersionsBySetAndProducts(@Param("priceSetId") Long priceSetId, @Param("productIds") Long[] productIds);
|
||||
|
||||
List<LblProduct> selectProductsBySet(@Param("priceSetId") Long priceSetId);
|
||||
|
||||
List<LblProduct> selectProductsBySetAndVersion(@Param("priceSetId") Long priceSetId,
|
||||
@Param("versionNo") Integer versionNo);
|
||||
|
||||
int activateVersionRows(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo,
|
||||
@Param("effectiveStart") java.util.Date effectiveStart, @Param("operator") String operator);
|
||||
|
||||
int expireOtherActive(@Param("priceSetId") Long priceSetId, @Param("versionNo") Integer versionNo,
|
||||
@Param("endDate") java.util.Date endDate, @Param("operator") String operator);
|
||||
}
|
||||
@@ -122,8 +122,8 @@ public final class LabelArCostCalculator
|
||||
+ "),邮编没有对上其中的地区,无法确定运费");
|
||||
return result;
|
||||
}
|
||||
PayWeightBand band = matchBand(bands, weight);
|
||||
if (band == null)
|
||||
List<PayWeightBand> matched = matchBands(bands, weight);
|
||||
if (matched.isEmpty())
|
||||
{
|
||||
PayWeightBand top = highestBand(bands);
|
||||
if (top == null || weight.compareTo(top.getWeightMax()) <= 0
|
||||
@@ -132,19 +132,43 @@ public final class LabelArCostCalculator
|
||||
result.setMessage(outOfRange(product, bands, weight));
|
||||
return result;
|
||||
}
|
||||
band = top;
|
||||
matched = new ArrayList<>();
|
||||
matched.add(top);
|
||||
}
|
||||
PayWeightBand band = matched.get(0);
|
||||
String currency = currencyOf(product, band);
|
||||
BigDecimal freight = bandAmount(band, weight);
|
||||
String freightRemark = band.getWeightLabel() + " " + plain(band.getPrice()) + " " + blank(band.getPriceUnit());
|
||||
if (zoneName != null && !zoneName.isBlank())
|
||||
BigDecimal freight = BigDecimal.ZERO;
|
||||
BigDecimal handleTotal = BigDecimal.ZERO;
|
||||
for (PayWeightBand one : matched)
|
||||
{
|
||||
freightRemark = freightRemark + ",地区 " + zoneName.replace('\n', ' ');
|
||||
BigDecimal amount = bandAmount(one, weight);
|
||||
if (isHandleBand(one))
|
||||
{
|
||||
handleTotal = handleTotal.add(amount);
|
||||
String handleRemark = blank(one.getChargeName()) + " " + plain(one.getPrice()) + " " + blank(one.getPriceUnit());
|
||||
if (zoneName != null && !zoneName.isBlank())
|
||||
{
|
||||
handleRemark = handleRemark + ",地区 " + zoneName.replace('\n', ' ');
|
||||
}
|
||||
result.getFees().add(line("HANDLE_FEE", "处理费", 0L, amount, currency, handleRemark));
|
||||
}
|
||||
else
|
||||
{
|
||||
freight = freight.add(amount);
|
||||
String freightRemark = one.getWeightLabel() + " " + plain(one.getPrice()) + " " + blank(one.getPriceUnit());
|
||||
if (zoneName != null && !zoneName.isBlank())
|
||||
{
|
||||
freightRemark = freightRemark + ",地区 " + zoneName.replace('\n', ' ');
|
||||
}
|
||||
result.getFees().add(line("BASIC_FREIGHT", "基础运费", 0L, amount, currency, freightRemark));
|
||||
}
|
||||
}
|
||||
if (freight.compareTo(BigDecimal.ZERO) == 0 && handleTotal.compareTo(BigDecimal.ZERO) > 0)
|
||||
{
|
||||
freight = handleTotal;
|
||||
}
|
||||
PayCostLine freightLine = line("BASIC_FREIGHT", "基础运费", 0L, freight, currency, freightRemark);
|
||||
result.getFees().add(freightLine);
|
||||
result.setCurrency(currency);
|
||||
addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
|
||||
appendSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
|
||||
band.getSectionName(), remoteHits, result);
|
||||
BigDecimal total = BigDecimal.ZERO;
|
||||
for (PayCostLine fee : result.getFees())
|
||||
@@ -263,7 +287,8 @@ public final class LabelArCostCalculator
|
||||
* @param remoteHits
|
||||
* @param result
|
||||
*/
|
||||
private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||
/** 供新计费复用:在已算完重量段后追加附加费。 */
|
||||
public static void appendSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
|
||||
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency,
|
||||
String freightSection, List<PayRemoteBook.Hit> remoteHits, PayOrderCostResult result)
|
||||
{
|
||||
@@ -694,13 +719,25 @@ public final class LabelArCostCalculator
|
||||
|
||||
private static PayWeightBand matchBand(List<PayWeightBand> bands, BigDecimal weight)
|
||||
{
|
||||
List<PayWeightBand> matched = matchBands(bands, weight);
|
||||
return matched.isEmpty() ? null : matched.get(0);
|
||||
}
|
||||
|
||||
/** 同一重量段可能同时有运费与处理费两行(UNIT_PLUS_HANDLE 拆分后)。 */
|
||||
private static List<PayWeightBand> matchBands(List<PayWeightBand> bands, BigDecimal weight)
|
||||
{
|
||||
List<PayWeightBand> matched = new ArrayList<>();
|
||||
for (PayWeightBand band : bands)
|
||||
{
|
||||
if (weight.compareTo(band.getWeightMin()) >= 0 && weight.compareTo(band.getWeightMax()) <= 0)
|
||||
{
|
||||
return band;
|
||||
matched.add(band);
|
||||
}
|
||||
}
|
||||
if (!matched.isEmpty())
|
||||
{
|
||||
return matched;
|
||||
}
|
||||
for (int i = 0; i < bands.size() - 1; i++)
|
||||
{
|
||||
PayWeightBand current = bands.get(i);
|
||||
@@ -709,10 +746,17 @@ public final class LabelArCostCalculator
|
||||
if (weight.compareTo(current.getWeightMax()) > 0 && weight.compareTo(next.getWeightMin()) < 0
|
||||
&& gap.compareTo(new BigDecimal("0.01")) <= 0)
|
||||
{
|
||||
return next;
|
||||
matched.add(next);
|
||||
return matched;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return matched;
|
||||
}
|
||||
|
||||
private static boolean isHandleBand(PayWeightBand band)
|
||||
{
|
||||
String name = blank(band == null ? null : band.getChargeName());
|
||||
return name.contains("处理费") || name.contains("挂号");
|
||||
}
|
||||
|
||||
private static String outOfRange(PayProduct product, List<PayWeightBand> bands, BigDecimal weight)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.bms.price.service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import com.bms.price.domain.ChannelProductMap;
|
||||
import com.bms.price.domain.LabelPackageVersion;
|
||||
import com.bms.price.domain.LblPreview;
|
||||
import com.bms.price.domain.LblProduct;
|
||||
import com.bms.price.domain.LblSurcharge;
|
||||
import com.bms.price.domain.LblWeightBand;
|
||||
import com.bms.price.domain.LblSaveCommand;
|
||||
import com.bms.price.domain.LblSaveTask;
|
||||
import com.bms.price.domain.PayParseProgress;
|
||||
import com.bms.price.pay.PayParseProgressListener;
|
||||
|
||||
/**
|
||||
* 打单价卡:独立解析、落库与版本管理(与应付 IPayProductService 分离)。
|
||||
*/
|
||||
public interface ILblProductService
|
||||
{
|
||||
LblPreview preview(byte[] bytes, String filename, String dictValue, String progressId, Long priceSetId);
|
||||
|
||||
PayParseProgress parseProgress(String progressId);
|
||||
|
||||
String save(LblSaveCommand command);
|
||||
|
||||
String save(LblSaveCommand command, PayParseProgressListener listener);
|
||||
|
||||
/** 提交异步保存,立即返回 taskId */
|
||||
String submitSave(LblSaveCommand command);
|
||||
|
||||
LblSaveTask getSaveTask(String taskId);
|
||||
|
||||
List<LabelPackageVersion> selectVersions(Long priceSetId);
|
||||
|
||||
String activateVersion(Long priceSetId, Integer versionNo, Date effectiveTime);
|
||||
|
||||
int deleteVersion(Long priceSetId, Integer versionNo);
|
||||
|
||||
List<LblProduct> selectProducts(Long priceSetId);
|
||||
|
||||
List<LblProduct> selectProductsByVersion(Long priceSetId, Integer versionNo);
|
||||
|
||||
LblProduct selectDetailByVersionId(Long versionId);
|
||||
|
||||
int updateDetail(LblProduct product);
|
||||
|
||||
/** 仅更新一条重量段可编辑字段 */
|
||||
int updateWeightBand(LblWeightBand band);
|
||||
|
||||
/** 仅更新一条附加费可编辑字段 */
|
||||
int updateSurcharge(LblSurcharge surcharge);
|
||||
|
||||
int removeProducts(Long priceSetId, Long[] productIds);
|
||||
|
||||
List<ChannelProductMap> listProductChannelBinds(Long productId, String payType);
|
||||
|
||||
String replaceProductChannelBinds(Long productId, String payType, List<String> channelIds);
|
||||
|
||||
LblProduct selectPriceForDate(Long priceSetId, String channelId, Date bizDate);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.bms.price.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.bms.price.domain.PrcZone;
|
||||
import com.bms.price.domain.PrcZoneDetail;
|
||||
@@ -36,4 +37,12 @@ public interface IPrcZoneService
|
||||
public int deletePrcZoneDetailByIds(Long[] detailIds);
|
||||
|
||||
public String importZoneExcel(MultipartFile file, String operName) throws Exception;
|
||||
|
||||
/**
|
||||
* 按国家同步邮编分区:仅覆盖本次出现的 zone,不清空该国其他分区。
|
||||
* @param countryCode2 ISO2
|
||||
* @param zonePostcodes zoneLabel -> postcodes
|
||||
* @return 摘要文案
|
||||
*/
|
||||
public String syncCountryZones(String countryCode2, Map<String, List<String>> zonePostcodes);
|
||||
}
|
||||
|
||||
+923
@@ -0,0 +1,923 @@
|
||||
package com.bms.price.service.impl;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import com.bms.common.enums.PaymentMesEnum;
|
||||
import com.bms.common.exception.ServiceException;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.SecurityUtils;
|
||||
import com.bms.common.utils.file.OssStorage;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.price.domain.ChannelProductMap;
|
||||
import com.bms.price.domain.LabelPackageVersion;
|
||||
import com.bms.price.domain.LblPreview;
|
||||
import com.bms.price.domain.LblProduct;
|
||||
import com.bms.price.domain.LblProductVersion;
|
||||
import com.bms.price.domain.LblSaveCommand;
|
||||
import com.bms.price.domain.LblSaveTask;
|
||||
import com.bms.price.domain.LblSurcharge;
|
||||
import com.bms.price.domain.LblWeightBand;
|
||||
import com.bms.price.domain.PayParseProgress;
|
||||
import com.bms.price.domain.PrcPriceSet;
|
||||
import com.bms.price.label.LblBandNormalizer;
|
||||
import com.bms.price.label.LblWorkbookParser;
|
||||
import com.bms.price.mapper.ChannelProductMapMapper;
|
||||
import com.bms.price.mapper.LblProductMapper;
|
||||
import com.bms.price.mapper.PrcPriceSetMapper;
|
||||
import com.bms.price.pay.PayParseProgressListener;
|
||||
import com.bms.price.service.ILblProductService;
|
||||
import com.bms.price.task.LblSaveAsyncRunner;
|
||||
import com.bms.price.service.IPrcZoneService;
|
||||
import com.bms.price.label.LblZonePostcodeAttach;
|
||||
import com.bms.common.core.domain.entity.SysDictData;
|
||||
import com.bms.system.service.ISysDictTypeService;
|
||||
|
||||
/**
|
||||
* 打单价卡服务:独立 Excel 解析 + prc_label_* 四表。
|
||||
*/
|
||||
@Service
|
||||
public class LblProductServiceImpl implements ILblProductService
|
||||
{
|
||||
private static final String DICT_TYPE = "payment_product_type";
|
||||
|
||||
private static final String TERM_PENDING = "0";
|
||||
|
||||
private static final String TERM_EFFECT = "1";
|
||||
|
||||
private static final String TERM_EXPIRED = "2";
|
||||
|
||||
private static final String PAY_TYPE_AR = "AR";
|
||||
|
||||
private static final long PARSE_PROGRESS_KEEP_MS = 30L * 60L * 1000L;
|
||||
|
||||
private static final long UPLOAD_KEEP_MS = 60L * 60L * 1000L;
|
||||
|
||||
private final LblWorkbookParser parser = new LblWorkbookParser();
|
||||
|
||||
private final Map<String, ParseSlot> parseProgress = new ConcurrentHashMap<String, ParseSlot>();
|
||||
|
||||
private final Map<String, UploadedSource> uploadedSources = new ConcurrentHashMap<String, UploadedSource>();
|
||||
|
||||
@Autowired
|
||||
private LblProductMapper lblProductMapper;
|
||||
|
||||
@Autowired
|
||||
private PrcPriceSetMapper priceSetMapper;
|
||||
|
||||
@Autowired
|
||||
private ChannelProductMapMapper channelProductMapMapper;
|
||||
|
||||
@Autowired
|
||||
private ISysDictTypeService dictTypeService;
|
||||
|
||||
@Autowired
|
||||
private OssStorage ossStorage;
|
||||
|
||||
@Autowired
|
||||
private IPrcZoneService prcZoneService;
|
||||
|
||||
@Autowired
|
||||
private LblSaveAsyncRunner lblSaveAsyncRunner;
|
||||
|
||||
@Override
|
||||
public LblPreview preview(byte[] bytes, String filename, String dictValue, String progressId, Long priceSetId)
|
||||
{
|
||||
String progressKey = progressKey(progressId);
|
||||
PayParseProgressListener listener = progressKey == null ? null
|
||||
: (percent, message) -> writeProgress(progressKey, percent, message, PayParseProgress.RUNNING);
|
||||
if (progressKey != null)
|
||||
{
|
||||
writeProgress(progressKey, 2, "正在准备解析", PayParseProgress.RUNNING);
|
||||
}
|
||||
try
|
||||
{
|
||||
LblPreview preview = parser.parseMatched(bytes, filename, dictValue, loadDictValues(), listener);
|
||||
LblBandNormalizer.normalize(preview);
|
||||
writeProgress(progressKey, 98, "正在核对已有产品", PayParseProgress.RUNNING);
|
||||
String stored = storeSource(bytes, filename);
|
||||
rememberSource(preview.getFileMd5(), stored);
|
||||
preview.setFilePath(stored);
|
||||
for (LblProduct product : preview.getProducts())
|
||||
{
|
||||
product.setFilePath(stored);
|
||||
Long existsId = isBlank(product.getDictValue()) ? null
|
||||
: lblProductMapper.selectProductIdBySetAndDict(priceSetId, product.getDictValue());
|
||||
if (existsId == null && !isBlank(product.getDictValue()))
|
||||
{
|
||||
LblProduct byDict = lblProductMapper.selectProductByDict(product.getDictValue());
|
||||
existsId = byDict == null ? null : byDict.getProductId();
|
||||
}
|
||||
product.setExistsId(existsId);
|
||||
boolean pendingInSet = priceSetId != null && existsId != null
|
||||
&& hasPending(priceSetId, existsId);
|
||||
product.setDuplicated(pendingInSet);
|
||||
}
|
||||
writeProgress(progressKey, 100, "解析完成", PayParseProgress.SUCCESS);
|
||||
return preview;
|
||||
}
|
||||
catch (RuntimeException ex)
|
||||
{
|
||||
String message = ex.getMessage() == null || ex.getMessage().isBlank() ? "价卡解析失败" : ex.getMessage();
|
||||
writeProgress(progressKey, 100, message, PayParseProgress.FAIL);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayParseProgress parseProgress(String progressId)
|
||||
{
|
||||
String key = progressKey(progressId);
|
||||
if (key == null)
|
||||
{
|
||||
return new PayParseProgress(0, "等待解析", PayParseProgress.PENDING);
|
||||
}
|
||||
ParseSlot slot = parseProgress.get(key);
|
||||
if (slot == null || System.currentTimeMillis() - slot.at > PARSE_PROGRESS_KEEP_MS)
|
||||
{
|
||||
return new PayParseProgress(0, "等待解析", PayParseProgress.PENDING);
|
||||
}
|
||||
return slot.progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String save(LblSaveCommand command)
|
||||
{
|
||||
return save(command, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String submitSave(LblSaveCommand command)
|
||||
{
|
||||
if (command == null || command.getProducts() == null || command.getProducts().isEmpty())
|
||||
{
|
||||
throw PaymentMesEnum.BMS_PAY_PICK_PROD.exception();
|
||||
}
|
||||
String taskId = IdUtils.fastSimpleUUID();
|
||||
lblSaveAsyncRunner.put(taskId, LblSaveTask.STATUS_PENDING, 0, "已入队",
|
||||
command.getPriceSetId(), null);
|
||||
lblSaveAsyncRunner.run(taskId, command, SecurityContextHolder.getContext().getAuthentication());
|
||||
return taskId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LblSaveTask getSaveTask(String taskId)
|
||||
{
|
||||
if (isBlank(taskId))
|
||||
{
|
||||
LblSaveTask empty = new LblSaveTask();
|
||||
empty.setStatus(LblSaveTask.STATUS_PENDING);
|
||||
empty.setPercent(0);
|
||||
empty.setMessage("等待保存");
|
||||
return empty;
|
||||
}
|
||||
LblSaveTask task = lblSaveAsyncRunner.get(taskId.trim());
|
||||
if (task == null)
|
||||
{
|
||||
LblSaveTask empty = new LblSaveTask();
|
||||
empty.setTaskId(taskId.trim());
|
||||
empty.setStatus(LblSaveTask.STATUS_PENDING);
|
||||
empty.setPercent(0);
|
||||
empty.setMessage("等待保存");
|
||||
return empty;
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String save(LblSaveCommand command, PayParseProgressListener listener)
|
||||
{
|
||||
if (command == null || command.getProducts() == null || command.getProducts().isEmpty())
|
||||
{
|
||||
throw PaymentMesEnum.BMS_PAY_PICK_PROD.exception();
|
||||
}
|
||||
report(listener, 5, "正在创建/校验套餐");
|
||||
Long priceSetId = ensureLabelPriceSet(command);
|
||||
report(listener, 8, "套餐 " + priceSetId + " 就绪,开始落库产品");
|
||||
String operator = SecurityUtils.getUsername();
|
||||
Date keepStart = null;
|
||||
String keepStatus = TERM_PENDING;
|
||||
int nextVersionNo;
|
||||
boolean editPendingVersion = command.getVersionNo() != null;
|
||||
if (editPendingVersion)
|
||||
{
|
||||
VersionKeep keep = clearEditableVersion(priceSetId, command.getVersionNo(), operator);
|
||||
keepStart = keep.effectiveStart;
|
||||
keepStatus = keep.status == null ? TERM_PENDING : keep.status;
|
||||
nextVersionNo = command.getVersionNo();
|
||||
}
|
||||
else
|
||||
{
|
||||
Integer maxSetVer = lblProductMapper.selectMaxVersionNo(priceSetId);
|
||||
nextVersionNo = (maxSetVer == null ? 0 : maxSetVer) + 1;
|
||||
}
|
||||
int saved = 0;
|
||||
int replaced = 0;
|
||||
int skipped = 0;
|
||||
List<LblProduct> products = command.getProducts();
|
||||
int total = products.size();
|
||||
Map<String, Map<String, List<String>>> zoneSyncByCountry = new LinkedHashMap<String, Map<String, List<String>>>();
|
||||
for (int i = 0; i < products.size(); i++)
|
||||
{
|
||||
LblProduct product = products.get(i);
|
||||
if (product == null || isBlank(product.getProductName()) || isBlank(product.getSheetName())
|
||||
|| isBlank(product.getFileMd5()))
|
||||
{
|
||||
throw PaymentMesEnum.BMS_PAY_PROD_FIELD.exception();
|
||||
}
|
||||
String displayName = blankTo(product.getProductName(), product.getSheetName());
|
||||
int pct = 10 + (int) Math.floor((i * 75.0) / Math.max(total, 1));
|
||||
report(listener, pct, "正在保存「" + displayName + "」(" + (i + 1) + "/" + total + ")");
|
||||
LblBandNormalizer.normalizeProduct(product);
|
||||
String dict = blankTo(product.getDictValue(), product.getProductName());
|
||||
product.setDictValue(dict);
|
||||
Long existsId = lblProductMapper.selectProductIdBySetAndDict(priceSetId, dict);
|
||||
if (existsId == null)
|
||||
{
|
||||
LblProduct byDict = lblProductMapper.selectProductByDict(dict);
|
||||
existsId = byDict == null ? null : byDict.getProductId();
|
||||
}
|
||||
boolean hasPending = !editPendingVersion && existsId != null && hasPending(priceSetId, existsId);
|
||||
if (hasPending && !command.isOverwrite())
|
||||
{
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
product.setFilePath(resolveSource(product));
|
||||
prepareCounts(product);
|
||||
product.setCreateBy(operator);
|
||||
product.setUpdateBy(operator);
|
||||
product.setCreateTime(new Date());
|
||||
if (existsId != null)
|
||||
{
|
||||
if (editPendingVersion || command.isOverwrite())
|
||||
{
|
||||
lblProductMapper.closePendingBySetProduct(priceSetId, existsId, yesterday(), operator);
|
||||
}
|
||||
product.setProductId(existsId);
|
||||
lblProductMapper.updateProduct(product);
|
||||
openVersion(product, priceSetId, nextVersionNo, operator);
|
||||
replaced++;
|
||||
}
|
||||
else
|
||||
{
|
||||
product.setProductId(null);
|
||||
lblProductMapper.insertProduct(product);
|
||||
openVersion(product, priceSetId, nextVersionNo, operator);
|
||||
}
|
||||
saved++;
|
||||
collectZoneSync(zoneSyncByCountry, product);
|
||||
}
|
||||
report(listener, 88, "正在同步邮编分区");
|
||||
for (Map.Entry<String, Map<String, List<String>>> e : zoneSyncByCountry.entrySet())
|
||||
{
|
||||
prcZoneService.syncCountryZones(e.getKey(), e.getValue());
|
||||
}
|
||||
if (editPendingVersion && keepStart != null)
|
||||
{
|
||||
List<LblProductVersion> rows = lblProductMapper.selectVersionsBySetAndNo(priceSetId, nextVersionNo);
|
||||
for (LblProductVersion row : rows)
|
||||
{
|
||||
row.setEffectiveStart(keepStart);
|
||||
row.setStatus(keepStatus);
|
||||
row.setUpdateBy(operator);
|
||||
lblProductMapper.updateVersion(row);
|
||||
}
|
||||
}
|
||||
report(listener, 98, "保存完成,正在收尾");
|
||||
return editPendingVersion
|
||||
? ("套餐" + priceSetId + "版本V" + nextVersionNo + "已更新" + saved + "个产品")
|
||||
: ("套餐" + priceSetId + "已保存" + saved + "个产品,新版本" + replaced + "个,跳过" + skipped + "个");
|
||||
}
|
||||
|
||||
private static void report(PayParseProgressListener listener, int percent, String message)
|
||||
{
|
||||
if (listener != null)
|
||||
{
|
||||
listener.onProgress(percent, message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LabelPackageVersion> selectVersions(Long priceSetId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<LabelPackageVersion> groups = lblProductMapper.selectLabelVersionGroups(priceSetId);
|
||||
if (groups == null || groups.isEmpty())
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Date now = new Date();
|
||||
for (LabelPackageVersion ver : groups)
|
||||
{
|
||||
ver.setPriceSetId(priceSetId);
|
||||
ver.setVersionLabel("V" + (ver.getVersionNo() == null ? 1 : ver.getVersionNo()));
|
||||
Date start = ver.getEffectiveTime();
|
||||
Date end = ver.getExpireTime();
|
||||
int pending = ver.getPendingCount() == null ? 0 : ver.getPendingCount();
|
||||
int active = ver.getActiveCount() == null ? 0 : ver.getActiveCount();
|
||||
if (start == null)
|
||||
{
|
||||
ver.setStatus(pending > 0 ? TERM_PENDING : (active > 0 ? TERM_EFFECT : TERM_EXPIRED));
|
||||
}
|
||||
else if (start.after(now))
|
||||
{
|
||||
ver.setStatus(TERM_PENDING);
|
||||
}
|
||||
else if (end != null && !end.after(now))
|
||||
{
|
||||
ver.setStatus(TERM_EXPIRED);
|
||||
}
|
||||
else
|
||||
{
|
||||
ver.setStatus(TERM_EFFECT);
|
||||
}
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String activateVersion(Long priceSetId, Integer versionNo, Date effectiveTime)
|
||||
{
|
||||
assertLabelSet(priceSetId);
|
||||
if (versionNo == null)
|
||||
{
|
||||
throw new ServiceException("请指定版本号");
|
||||
}
|
||||
Date start = effectiveTime == null ? new Date() : effectiveTime;
|
||||
lblProductMapper.expireOtherActive(priceSetId, versionNo, yesterday(), SecurityUtils.getUsername());
|
||||
lblProductMapper.activateVersionRows(priceSetId, versionNo, start, SecurityUtils.getUsername());
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (set != null && !"E".equalsIgnoreCase(String.valueOf(set.getStatus())))
|
||||
{
|
||||
set.setStatus("E");
|
||||
set.setUpdateBy(SecurityUtils.getUsername());
|
||||
priceSetMapper.updatePrcPriceSet(set);
|
||||
}
|
||||
return "版本V" + versionNo + "已启动";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deleteVersion(Long priceSetId, Integer versionNo)
|
||||
{
|
||||
assertLabelSet(priceSetId);
|
||||
List<Long> ids = lblProductMapper.selectVersionIdsBySetAndNo(priceSetId, versionNo);
|
||||
if (ids != null)
|
||||
{
|
||||
for (Long versionId : ids)
|
||||
{
|
||||
lblProductMapper.deleteWeightBandsByVersion(versionId);
|
||||
lblProductMapper.deleteSurchargesByVersion(versionId);
|
||||
}
|
||||
}
|
||||
return lblProductMapper.deletePendingVersionsBySetAndNo(priceSetId, versionNo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LblProduct> selectProducts(Long priceSetId)
|
||||
{
|
||||
assertLabelSet(priceSetId);
|
||||
List<LblProduct> list = lblProductMapper.selectProductsBySet(priceSetId);
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LblProduct> selectProductsByVersion(Long priceSetId, Integer versionNo)
|
||||
{
|
||||
assertLabelSet(priceSetId);
|
||||
List<LblProduct> list = lblProductMapper.selectProductsBySetAndVersion(priceSetId, versionNo);
|
||||
return list == null ? Collections.emptyList() : list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LblProduct selectDetailByVersionId(Long versionId)
|
||||
{
|
||||
LblProductVersion ver = lblProductMapper.selectVersionById(versionId);
|
||||
if (ver == null)
|
||||
{
|
||||
throw new ServiceException("价卡版本不存在");
|
||||
}
|
||||
LblProduct product = lblProductMapper.selectProductById(ver.getProductId());
|
||||
if (product == null)
|
||||
{
|
||||
throw new ServiceException("产品不存在");
|
||||
}
|
||||
product.setVersionId(ver.getVersionId());
|
||||
product.setVersionNo(ver.getVersionNo());
|
||||
product.setVersionStatus(ver.getStatus());
|
||||
product.setVersionStart(ver.getEffectiveStart());
|
||||
product.setVersionEnd(ver.getEffectiveEnd());
|
||||
product.setWeightBands(lblProductMapper.selectWeightBandsByVersion(versionId));
|
||||
product.setSurcharges(lblProductMapper.selectSurchargesByVersion(versionId));
|
||||
return product;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateWeightBand(LblWeightBand band)
|
||||
{
|
||||
if (band == null || band.getBandId() == null)
|
||||
{
|
||||
throw new ServiceException("缺少重量段ID");
|
||||
}
|
||||
LblWeightBand exists = lblProductMapper.selectWeightBandById(band.getBandId());
|
||||
if (exists == null)
|
||||
{
|
||||
throw new ServiceException("重量段不存在");
|
||||
}
|
||||
if (band.getChargeMode() == null || band.getChargeMode().isEmpty())
|
||||
{
|
||||
band.setChargeMode(exists.getChargeMode());
|
||||
}
|
||||
if (band.getFeeType() == null || band.getFeeType().isEmpty())
|
||||
{
|
||||
band.setFeeType(exists.getFeeType());
|
||||
}
|
||||
if (band.getPostcodeParam() == null)
|
||||
{
|
||||
band.setPostcodeParam(exists.getPostcodeParam() == null ? "" : exists.getPostcodeParam());
|
||||
}
|
||||
return lblProductMapper.updateWeightBand(band);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateSurcharge(LblSurcharge surcharge)
|
||||
{
|
||||
if (surcharge == null || surcharge.getSurchargeId() == null)
|
||||
{
|
||||
throw new ServiceException("缺少附加费ID");
|
||||
}
|
||||
LblSurcharge exists = lblProductMapper.selectSurchargeById(surcharge.getSurchargeId());
|
||||
if (exists == null)
|
||||
{
|
||||
throw new ServiceException("附加费不存在");
|
||||
}
|
||||
return lblProductMapper.updateSurcharge(surcharge);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateDetail(LblProduct product)
|
||||
{
|
||||
if (product == null || product.getVersionId() == null)
|
||||
{
|
||||
throw new ServiceException("缺少版本ID");
|
||||
}
|
||||
LblBandNormalizer.normalizeProduct(product);
|
||||
prepareCounts(product);
|
||||
product.setUpdateBy(SecurityUtils.getUsername());
|
||||
lblProductMapper.updateProduct(product);
|
||||
Long versionId = product.getVersionId();
|
||||
lblProductMapper.deleteWeightBandsByVersion(versionId);
|
||||
lblProductMapper.deleteSurchargesByVersion(versionId);
|
||||
insertDetails(product, versionId);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int removeProducts(Long priceSetId, Long[] productIds)
|
||||
{
|
||||
assertLabelSet(priceSetId);
|
||||
if (productIds == null || productIds.length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
List<Long> versionIds = lblProductMapper.selectVersionIdsBySetAndProducts(priceSetId, productIds);
|
||||
if (versionIds != null)
|
||||
{
|
||||
for (Long versionId : versionIds)
|
||||
{
|
||||
lblProductMapper.deleteWeightBandsByVersion(versionId);
|
||||
lblProductMapper.deleteSurchargesByVersion(versionId);
|
||||
}
|
||||
}
|
||||
return lblProductMapper.deleteVersionsBySetAndProducts(priceSetId, productIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChannelProductMap> listProductChannelBinds(Long productId, String payType)
|
||||
{
|
||||
String type = isBlank(payType) ? PAY_TYPE_AR : payType.trim();
|
||||
List<ChannelProductMap> list = channelProductMapMapper.selectByProductIdAndType(productId, type);
|
||||
return list == null ? new ArrayList<ChannelProductMap>() : list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String replaceProductChannelBinds(Long productId, String payType, List<String> channelIds)
|
||||
{
|
||||
if (productId == null)
|
||||
{
|
||||
throw new ServiceException("产品不能为空");
|
||||
}
|
||||
String type = isBlank(payType) ? PAY_TYPE_AR : payType.trim();
|
||||
List<String> wanted = channelIds == null ? new ArrayList<String>() : channelIds.stream()
|
||||
.filter(s -> s != null && !s.trim().isEmpty()).map(String::trim).distinct().collect(Collectors.toList());
|
||||
List<ChannelProductMap> existing = channelProductMapMapper.selectByProductIdAndType(productId, type);
|
||||
if (existing != null)
|
||||
{
|
||||
for (ChannelProductMap row : existing)
|
||||
{
|
||||
if (row.getChannelId() != null && !wanted.contains(row.getChannelId()))
|
||||
{
|
||||
channelProductMapMapper.deleteByChannelAndType(row.getChannelId(), type);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (String channelId : wanted)
|
||||
{
|
||||
ChannelProductMap row = new ChannelProductMap();
|
||||
row.setChannelId(channelId);
|
||||
row.setProductId(productId);
|
||||
row.setPayType(type);
|
||||
channelProductMapMapper.upsert(row);
|
||||
}
|
||||
return "已绑定" + wanted.size() + "个渠道";
|
||||
}
|
||||
|
||||
@Override
|
||||
public LblProduct selectPriceForDate(Long priceSetId, String channelId, Date bizDate)
|
||||
{
|
||||
if (priceSetId == null || isBlank(channelId) || bizDate == null)
|
||||
{
|
||||
throw new ServiceException("打单计费参数不完整");
|
||||
}
|
||||
ChannelProductMap link = channelProductMapMapper.selectByChannelAndType(channelId, PAY_TYPE_AR);
|
||||
if (link == null || link.getProductId() == null)
|
||||
{
|
||||
throw new ServiceException("渠道还没有绑定打单应收产品");
|
||||
}
|
||||
List<LblProductVersion> versions = lblProductMapper.selectVersionsBySet(priceSetId);
|
||||
LblProductVersion hit = null;
|
||||
for (LblProductVersion ver : versions)
|
||||
{
|
||||
if (!link.getProductId().equals(ver.getProductId()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (!TERM_EFFECT.equals(ver.getStatus()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (ver.getEffectiveStart() == null || ver.getEffectiveStart().after(bizDate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (ver.getEffectiveEnd() != null && !ver.getEffectiveEnd().after(bizDate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
hit = ver;
|
||||
break;
|
||||
}
|
||||
if (hit == null)
|
||||
{
|
||||
throw new ServiceException("订单时间没有落在打单价卡有效期内");
|
||||
}
|
||||
return selectDetailByVersionId(hit.getVersionId());
|
||||
}
|
||||
|
||||
private void openVersion(LblProduct product, Long priceSetId, int versionNo, String operator)
|
||||
{
|
||||
LblProductVersion version = new LblProductVersion();
|
||||
version.setPriceSetId(priceSetId);
|
||||
version.setProductId(product.getProductId());
|
||||
version.setDictValue(product.getDictValue());
|
||||
version.setVersionNo(versionNo);
|
||||
version.setFilePath(product.getFilePath());
|
||||
version.setStatus(TERM_PENDING);
|
||||
version.setCreateBy(operator);
|
||||
version.setUpdateBy(operator);
|
||||
lblProductMapper.insertVersion(version);
|
||||
product.setVersionId(version.getVersionId());
|
||||
insertDetails(product, version.getVersionId());
|
||||
}
|
||||
|
||||
private void insertDetails(LblProduct product, Long versionId)
|
||||
{
|
||||
if (product.getWeightBands() != null)
|
||||
{
|
||||
int i = 0;
|
||||
for (LblWeightBand band : product.getWeightBands())
|
||||
{
|
||||
if (band == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
band.setProductId(product.getProductId());
|
||||
band.setVersionId(versionId);
|
||||
if (band.getCountryCode() == null)
|
||||
{
|
||||
band.setCountryCode("");
|
||||
}
|
||||
if (band.getZoneCode() == null)
|
||||
{
|
||||
band.setZoneCode("");
|
||||
}
|
||||
if (band.getPostcodeParam() == null)
|
||||
{
|
||||
band.setPostcodeParam("");
|
||||
}
|
||||
band.setSortNo(i++);
|
||||
lblProductMapper.insertWeightBand(band);
|
||||
}
|
||||
}
|
||||
if (product.getSurcharges() != null)
|
||||
{
|
||||
int i = 0;
|
||||
for (LblSurcharge fee : product.getSurcharges())
|
||||
{
|
||||
if (fee == null || isBlank(fee.getFeeName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
fee.setProductId(product.getProductId());
|
||||
fee.setVersionId(versionId);
|
||||
fee.setSortNo(i++);
|
||||
lblProductMapper.insertSurcharge(fee);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private VersionKeep clearEditableVersion(Long priceSetId, Integer versionNo, String operator)
|
||||
{
|
||||
List<LblProductVersion> rows = lblProductMapper.selectVersionsBySetAndNo(priceSetId, versionNo);
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
throw new ServiceException("版本不存在");
|
||||
}
|
||||
VersionKeep keep = new VersionKeep();
|
||||
keep.effectiveStart = rows.get(0).getEffectiveStart();
|
||||
keep.status = rows.get(0).getStatus();
|
||||
for (LblProductVersion row : rows)
|
||||
{
|
||||
if (!TERM_PENDING.equals(row.getStatus()) && !TERM_EFFECT.equals(row.getStatus()))
|
||||
{
|
||||
throw new ServiceException("只能编辑待生效或已生效版本");
|
||||
}
|
||||
lblProductMapper.deleteWeightBandsByVersion(row.getVersionId());
|
||||
lblProductMapper.deleteSurchargesByVersion(row.getVersionId());
|
||||
}
|
||||
lblProductMapper.deleteAllVersionsBySetAndNo(priceSetId, versionNo);
|
||||
return keep;
|
||||
}
|
||||
|
||||
private Long ensureLabelPriceSet(LblSaveCommand command)
|
||||
{
|
||||
if (command.getPriceSetId() != null)
|
||||
{
|
||||
assertLabelSet(command.getPriceSetId());
|
||||
return command.getPriceSetId();
|
||||
}
|
||||
if (isBlank(command.getPriceSetName()))
|
||||
{
|
||||
throw new ServiceException("请填写打单套餐名称");
|
||||
}
|
||||
String operator = SecurityUtils.getUsername();
|
||||
PrcPriceSet set = new PrcPriceSet();
|
||||
set.setPriceSetName(command.getPriceSetName().trim());
|
||||
set.setPriceSetNote(command.getPriceSetNote());
|
||||
set.setSetType("FIXED");
|
||||
set.setSetCategory(PrcPriceSet.CATEGORY_LABEL);
|
||||
set.setStatus("D");
|
||||
set.setCreateBy(operator);
|
||||
set.setUpdateBy(operator);
|
||||
priceSetMapper.insertPrcPriceSet(set);
|
||||
set.setPriceSetCode(String.format("LBL%04d", set.getPriceSetId()));
|
||||
priceSetMapper.updatePrcPriceSet(set);
|
||||
command.setPriceSetId(set.getPriceSetId());
|
||||
return set.getPriceSetId();
|
||||
}
|
||||
|
||||
private void assertLabelSet(Long priceSetId)
|
||||
{
|
||||
PrcPriceSet db = priceSetMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (db == null)
|
||||
{
|
||||
throw new ServiceException("打单套餐不存在");
|
||||
}
|
||||
if (!PrcPriceSet.CATEGORY_LABEL.equalsIgnoreCase(String.valueOf(db.getSetCategory())))
|
||||
{
|
||||
throw new ServiceException("只能操作打单套餐");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasPending(Long priceSetId, Long productId)
|
||||
{
|
||||
List<LblProductVersion> list = lblProductMapper.selectVersionsBySet(priceSetId);
|
||||
if (list == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (LblProductVersion ver : list)
|
||||
{
|
||||
if (productId.equals(ver.getProductId()) && TERM_PENDING.equals(ver.getStatus()))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void collectZoneSync(Map<String, Map<String, List<String>>> byCountry, LblProduct product)
|
||||
{
|
||||
if (product == null || product.getZonePostcodes() == null || product.getZonePostcodes().isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
String country = LblZonePostcodeAttach.countryCodeOf(product);
|
||||
if (country == null || country.isBlank())
|
||||
{
|
||||
throw new ServiceException("产品「" + product.getProductName() + "」有邮编分区表,但无法识别国家代码");
|
||||
}
|
||||
Map<String, List<String>> bucket = byCountry.computeIfAbsent(country, k -> new LinkedHashMap<String, List<String>>());
|
||||
for (Map.Entry<String, List<String>> e : product.getZonePostcodes().entrySet())
|
||||
{
|
||||
if (e.getKey() == null || e.getValue() == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
List<String> list = bucket.computeIfAbsent(e.getKey(), k -> new ArrayList<String>());
|
||||
for (String pc : e.getValue())
|
||||
{
|
||||
if (pc != null && !pc.isBlank() && !list.contains(pc))
|
||||
{
|
||||
list.add(pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareCounts(LblProduct product)
|
||||
{
|
||||
int bands = product.getWeightBands() == null ? 0 : product.getWeightBands().size();
|
||||
int fees = product.getSurcharges() == null ? 0 : product.getSurcharges().size();
|
||||
product.setWeightCount(bands);
|
||||
product.setSurchargeCount(fees);
|
||||
}
|
||||
|
||||
private Set<String> loadDictValues()
|
||||
{
|
||||
List<SysDictData> rows = dictTypeService.selectDictDataByType(DICT_TYPE);
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
throw PaymentMesEnum.BMS_PAY_DICT_EMPTY.exception();
|
||||
}
|
||||
return rows.stream().map(SysDictData::getDictValue).filter(v -> v != null && !v.isBlank())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private String storeSource(byte[] bytes, String filename)
|
||||
{
|
||||
String original = isBlank(filename) ? "import.xlsx" : filename.trim();
|
||||
String ext = "xlsx";
|
||||
int dot = original.lastIndexOf('.');
|
||||
if (dot >= 0 && dot < original.length() - 1)
|
||||
{
|
||||
ext = original.substring(dot + 1).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
String contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
String objectKey = "price/label/" + DateUtils.datePath() + "/" + IdUtils.fastSimpleUUID() + "." + ext;
|
||||
try
|
||||
{
|
||||
return ossStorage.upload(new ByteArrayInputStream(bytes), bytes.length, objectKey, contentType);
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
throw PaymentMesEnum.BMS_PAY_FILE_UP.exception();
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveSource(LblProduct product)
|
||||
{
|
||||
String path = sourcePath(product.getFilePath());
|
||||
if (path == null)
|
||||
{
|
||||
path = rememberedSource(product.getFileMd5());
|
||||
}
|
||||
if (path == null)
|
||||
{
|
||||
throw PaymentMesEnum.BMS_PAY_FILE_WAIT.exception();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private void rememberSource(String fileMd5, String path)
|
||||
{
|
||||
if (isBlank(fileMd5) || sourcePath(path) == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
uploadedSources.put(fileMd5, new UploadedSource(path, System.currentTimeMillis()));
|
||||
long now = System.currentTimeMillis();
|
||||
Iterator<Map.Entry<String, UploadedSource>> it = uploadedSources.entrySet().iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Map.Entry<String, UploadedSource> e = it.next();
|
||||
if (now - e.getValue().at > UPLOAD_KEEP_MS)
|
||||
{
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String rememberedSource(String fileMd5)
|
||||
{
|
||||
if (isBlank(fileMd5))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
UploadedSource src = uploadedSources.get(fileMd5);
|
||||
if (src == null || System.currentTimeMillis() - src.at > UPLOAD_KEEP_MS)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return src.path;
|
||||
}
|
||||
|
||||
private static String sourcePath(String path)
|
||||
{
|
||||
return isBlank(path) ? null : path.trim();
|
||||
}
|
||||
|
||||
private String progressKey(String progressId)
|
||||
{
|
||||
return isBlank(progressId) ? null : progressId.trim();
|
||||
}
|
||||
|
||||
private void writeProgress(String key, int percent, String message, String status)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
parseProgress.put(key, new ParseSlot(new PayParseProgress(percent, message, status), System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
private static Date yesterday()
|
||||
{
|
||||
return new Date(System.currentTimeMillis() - 24L * 60L * 60L * 1000L);
|
||||
}
|
||||
|
||||
private static boolean isBlank(String v)
|
||||
{
|
||||
return v == null || v.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String blankTo(String v, String fallback)
|
||||
{
|
||||
return isBlank(v) ? fallback : v.trim();
|
||||
}
|
||||
|
||||
private static final class ParseSlot
|
||||
{
|
||||
private final PayParseProgress progress;
|
||||
private final long at;
|
||||
|
||||
private ParseSlot(PayParseProgress progress, long at)
|
||||
{
|
||||
this.progress = progress;
|
||||
this.at = at;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class UploadedSource
|
||||
{
|
||||
private final String path;
|
||||
private final long at;
|
||||
|
||||
private UploadedSource(String path, long at)
|
||||
{
|
||||
this.path = path;
|
||||
this.at = at;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class VersionKeep
|
||||
{
|
||||
private Date effectiveStart;
|
||||
private String status;
|
||||
}
|
||||
}
|
||||
@@ -276,6 +276,110 @@ public class PrcZoneServiceImpl implements IPrcZoneService
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String syncCountryZones(String countryCode2, Map<String, List<String>> zonePostcodes)
|
||||
{
|
||||
if (StringUtils.isEmpty(countryCode2))
|
||||
{
|
||||
throw new ServiceException("国家代码不能为空");
|
||||
}
|
||||
if (zonePostcodes == null || zonePostcodes.isEmpty())
|
||||
{
|
||||
return "无邮编分区需同步";
|
||||
}
|
||||
String code = countryCode2.trim().toUpperCase(Locale.ROOT);
|
||||
BmsRegion region = regionMapper.selectRegionByCode2(code);
|
||||
if (region == null || region.getRegionId() == null)
|
||||
{
|
||||
throw new ServiceException("分区管理未找到国家 " + code);
|
||||
}
|
||||
List<PrcZone> existing = zoneMapper.selectPrcZoneListByRegionId(region.getRegionId());
|
||||
Map<String, PrcZone> byCode = new HashMap<String, PrcZone>();
|
||||
if (existing != null)
|
||||
{
|
||||
for (PrcZone z : existing)
|
||||
{
|
||||
if (z != null && StringUtils.isNotEmpty(z.getZoneCode()))
|
||||
{
|
||||
byCode.put(z.getZoneCode().trim().toUpperCase(Locale.ROOT), z);
|
||||
}
|
||||
}
|
||||
}
|
||||
int zoneUpsert = 0;
|
||||
int detailInserted = 0;
|
||||
List<Map.Entry<String, List<String>>> entries = new ArrayList<>(zonePostcodes.entrySet());
|
||||
entries.sort(Comparator.comparingInt(e -> extractZoneSort(e.getKey())));
|
||||
for (Map.Entry<String, List<String>> entry : entries)
|
||||
{
|
||||
String label = StringUtils.trim(entry.getKey());
|
||||
if (StringUtils.isEmpty(label))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
label = label.replace('\r', ' ').replace('\n', ' ').replaceAll("\\s+", " ").trim();
|
||||
String zoneCode = ZoneCodeAbbrevs.toAbbrev(label);
|
||||
if (StringUtils.isEmpty(zoneCode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (zoneCode.length() > 50)
|
||||
{
|
||||
zoneCode = zoneCode.substring(0, 50);
|
||||
}
|
||||
String key = zoneCode.trim().toUpperCase(Locale.ROOT);
|
||||
PrcZone zone = byCode.get(key);
|
||||
if (zone == null)
|
||||
{
|
||||
zone = new PrcZone();
|
||||
zone.setRegionId(region.getRegionId());
|
||||
zone.setCountryCode(code);
|
||||
zone.setZoneCode(zoneCode);
|
||||
zone.setZoneName(label.length() > 100 ? label.substring(0, 100) : label);
|
||||
zone.setZoneSort(extractZoneSort(label));
|
||||
zoneMapper.insertPrcZone(zone);
|
||||
byCode.put(key, zone);
|
||||
}
|
||||
else
|
||||
{
|
||||
zone.setZoneName(label.length() > 100 ? label.substring(0, 100) : label);
|
||||
zoneMapper.updatePrcZone(zone);
|
||||
detailMapper.deletePrcZoneDetailByZoneId(zone.getZoneId());
|
||||
}
|
||||
zoneUpsert++;
|
||||
Set<String> uniquePostcodes = new LinkedHashSet<String>();
|
||||
if (entry.getValue() != null)
|
||||
{
|
||||
uniquePostcodes.addAll(entry.getValue());
|
||||
}
|
||||
List<PrcZoneDetail> batch = new ArrayList<>(BATCH_SIZE);
|
||||
for (String postcode : uniquePostcodes)
|
||||
{
|
||||
if (StringUtils.isEmpty(postcode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
PrcZoneDetail detail = new PrcZoneDetail();
|
||||
detail.setZoneId(zone.getZoneId());
|
||||
detail.setPostcode(postcode.trim());
|
||||
batch.add(detail);
|
||||
if (batch.size() >= BATCH_SIZE)
|
||||
{
|
||||
detailMapper.insertPrcZoneDetailBatch(batch);
|
||||
detailInserted += batch.size();
|
||||
batch = new ArrayList<>(BATCH_SIZE);
|
||||
}
|
||||
}
|
||||
if (!batch.isEmpty())
|
||||
{
|
||||
detailMapper.insertPrcZoneDetailBatch(batch);
|
||||
detailInserted += batch.size();
|
||||
}
|
||||
}
|
||||
return "已同步 " + code + " 分区" + zoneUpsert + "个、邮编" + detailInserted + "条";
|
||||
}
|
||||
|
||||
private int[] replaceRegionZones(BmsRegion region, Map<String, List<String>> zonePostcodes)
|
||||
{
|
||||
if (StringUtils.isEmpty(region.getCountryCode2()))
|
||||
|
||||
+160
-8
@@ -8,6 +8,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -42,7 +43,7 @@ import com.bms.price.domain.PrcQuote;
|
||||
import com.bms.price.domain.PrcQuoteFee;
|
||||
import com.bms.price.domain.PrcSurchargeRel;
|
||||
import com.bms.price.domain.PrcZone;
|
||||
import com.bms.price.pay.LabelArCostCalculator;
|
||||
import com.bms.price.pay.PayZoneMatcher;
|
||||
import com.bms.price.mapper.PrcZoneDetailMapper;
|
||||
import com.bms.price.domain.vo.TmsPriceBillRequest;
|
||||
import com.bms.price.domain.vo.TmsPriceQuoteFeeLine;
|
||||
@@ -68,14 +69,20 @@ import com.bms.price.mapper.PrcSurchargeRelMapper;
|
||||
import com.bms.price.search.EsBizLogSupport;
|
||||
import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.service.ILblProductService;
|
||||
import com.bms.price.service.IPayProductService;
|
||||
import com.bms.price.label.LblCostCalculator;
|
||||
import com.bms.price.label.LblProductPayAdapter;
|
||||
import com.bms.price.domain.LblFeeType;
|
||||
import com.bms.price.domain.LblProduct;
|
||||
import com.bms.price.domain.LblWeightBand;
|
||||
import com.bms.price.service.ITmsPriceQuoteService;
|
||||
import com.bms.system.service.IBmsProductCodeMappingService;
|
||||
|
||||
/**
|
||||
* 共用的试算/计费计算:按 bizDate 取客户绑定和套餐版本(版本 effective_time)。
|
||||
* 绑定的 pr_begindate/pr_enddate 不作为计费时间门槛。
|
||||
* 先按 prc_price_set.set_category 分流:LABEL 走打单 pay_*;PACKAGE/EXCLUSIVE 走销售快照。
|
||||
* 先按 prc_price_set.set_category 分流:LABEL 走打单 prc_label_*;PACKAGE/EXCLUSIVE 走销售快照。
|
||||
* 销售价只来自套餐快照(FIXED 明细 / 按目录底价 DISCOUNT)。
|
||||
* 无绑定或无生效版本:试算和计费都直接失败(不用 PRODUCT_DEFAULT)。
|
||||
* 账单同步时,产品/国家/重量段未命中为软失败(billing_fail_status 1/2/3,金额为 0)。
|
||||
@@ -144,6 +151,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
@Autowired
|
||||
private IPayProductService payProductService;
|
||||
|
||||
@Autowired
|
||||
private ILblProductService lblProductService;
|
||||
|
||||
@Autowired
|
||||
private PrcOrderBillExtMapper orderBillExtMapper;
|
||||
|
||||
@@ -209,7 +219,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return result;
|
||||
}
|
||||
|
||||
/** LABEL 费用已由 LabelArCostCalculator 算完,不再叠加销售附加费。 */
|
||||
/** LABEL 费用已由 LblCostCalculator 算完,不再叠加销售附加费。 */
|
||||
private void applySalesSurchargesIfNeeded(TmsPriceQuoteResult result)
|
||||
{
|
||||
if (result == null || SRC_LABEL.equals(result.getMatchSource()))
|
||||
@@ -665,7 +675,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* 打单套餐计费:渠道 AR → product_id,套餐+时间 → term,LabelArCostCalculator 算费(与应付独立)。
|
||||
* 打单套餐计费:渠道 AR → product_id,套餐+时间 → version,LblCostCalculator 算费。
|
||||
*/
|
||||
private TmsPriceQuoteResult doLabelQuote(TmsPriceQuoteRequest request, PrcCustomerPriceSet bind,
|
||||
PrcPriceSet set, String productCode, String productName, String countryCode, String countryName,
|
||||
@@ -681,10 +691,10 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
throw new ServiceException("打单计费需要渠道");
|
||||
}
|
||||
PayProduct product;
|
||||
LblProduct lblProduct;
|
||||
try
|
||||
{
|
||||
product = payProductService.selectLabelPriceForDate(set.getPriceSetId(), channelId, bizDate);
|
||||
lblProduct = lblProductService.selectPriceForDate(set.getPriceSetId(), channelId, bizDate);
|
||||
}
|
||||
catch (ServiceException ex)
|
||||
{
|
||||
@@ -695,6 +705,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
PayProduct product = LblProductPayAdapter.toPayProduct(lblProduct);
|
||||
BigDecimal length = null;
|
||||
BigDecimal width = null;
|
||||
BigDecimal height = null;
|
||||
@@ -707,8 +718,27 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
height = billReq.getPkgHeight();
|
||||
orderStatus = billReq.getOrderStatus();
|
||||
}
|
||||
PayOrderCostResult cost = LabelArCostCalculator.calculate(product, weight, length, width, height,
|
||||
request.getDestPostcode(), bizDate, orderStatus);
|
||||
String freightZone;
|
||||
try
|
||||
{
|
||||
freightZone = resolveLabelFreightZone(lblProduct, countryCode, request.getDestPostcode());
|
||||
}
|
||||
catch (ServiceException ex)
|
||||
{
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
Integer fail = BillBillingFailStatus.POSTCODE_ZONE_NOT_FOUND.getCode();
|
||||
if (ex.getMessage() != null && ex.getMessage().contains("目的邮编"))
|
||||
{
|
||||
fail = BillBillingFailStatus.POSTCODE_REQUIRED.getCode();
|
||||
}
|
||||
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
|
||||
weight, bizDate, fail);
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
PayOrderCostResult cost = LblCostCalculator.calculate(lblProduct, weight, length, width, height,
|
||||
request.getDestPostcode(), freightZone, bizDate, orderStatus);
|
||||
if (cost == null || cost.getFees() == null || cost.getFees().isEmpty())
|
||||
{
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
@@ -725,6 +755,128 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打单多分区价卡:先按国家收窄运费分区;
|
||||
* 该国只有 1 个运费分区(如 PT Mainland)直接用,不强依分区管理;
|
||||
* 多分区(如 ES 密度)再用 prc_zone 按邮编解析。
|
||||
*/
|
||||
private String resolveLabelFreightZone(LblProduct product, String countryCode, String destPostcode)
|
||||
{
|
||||
List<String> zoneNames = labelBandZoneNames(product);
|
||||
if (zoneNames.size() <= 1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (StringUtils.isEmpty(destPostcode))
|
||||
{
|
||||
throw new ServiceException("请填写目的邮编");
|
||||
}
|
||||
String cc = countryCode == null ? "" : countryCode.trim().toUpperCase(Locale.ROOT);
|
||||
List<String> countryFreight = labelFreightZonesForCountry(product, cc);
|
||||
if (countryFreight.size() == 1)
|
||||
{
|
||||
return countryFreight.get(0);
|
||||
}
|
||||
List<String> candidates = countryFreight.isEmpty() ? zoneNames : countryFreight;
|
||||
if (StringUtils.isEmpty(cc))
|
||||
{
|
||||
throw new ServiceException("邮编未匹配到分区");
|
||||
}
|
||||
PrcZone resolved = zoneDetailMapper.selectZoneByCountryPostcode(cc, normalizePostcode(destPostcode));
|
||||
if (resolved == null)
|
||||
{
|
||||
throw new ServiceException("邮编未匹配到分区");
|
||||
}
|
||||
String mapped = PayZoneMatcher.mapRegion(candidates, resolved.getZoneCode());
|
||||
if (mapped == null)
|
||||
{
|
||||
mapped = PayZoneMatcher.mapRegion(candidates, resolved.getZoneName());
|
||||
}
|
||||
if (mapped == null)
|
||||
{
|
||||
throw new ServiceException("邮编分区「"
|
||||
+ (resolved.getZoneName() != null ? resolved.getZoneName() : resolved.getZoneCode())
|
||||
+ "」对不上价卡地区");
|
||||
}
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private static List<String> labelBandZoneNames(LblProduct product)
|
||||
{
|
||||
LinkedHashSet<String> names = new LinkedHashSet<String>();
|
||||
if (product == null || product.getWeightBands() == null)
|
||||
{
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
for (LblWeightBand band : product.getWeightBands())
|
||||
{
|
||||
if (band == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String zone = band.getZoneName();
|
||||
if (zone != null && !zone.isBlank())
|
||||
{
|
||||
names.add(zone.trim());
|
||||
}
|
||||
}
|
||||
return new ArrayList<String>(names);
|
||||
}
|
||||
|
||||
/** 该国家下运费(FREIGHT)分区列名;偏远行不算入。 */
|
||||
private static List<String> labelFreightZonesForCountry(LblProduct product, String countryCode)
|
||||
{
|
||||
LinkedHashSet<String> names = new LinkedHashSet<String>();
|
||||
if (product == null || product.getWeightBands() == null)
|
||||
{
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
for (LblWeightBand band : product.getWeightBands())
|
||||
{
|
||||
if (band == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String ft = band.getFeeType();
|
||||
if (ft != null && !ft.isEmpty() && !LblFeeType.FREIGHT.equals(ft))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String zone = band.getZoneName();
|
||||
if (zone == null || zone.isBlank())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (StringUtils.isNotEmpty(countryCode) && !bandCountryMatches(band.getCountryCode(), countryCode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
names.add(zone.trim());
|
||||
}
|
||||
return new ArrayList<String>(names);
|
||||
}
|
||||
|
||||
private static boolean bandCountryMatches(String bandCountryCode, String orderCountryCode)
|
||||
{
|
||||
if (StringUtils.isEmpty(orderCountryCode))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (StringUtils.isEmpty(bandCountryCode))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
String order = orderCountryCode.trim().toUpperCase(Locale.ROOT);
|
||||
for (String part : bandCountryCode.split("[,;/\\s]+"))
|
||||
{
|
||||
if (part != null && order.equals(part.trim().toUpperCase(Locale.ROOT)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 试算用 request.channelId;计费优先 prc_order_bill.product_floor_code,
|
||||
* 没有再取 prc_order_bill_ext.product_floor_code,再解析为渠道ID。
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.bms.price.task;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.LblSaveCommand;
|
||||
import com.bms.price.domain.LblSaveTask;
|
||||
import com.bms.price.pay.PayParseProgressListener;
|
||||
import com.bms.price.service.ILblProductService;
|
||||
|
||||
/**
|
||||
* 打单价卡异步保存:进度存内存(与解析进度一致),前端轮询。
|
||||
*/
|
||||
@Component
|
||||
public class LblSaveAsyncRunner
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(LblSaveAsyncRunner.class);
|
||||
|
||||
private static final long KEEP_MS = 60L * 60L * 1000L;
|
||||
|
||||
private final Map<String, Slot> tasks = new ConcurrentHashMap<String, Slot>();
|
||||
|
||||
@Autowired
|
||||
@Lazy
|
||||
private ILblProductService lblProductService;
|
||||
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void run(String taskId, LblSaveCommand command, Authentication authentication)
|
||||
{
|
||||
SecurityContext previous = SecurityContextHolder.getContext();
|
||||
try
|
||||
{
|
||||
if (authentication != null)
|
||||
{
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
context.setAuthentication(authentication);
|
||||
SecurityContextHolder.setContext(context);
|
||||
}
|
||||
put(taskId, LblSaveTask.STATUS_RUNNING, 3, "正在准备保存",
|
||||
command == null ? null : command.getPriceSetId(), null);
|
||||
PayParseProgressListener listener = (percent, message) -> put(taskId, LblSaveTask.STATUS_RUNNING,
|
||||
percent, message, command == null ? null : command.getPriceSetId(), null);
|
||||
String message = lblProductService.save(command, listener);
|
||||
put(taskId, LblSaveTask.STATUS_SUCCESS, 100, message,
|
||||
command == null ? null : command.getPriceSetId(), null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("async label save failed, taskId={}", taskId, e);
|
||||
String msg = e.getMessage();
|
||||
if (StringUtils.isEmpty(msg))
|
||||
{
|
||||
msg = e.getClass().getSimpleName();
|
||||
}
|
||||
put(taskId, LblSaveTask.STATUS_FAIL, 100, msg,
|
||||
command == null ? null : command.getPriceSetId(), msg);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SecurityContextHolder.setContext(previous);
|
||||
}
|
||||
}
|
||||
|
||||
public void put(String taskId, String status, int percent, String message, Long priceSetId, String error)
|
||||
{
|
||||
LblSaveTask task = new LblSaveTask();
|
||||
task.setTaskId(taskId);
|
||||
task.setStatus(status);
|
||||
task.setPercent(percent);
|
||||
task.setMessage(message);
|
||||
task.setPriceSetId(priceSetId);
|
||||
task.setError(error);
|
||||
tasks.put(taskId, new Slot(task, System.currentTimeMillis()));
|
||||
if (tasks.size() > 200)
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
Iterator<Map.Entry<String, Slot>> it = tasks.entrySet().iterator();
|
||||
while (it.hasNext())
|
||||
{
|
||||
Map.Entry<String, Slot> e = it.next();
|
||||
if (now - e.getValue().at > KEEP_MS)
|
||||
{
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LblSaveTask get(String taskId)
|
||||
{
|
||||
if (taskId == null || taskId.isBlank())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Slot slot = tasks.get(taskId.trim());
|
||||
if (slot == null || System.currentTimeMillis() - slot.at > KEEP_MS)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return slot.task;
|
||||
}
|
||||
|
||||
private static final class Slot
|
||||
{
|
||||
private final LblSaveTask task;
|
||||
private final long at;
|
||||
|
||||
private Slot(LblSaveTask task, long at)
|
||||
{
|
||||
this.task = task;
|
||||
this.at = at;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.bms.price.mapper.LblProductMapper">
|
||||
|
||||
<insert id="insertProduct" useGeneratedKeys="true" keyProperty="productId">
|
||||
insert into prc_label_product (
|
||||
product_name, dict_value, country_scope, charge_type, extra_per_kg, service_type, classification,
|
||||
catalog_remark, update_content, weight_limit, sheet_name, related_sheet, source_file, file_path, file_md5,
|
||||
supplier_name, currency, service_note, parse_status, weight_count, surcharge_count,
|
||||
created_by, create_time, updated_by, update_time, remark
|
||||
) values (
|
||||
#{productName}, #{dictValue}, #{countryScope}, #{chargeType}, #{extraPerKg}, #{serviceType}, #{classification},
|
||||
#{catalogRemark}, #{updateContent}, #{weightLimit}, #{sheetName}, #{relatedSheet}, #{sourceFile}, #{filePath}, #{fileMd5},
|
||||
#{supplierName}, #{currency}, #{serviceNote}, ifnull(#{parseStatus}, 'OK'), ifnull(#{weightCount}, 0), ifnull(#{surchargeCount}, 0),
|
||||
#{createBy}, sysdate(), #{updateBy}, sysdate(), #{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateProduct">
|
||||
update prc_label_product
|
||||
set product_name = #{productName},
|
||||
dict_value = #{dictValue},
|
||||
country_scope = #{countryScope},
|
||||
charge_type = #{chargeType},
|
||||
extra_per_kg = #{extraPerKg},
|
||||
service_type = #{serviceType},
|
||||
classification = #{classification},
|
||||
catalog_remark = #{catalogRemark},
|
||||
update_content = #{updateContent},
|
||||
weight_limit = #{weightLimit},
|
||||
sheet_name = #{sheetName},
|
||||
related_sheet = #{relatedSheet},
|
||||
source_file = #{sourceFile},
|
||||
file_path = #{filePath},
|
||||
file_md5 = #{fileMd5},
|
||||
supplier_name = #{supplierName},
|
||||
currency = #{currency},
|
||||
service_note = #{serviceNote},
|
||||
parse_status = ifnull(#{parseStatus}, parse_status),
|
||||
weight_count = ifnull(#{weightCount}, weight_count),
|
||||
surcharge_count = ifnull(#{surchargeCount}, surcharge_count),
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate(),
|
||||
remark = #{remark}
|
||||
where product_id = #{productId}
|
||||
</update>
|
||||
|
||||
<select id="selectProductById" resultType="com.bms.price.domain.LblProduct">
|
||||
select product_id as productId, product_name as productName, dict_value as dictValue, country_scope as countryScope,
|
||||
charge_type as chargeType, extra_per_kg as extraPerKg, service_type as serviceType, classification,
|
||||
catalog_remark as catalogRemark, update_content as updateContent, weight_limit as weightLimit,
|
||||
sheet_name as sheetName, related_sheet as relatedSheet, source_file as sourceFile, file_path as filePath,
|
||||
file_md5 as fileMd5, supplier_name as supplierName, currency, service_note as serviceNote,
|
||||
parse_status as parseStatus, weight_count as weightCount, surcharge_count as surchargeCount,
|
||||
created_by as createBy, create_time as createTime, updated_by as updateBy, update_time as updateTime, remark
|
||||
from prc_label_product where product_id = #{productId}
|
||||
</select>
|
||||
|
||||
<select id="selectProductByDict" resultType="com.bms.price.domain.LblProduct">
|
||||
select product_id as productId, product_name as productName, dict_value as dictValue
|
||||
from prc_label_product where dict_value = #{dictValue} order by product_id desc limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectProductIdBySetAndDict" resultType="long">
|
||||
select v.product_id
|
||||
from prc_label_product_version v
|
||||
where v.price_set_id = #{priceSetId} and v.dict_value = #{dictValue}
|
||||
order by v.version_id desc limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertVersion" useGeneratedKeys="true" keyProperty="versionId">
|
||||
insert into prc_label_product_version (
|
||||
price_set_id, product_id, dict_value, version_no, effective_start, effective_end, file_path, status,
|
||||
created_by, create_time, updated_by, update_time, remark
|
||||
) values (
|
||||
#{priceSetId}, #{productId}, #{dictValue}, ifnull(#{versionNo}, 1), #{effectiveStart}, #{effectiveEnd}, #{filePath},
|
||||
ifnull(#{status}, '0'), #{createBy}, sysdate(), #{updateBy}, sysdate(), #{remark}
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateVersion">
|
||||
update prc_label_product_version
|
||||
set effective_start = #{effectiveStart},
|
||||
effective_end = #{effectiveEnd},
|
||||
file_path = #{filePath},
|
||||
status = #{status},
|
||||
updated_by = #{updateBy},
|
||||
update_time = sysdate(),
|
||||
remark = #{remark}
|
||||
where version_id = #{versionId}
|
||||
</update>
|
||||
|
||||
<update id="closePendingBySetProduct">
|
||||
update prc_label_product_version
|
||||
set status = '2',
|
||||
effective_end = ifnull(effective_end, #{endDate}),
|
||||
updated_by = #{operator},
|
||||
update_time = sysdate()
|
||||
where price_set_id = #{priceSetId}
|
||||
and product_id = #{productId}
|
||||
and status = '0'
|
||||
</update>
|
||||
|
||||
<select id="selectVersionIdsBySetAndNo" resultType="long">
|
||||
select version_id from prc_label_product_version
|
||||
where price_set_id = #{priceSetId} and version_no = #{versionNo}
|
||||
</select>
|
||||
|
||||
<delete id="deletePendingVersionsBySetAndNo">
|
||||
delete from prc_label_product_version
|
||||
where price_set_id = #{priceSetId} and version_no = #{versionNo} and status = '0'
|
||||
</delete>
|
||||
|
||||
<delete id="deleteAllVersionsBySetAndNo">
|
||||
delete from prc_label_product_version
|
||||
where price_set_id = #{priceSetId} and version_no = #{versionNo}
|
||||
</delete>
|
||||
|
||||
<select id="selectVersionsBySet" resultType="com.bms.price.domain.LblProductVersion">
|
||||
select v.version_id as versionId, v.price_set_id as priceSetId, v.product_id as productId, v.dict_value as dictValue,
|
||||
v.version_no as versionNo, v.effective_start as effectiveStart, v.effective_end as effectiveEnd, v.file_path as filePath,
|
||||
v.status, p.product_name as productName, p.source_file as sourceFile, p.weight_count as weightCount,
|
||||
p.surcharge_count as surchargeCount, p.currency
|
||||
from prc_label_product_version v
|
||||
join prc_label_product p on p.product_id = v.product_id
|
||||
where v.price_set_id = #{priceSetId} and v.status in ('0', '1')
|
||||
order by p.product_name, v.version_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectVersionsBySetAndNo" resultType="com.bms.price.domain.LblProductVersion">
|
||||
select v.version_id as versionId, v.price_set_id as priceSetId, v.product_id as productId, v.dict_value as dictValue,
|
||||
v.version_no as versionNo, v.effective_start as effectiveStart, v.effective_end as effectiveEnd, v.file_path as filePath,
|
||||
v.status, p.product_name as productName, p.source_file as sourceFile, p.weight_count as weightCount,
|
||||
p.surcharge_count as surchargeCount, p.currency
|
||||
from prc_label_product_version v
|
||||
join prc_label_product p on p.product_id = v.product_id
|
||||
where v.price_set_id = #{priceSetId} and v.version_no = #{versionNo}
|
||||
order by p.product_name, v.version_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectVersionById" resultType="com.bms.price.domain.LblProductVersion">
|
||||
select version_id as versionId, price_set_id as priceSetId, product_id as productId, dict_value as dictValue,
|
||||
version_no as versionNo, effective_start as effectiveStart, effective_end as effectiveEnd, file_path as filePath, status
|
||||
from prc_label_product_version where version_id = #{versionId}
|
||||
</select>
|
||||
|
||||
<select id="selectMaxVersionNo" resultType="int">
|
||||
select ifnull(max(version_no), 0) from prc_label_product_version where price_set_id = #{priceSetId}
|
||||
</select>
|
||||
|
||||
<select id="selectLabelVersionGroups" resultType="com.bms.price.domain.LabelPackageVersion">
|
||||
select price_set_id as priceSetId,
|
||||
version_no as versionNo,
|
||||
min(effective_start) as effectiveTime,
|
||||
max(effective_end) as expireTime,
|
||||
count(1) as productCount,
|
||||
sum(case when status = '1' then 1 else 0 end) as activeCount,
|
||||
sum(case when status = '0' then 1 else 0 end) as pendingCount,
|
||||
min(create_time) as createTime
|
||||
from prc_label_product_version
|
||||
where price_set_id = #{priceSetId}
|
||||
group by price_set_id, version_no
|
||||
order by version_no desc
|
||||
</select>
|
||||
|
||||
<insert id="insertWeightBand" useGeneratedKeys="true" keyProperty="bandId">
|
||||
insert into prc_label_weight_band (
|
||||
product_id, version_id, fee_type, country_name, country_code, zone_code, zone_name, postcode_param,
|
||||
weight_label, weight_min, weight_max, volume_text, charge_name, charge_mode, calc_param,
|
||||
price, price_unit, extra_price, extra_price_unit, sort_no
|
||||
) values (
|
||||
#{productId}, #{versionId}, ifnull(#{feeType}, 'FREIGHT'), #{countryName}, ifnull(#{countryCode}, ''), ifnull(#{zoneCode}, ''), #{zoneName}, ifnull(#{postcodeParam}, ''),
|
||||
#{weightLabel}, #{weightMin}, #{weightMax}, #{volumeText}, #{chargeName}, ifnull(#{chargeMode}, 'ITEM'), #{calcParam},
|
||||
#{price}, #{priceUnit}, #{extraPrice}, #{extraPriceUnit}, ifnull(#{sortNo}, 0)
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateWeightBand">
|
||||
update prc_label_weight_band
|
||||
set fee_type = ifnull(#{feeType}, fee_type),
|
||||
charge_mode = ifnull(#{chargeMode}, charge_mode),
|
||||
calc_param = #{calcParam},
|
||||
postcode_param = ifnull(#{postcodeParam}, ''),
|
||||
price = #{price},
|
||||
extra_price = #{extraPrice}
|
||||
where band_id = #{bandId}
|
||||
</update>
|
||||
|
||||
<select id="selectWeightBandById" resultType="com.bms.price.domain.LblWeightBand">
|
||||
select band_id as bandId, product_id as productId, version_id as versionId, fee_type as feeType,
|
||||
country_name as countryName, country_code as countryCode, zone_code as zoneCode, zone_name as zoneName,
|
||||
postcode_param as postcodeParam, weight_label as weightLabel, weight_min as weightMin, weight_max as weightMax,
|
||||
volume_text as volumeText, charge_name as chargeName, charge_mode as chargeMode, calc_param as calcParam,
|
||||
price, price_unit as priceUnit, extra_price as extraPrice, extra_price_unit as extraPriceUnit, sort_no as sortNo
|
||||
from prc_label_weight_band where band_id = #{bandId}
|
||||
</select>
|
||||
|
||||
<delete id="deleteWeightBandsByVersion">
|
||||
delete from prc_label_weight_band where version_id = #{versionId}
|
||||
</delete>
|
||||
|
||||
<select id="selectWeightBandsByVersion" resultType="com.bms.price.domain.LblWeightBand">
|
||||
select band_id as bandId, product_id as productId, version_id as versionId, fee_type as feeType,
|
||||
country_name as countryName, country_code as countryCode, zone_code as zoneCode, zone_name as zoneName,
|
||||
postcode_param as postcodeParam, weight_label as weightLabel, weight_min as weightMin, weight_max as weightMax,
|
||||
volume_text as volumeText, charge_name as chargeName, charge_mode as chargeMode, calc_param as calcParam,
|
||||
price, price_unit as priceUnit, extra_price as extraPrice, extra_price_unit as extraPriceUnit, sort_no as sortNo
|
||||
from prc_label_weight_band where version_id = #{versionId} order by sort_no, band_id
|
||||
</select>
|
||||
|
||||
<insert id="insertSurcharge" useGeneratedKeys="true" keyProperty="surchargeId">
|
||||
insert into prc_label_surcharge (
|
||||
product_id, version_id, fee_name, sc_id, zone_code, zone_name, charge_text, amount, currency, price_unit,
|
||||
fee_desc, period_text, effective_start, effective_end, charge_timing, calc_rule, calc_param, postcode_param,
|
||||
weight_min, weight_max, sort_no
|
||||
) values (
|
||||
#{productId}, #{versionId}, #{feeName}, #{scId}, #{zoneCode}, #{zoneName}, #{chargeText}, #{amount}, #{currency}, #{priceUnit},
|
||||
#{feeDesc}, #{periodText}, #{effectiveStart}, #{effectiveEnd}, #{chargeTiming}, ifnull(#{calcRule}, 'MANUAL'), #{calcParam}, #{postcodeParam},
|
||||
#{weightMin}, #{weightMax}, ifnull(#{sortNo}, 0)
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateSurcharge">
|
||||
update prc_label_surcharge
|
||||
set fee_name = ifnull(#{feeName}, fee_name),
|
||||
amount = #{amount},
|
||||
currency = #{currency},
|
||||
price_unit = #{priceUnit},
|
||||
charge_text = #{chargeText},
|
||||
fee_desc = #{feeDesc},
|
||||
calc_rule = ifnull(#{calcRule}, calc_rule),
|
||||
calc_param = #{calcParam},
|
||||
postcode_param = #{postcodeParam},
|
||||
zone_code = #{zoneCode},
|
||||
zone_name = #{zoneName}
|
||||
where surcharge_id = #{surchargeId}
|
||||
</update>
|
||||
|
||||
<select id="selectSurchargeById" resultType="com.bms.price.domain.LblSurcharge">
|
||||
select surcharge_id as surchargeId, product_id as productId, version_id as versionId, fee_name as feeName,
|
||||
sc_id as scId, zone_code as zoneCode, zone_name as zoneName, charge_text as chargeText, amount, currency,
|
||||
price_unit as priceUnit, fee_desc as feeDesc, period_text as periodText,
|
||||
effective_start as effectiveStart, effective_end as effectiveEnd, charge_timing as chargeTiming,
|
||||
calc_rule as calcRule, calc_param as calcParam, postcode_param as postcodeParam,
|
||||
weight_min as weightMin, weight_max as weightMax, sort_no as sortNo
|
||||
from prc_label_surcharge where surcharge_id = #{surchargeId}
|
||||
</select>
|
||||
|
||||
<delete id="deleteSurchargesByVersion">
|
||||
delete from prc_label_surcharge where version_id = #{versionId}
|
||||
</delete>
|
||||
|
||||
<select id="selectSurchargesByVersion" resultType="com.bms.price.domain.LblSurcharge">
|
||||
select surcharge_id as surchargeId, product_id as productId, version_id as versionId, fee_name as feeName, sc_id as scId,
|
||||
zone_code as zoneCode, zone_name as zoneName, charge_text as chargeText, amount, currency, price_unit as priceUnit,
|
||||
fee_desc as feeDesc, period_text as periodText, effective_start as effectiveStart, effective_end as effectiveEnd,
|
||||
charge_timing as chargeTiming, calc_rule as calcRule, calc_param as calcParam, postcode_param as postcodeParam,
|
||||
weight_min as weightMin, weight_max as weightMax, sort_no as sortNo
|
||||
from prc_label_surcharge where version_id = #{versionId} order by sort_no, surcharge_id
|
||||
</select>
|
||||
|
||||
<select id="selectVersionIdsBySetAndProducts" resultType="long">
|
||||
select version_id from prc_label_product_version
|
||||
where price_set_id = #{priceSetId}
|
||||
and product_id in
|
||||
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</select>
|
||||
|
||||
<delete id="deleteVersionsBySetAndProducts">
|
||||
delete from prc_label_product_version
|
||||
where price_set_id = #{priceSetId}
|
||||
and product_id in
|
||||
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
</delete>
|
||||
|
||||
<select id="selectProductsBySet" resultType="com.bms.price.domain.LblProduct">
|
||||
select p.product_id as productId, p.product_name as productName, p.dict_value as dictValue,
|
||||
v.version_id as versionId, v.version_no as versionNo, v.status as versionStatus,
|
||||
v.effective_start as versionStart, v.effective_end as versionEnd,
|
||||
p.country_scope as countryScope, p.charge_type as chargeType, p.currency, p.sheet_name as sheetName,
|
||||
p.source_file as sourceFile, p.file_path as filePath, p.weight_count as weightCount, p.surcharge_count as surchargeCount
|
||||
from prc_label_product_version v
|
||||
join prc_label_product p on p.product_id = v.product_id
|
||||
where v.price_set_id = #{priceSetId} and v.status in ('0', '1')
|
||||
order by p.product_name, v.version_id desc
|
||||
</select>
|
||||
|
||||
<select id="selectProductsBySetAndVersion" resultType="com.bms.price.domain.LblProduct">
|
||||
select p.product_id as productId, p.product_name as productName, p.dict_value as dictValue,
|
||||
v.version_id as versionId, v.version_no as versionNo, v.status as versionStatus,
|
||||
v.effective_start as versionStart, v.effective_end as versionEnd,
|
||||
p.country_scope as countryScope, p.charge_type as chargeType, p.currency, p.sheet_name as sheetName,
|
||||
p.source_file as sourceFile, p.file_path as filePath, p.weight_count as weightCount, p.surcharge_count as surchargeCount
|
||||
from prc_label_product_version v
|
||||
join prc_label_product p on p.product_id = v.product_id
|
||||
where v.price_set_id = #{priceSetId} and v.version_no = #{versionNo}
|
||||
order by p.product_name, v.version_id desc
|
||||
</select>
|
||||
|
||||
<update id="activateVersionRows">
|
||||
update prc_label_product_version
|
||||
set status = '1',
|
||||
effective_start = #{effectiveStart},
|
||||
updated_by = #{operator},
|
||||
update_time = sysdate()
|
||||
where price_set_id = #{priceSetId} and version_no = #{versionNo}
|
||||
</update>
|
||||
|
||||
<update id="expireOtherActive">
|
||||
update prc_label_product_version
|
||||
set status = '2',
|
||||
effective_end = #{endDate},
|
||||
updated_by = #{operator},
|
||||
update_time = sysdate()
|
||||
where price_set_id = #{priceSetId}
|
||||
and version_no <> #{versionNo}
|
||||
and status = '1'
|
||||
</update>
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user