新增路线产品

This commit is contained in:
liuminghui
2026-11-06 13:39:26 +08:00
parent b4da248815
commit 6f342d424f
32 changed files with 1850 additions and 785 deletions
@@ -3,6 +3,7 @@ package com.bms.masterdata.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.bms.common.annotation.Excel;
import lombok.Data;
/**
@@ -14,72 +15,136 @@ import lombok.Data;
@Data
public class LineProduct
{
/** 源主键 ID */
/** 源主键 ID。列表、表单和导出都不展示。 */
private String id;
/** 产品(线路)编码 */
@Excel(name = "产品编码")
private String lineCode;
/** 产品(线路)名称 */
@Excel(name = "产品名称")
private String lineName;
@Excel(name = "产品类型")
private String productType;
@Excel(name = "线路类型")
private String lineType;
/** 是否可收货(1是 0否) */
@Excel(name = "是否可收货", readConverterExp = "1=是,0=否")
private String blCargo;
@Excel(name = "货物名称")
private String cargoName;
@Excel(name = "仓储服务名称")
private String productStorage;
@Excel(name = "头程服务名称")
private String productFirst;
@Excel(name = "清关服务名称")
private String productClearance;
@Excel(name = "仓储服务编码")
private String productStorageCode;
@Excel(name = "头程服务编码")
private String productFirstCode;
@Excel(name = "清关服务编码")
private String productClearanceCode;
/** 是否启用(1启用 0停用) */
@Excel(name = "是否启用", readConverterExp = "1=启用,0=停用")
private String blOpen;
/** 是否分段(1是 0否) */
@Excel(name = "是否分段", readConverterExp = "1=是,0=否")
private String blSection;
@Excel(name = "派送商编码")
private String dispCode;
@Excel(name = "派送商名称")
private String dispName;
@Excel(name = "费用类别")
private String feeClass;
@Excel(name = "发货人ID")
private String shipperId;
/** 发货规则 */
@Excel(name = "发货规则")
private String shipmentRule;
/** 限重(kg) */
@Excel(name = "限重(kg)")
private BigDecimal weight;
@Excel(name = "货物类型")
private String cargoType;
@Excel(name = "产品分类")
private String productCategory;
/** 体积基数(抛货系数) */
@Excel(name = "体积基数")
private BigDecimal volumeBasis;
@Excel(name = "重量类型")
private String weightType;
@Excel(name = "服务类型")
private String serviceType;
@Excel(name = "发货类型")
private String shipmentType;
@Excel(name = "税费金额")
private BigDecimal taxAmount;
@Excel(name = "税费币种")
private String taxCurrency;
@Excel(name = "电池类型")
private String batteryType;
@Excel(name = "备注")
private String remark;
/** 源创建人,页面不展示 */
private String createName;
/** 源创建人工号,页面不展示 */
private String createManCode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "创建时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createDate;
@Excel(name = "修改人")
private String modifier;
@Excel(name = "修改人工号")
private String modifierCode;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "修改时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date modifyDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "GMT创建时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date gmtCreate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "GMT修改时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date gmtModified;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Excel(name = "同步时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date syncTime;
}
@@ -0,0 +1,26 @@
package com.bms.masterdata.mapper;
import java.util.List;
import com.bms.masterdata.domain.LineProduct;
/**
* 路线产品 srv_line_product
*/
public interface LineProductMapper
{
List<LineProduct> selectLineProductList(LineProduct query);
LineProduct selectLineProductById(String id);
LineProduct selectLineProductByCode(String lineCode);
int countProductChannelByProductId(String productId);
int insertLineProduct(LineProduct product);
int updateLineProduct(LineProduct product);
int updateLineProductStatus(LineProduct product);
int deleteLineProductByIds(String[] ids);
}
@@ -0,0 +1,22 @@
package com.bms.masterdata.service;
import java.util.List;
import com.bms.masterdata.domain.LineProduct;
/**
* 路线产品
*/
public interface ILineProductService
{
List<LineProduct> selectLineProductList(LineProduct query);
LineProduct selectLineProductById(String id);
int insertLineProduct(LineProduct product);
int updateLineProduct(LineProduct product);
int updateLineProductStatus(LineProduct product);
int deleteLineProductByIds(String[] ids);
}
@@ -0,0 +1,228 @@
package com.bms.masterdata.service.impl;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.StringUtils;
import com.bms.common.utils.uuid.IdUtils;
import com.bms.masterdata.domain.LineProduct;
import com.bms.masterdata.mapper.LineProductMapper;
import com.bms.masterdata.service.ILineProductService;
/**
* 路线产品
*/
@Service
public class LineProductServiceImpl implements ILineProductService
{
@Autowired
private LineProductMapper lineProductMapper;
@Override
public List<LineProduct> selectLineProductList(LineProduct query)
{
return lineProductMapper.selectLineProductList(query);
}
@Override
public LineProduct selectLineProductById(String id)
{
return lineProductMapper.selectLineProductById(id);
}
@Override
public int insertLineProduct(LineProduct product)
{
prepare(product, true);
product.setId(IdUtils.fastSimpleUUID());
assertCodeUnique(product);
return lineProductMapper.insertLineProduct(product);
}
@Override
public int updateLineProduct(LineProduct product)
{
if (product == null || StringUtils.isEmpty(product.getId()))
{
throw new ServiceException("路线产品不存在");
}
LineProduct existing = lineProductMapper.selectLineProductById(product.getId());
if (existing == null)
{
throw new ServiceException("路线产品不存在或已被删除");
}
prepare(product, false);
assertCodeUnique(product);
return lineProductMapper.updateLineProduct(product);
}
@Override
public int updateLineProductStatus(LineProduct product)
{
if (product == null || StringUtils.isEmpty(product.getId()))
{
throw new ServiceException("路线产品不存在");
}
if (!"0".equals(product.getBlOpen()) && !"1".equals(product.getBlOpen()))
{
throw new ServiceException("启用状态只能是启用或停用");
}
LineProduct existing = lineProductMapper.selectLineProductById(product.getId());
if (existing == null)
{
throw new ServiceException("路线产品不存在或已被删除");
}
return lineProductMapper.updateLineProductStatus(product);
}
@Override
public int deleteLineProductByIds(String[] ids)
{
if (ids == null || ids.length == 0)
{
throw new ServiceException("请选择要删除的路线产品");
}
for (String id : ids)
{
if (StringUtils.isEmpty(id))
{
continue;
}
LineProduct existing = lineProductMapper.selectLineProductById(id);
if (existing == null)
{
continue;
}
if (lineProductMapper.countProductChannelByProductId(id) > 0)
{
throw new ServiceException("路线产品「" + existing.getLineName() + "」已被渠道关系引用,不能删除,请改为停用");
}
}
return lineProductMapper.deleteLineProductByIds(ids);
}
private void prepare(LineProduct product, boolean creating)
{
if (product == null)
{
throw new ServiceException("路线产品不能为空");
}
product.setLineCode(StringUtils.trim(product.getLineCode()));
product.setLineName(StringUtils.trim(product.getLineName()));
product.setProductType(limit(product.getProductType(), 20, "产品类型"));
product.setLineType(limit(product.getLineType(), 50, "线路类型"));
product.setCargoName(limit(product.getCargoName(), 100, "货物名称"));
product.setProductStorage(trimToNull(product.getProductStorage()));
product.setProductFirst(trimToNull(product.getProductFirst()));
product.setProductClearance(trimToNull(product.getProductClearance()));
product.setProductStorageCode(limit(product.getProductStorageCode(), 1000, "仓储服务编码"));
product.setProductFirstCode(limit(product.getProductFirstCode(), 1000, "头程服务编码"));
product.setProductClearanceCode(limit(product.getProductClearanceCode(), 1000, "清关服务编码"));
product.setDispCode(limit(product.getDispCode(), 60, "派送商编码"));
product.setDispName(limit(product.getDispName(), 100, "派送商名称"));
product.setFeeClass(limit(product.getFeeClass(), 100, "费用类别"));
product.setShipperId(limit(product.getShipperId(), 64, "发货人ID"));
product.setShipmentRule(flagOrNull(product.getShipmentRule(), "发货规则"));
product.setCargoType(limit(product.getCargoType(), 64, "货物类型"));
product.setProductCategory(limit(product.getProductCategory(), 100, "产品分类"));
product.setWeightType(flagOrNull(product.getWeightType(), "重量类型"));
product.setServiceType(flagOrNull(product.getServiceType(), "服务类型"));
product.setShipmentType(limit(product.getShipmentType(), 32, "发货类型"));
product.setTaxCurrency(limit(product.getTaxCurrency(), 16, "税费币种"));
product.setBatteryType(limit(product.getBatteryType(), 64, "电池类型"));
product.setRemark(limit(product.getRemark(), 500, "备注"));
product.setBlCargo(yesNo(product.getBlCargo(), "是否可收货"));
product.setBlSection(yesNo(product.getBlSection(), "是否分段"));
if (StringUtils.isEmpty(product.getLineCode()))
{
throw new ServiceException("产品编码不能为空");
}
if (product.getLineCode().length() > 64)
{
throw new ServiceException("产品编码不能超过64个字符");
}
if (StringUtils.isEmpty(product.getLineName()))
{
throw new ServiceException("产品名称不能为空");
}
if (product.getLineName().length() > 200)
{
throw new ServiceException("产品名称不能超过200个字符");
}
if (StringUtils.isEmpty(product.getBlOpen()))
{
if (creating)
{
product.setBlOpen("1");
}
else
{
throw new ServiceException("请选择启用状态");
}
}
if (!"0".equals(product.getBlOpen()) && !"1".equals(product.getBlOpen()))
{
throw new ServiceException("启用状态只能是启用或停用");
}
if (product.getWeight() != null && product.getWeight().compareTo(BigDecimal.ZERO) < 0)
{
throw new ServiceException("限重不能为负数");
}
if (product.getVolumeBasis() != null && product.getVolumeBasis().compareTo(BigDecimal.ZERO) < 0)
{
throw new ServiceException("体积基数不能为负数");
}
if (product.getTaxAmount() != null && product.getTaxAmount().compareTo(BigDecimal.ZERO) < 0)
{
throw new ServiceException("税费金额不能为负数");
}
}
private void assertCodeUnique(LineProduct product)
{
LineProduct same = lineProductMapper.selectLineProductByCode(product.getLineCode());
if (same != null && !StringUtils.equals(same.getId(), product.getId()))
{
throw new ServiceException("产品编码「" + product.getLineCode() + "」已存在");
}
}
private String yesNo(String value, String label)
{
String text = trimToNull(value);
if (text != null && !"0".equals(text) && !"1".equals(text))
{
throw new ServiceException(label + "只能是是或否");
}
return text;
}
/** 单字符标记:空则清空,有值时只能是一个字符。 */
private String flagOrNull(String value, String label)
{
String text = trimToNull(value);
if (text != null && text.length() > 1)
{
throw new ServiceException(label + "只能填写一个字符");
}
return text;
}
private String limit(String value, int max, String label)
{
String text = trimToNull(value);
if (text != null && text.length() > max)
{
throw new ServiceException(label + "不能超过" + max + "个字符");
}
return text;
}
private String trimToNull(String value)
{
String text = StringUtils.trim(value);
return StringUtils.isEmpty(text) ? null : text;
}
}
@@ -1,18 +0,0 @@
package com.bms.price.domain;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 一个应付产品上的附加费开关。多个产品各自一组,互不影响。
*/
@Data
public class PayFeeRuleGroup
{
private Long productId;
private String productName;
private List<PayProductFeeRule> rules = new ArrayList<>();
}
@@ -0,0 +1,62 @@
package com.bms.price.domain;
/**
* 应付价卡解析进度。给导入弹框轮询,不入库。
*/
public class PayParseProgress
{
public static final String PENDING = "PENDING";
public static final String RUNNING = "RUNNING";
public static final String SUCCESS = "SUCCESS";
public static final String FAIL = "FAIL";
private int percent;
private String message;
private String status;
public PayParseProgress()
{
}
public PayParseProgress(int percent, String message, String status)
{
this.percent = percent;
this.message = message;
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 String getStatus()
{
return status;
}
public void setStatus(String status)
{
this.status = status;
}
}
@@ -1,47 +0,0 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import com.bms.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应付产品附加费开关。挂在根产品上,价格仍在价卡版本的附加费行里。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PayProductFeeRule extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long ruleId;
/** 根产品编号 */
private Long productId;
/** 附加费名称,用来到价卡版本里取价 */
private String feeName;
/** 1 参与计算,0 不参与 */
private String enabled;
/** SIZE 尺寸,VOLUME 体积,WEIGHT 重量,POSTCODE 邮编,ALWAYS 每票,RETURN 退件 */
private String triggerType;
private BigDecimal sideMaxCm;
private BigDecimal sumMaxCm;
private BigDecimal volumeMaxCm3;
private BigDecimal minLengthCm;
private BigDecimal minWidthCm;
private BigDecimal minHeightCm;
private BigDecimal weightMaxKg;
/** ANY 任一条超出就收,ALL 写了的条件都超出才收 */
private String joinMode;
}
@@ -51,7 +51,7 @@ public class PaySurcharge
/** 固定收取、产生时收取、旺季期间 */
private String chargeTiming;
/** ALWAYS、POSTCODE、WEIGHT、SIZE、RETURN、MANUAL */
/** ALWAYS、POSTCODE、WEIGHT、SIZE、RETURN、EXTRA_KG、MANUAL */
private String calcRule;
/** 邮编列表、重量阈值或尺寸表达式 */
@@ -1,19 +0,0 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PayProductFeeRule;
/**
* 应付产品附加费开关
*/
public interface PayProductFeeRuleMapper
{
List<PayProductFeeRule> selectByProductId(Long productId);
int deleteByProductId(Long productId);
int deleteByProductIds(@Param("productIds") List<Long> productIds);
int insertBatch(@Param("list") List<PayProductFeeRule> list);
}
@@ -68,8 +68,14 @@ public final class PayCostCalculator
PayWeightBand band = matchBand(bands, weight);
if (band == null)
{
result.setMessage(outOfRange(product, bands, weight));
return result;
PayWeightBand top = highestBand(bands);
if (top == null || weight.compareTo(top.getWeightMax()) <= 0
|| !hasExtraKg(product, zoneName, top.getWeightMax(), top.getSectionName()))
{
result.setMessage(outOfRange(product, bands, weight));
return result;
}
band = top;
}
String currency = currencyOf(product, band);
BigDecimal freight = bandAmount(band, weight);
@@ -81,7 +87,8 @@ public final class PayCostCalculator
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, result);
addSurcharges(product, weight, length, width, height, postcode, zoneName, bizDate, orderStatus, freight, currency,
band.getSectionName(), result);
BigDecimal total = BigDecimal.ZERO;
for (PayCostLine fee : result.getFees())
{
@@ -111,7 +118,7 @@ public final class PayCostCalculator
private static void addSurcharges(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, String zoneName, Date bizDate, Integer orderStatus, BigDecimal freight, String currency,
PayOrderCostResult result)
String freightSection, PayOrderCostResult result)
{
if (product.getSurcharges() == null)
{
@@ -171,6 +178,13 @@ public final class PayCostCalculator
addFlat(result.getFees(), fee, currency, "订单已退件");
}
}
else if (PaySurchargeRules.EXTRA_KG.equals(rule))
{
if (sameSection(fee.getSectionName(), freightSection))
{
addExtraKg(result.getFees(), fee, weight, currency);
}
}
else if (PaySurchargeRules.ALWAYS.equals(rule))
{
PayCostLine matched = matchedFee(fee, freight, currency, blank(fee.getChargeText()));
@@ -214,6 +228,97 @@ public final class PayCostCalculator
return kept;
}
private static void addExtraKg(List<PayCostLine> fees, PaySurcharge fee, BigDecimal weight, String currency)
{
if (fee.getAmount() == null || weight == null)
{
return;
}
BigDecimal threshold;
try
{
threshold = new BigDecimal(fee.getCalcParam() == null ? "" : fee.getCalcParam().trim());
}
catch (NumberFormatException ex)
{
return;
}
if (weight.compareTo(threshold) <= 0)
{
return;
}
String feeCurrency = normalize(fee.getCurrency());
if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency))
{
return;
}
BigDecimal over = weight.subtract(threshold);
BigDecimal amount = fee.getAmount().multiply(over).setScale(2, RoundingMode.HALF_UP);
long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId();
String remark = "超出 " + plain(over) + "kg × " + plain(fee.getAmount()) + " " + blank(fee.getPriceUnit());
fees.add(line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark));
}
/** 同一地区有多张服务表时,续重只跟本次首重所在的那一张表。 */
private static boolean sameSection(String feeSection, String freightSection)
{
if (feeSection == null || feeSection.isBlank() || freightSection == null || freightSection.isBlank())
{
return true;
}
return feeSection.trim().equals(freightSection.trim());
}
/** 重量超过最高档,且该地区、该服务表有每公斤续重时,基础运费先用最高档。 */
private static boolean hasExtraKg(PayProduct product, String zoneName, BigDecimal threshold, String section)
{
if (product.getSurcharges() == null || threshold == null)
{
return false;
}
for (PaySurcharge fee : product.getSurcharges())
{
if (fee == null || !PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()) || fee.getAmount() == null)
{
continue;
}
if (zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank()
&& !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
{
continue;
}
if (!sameSection(fee.getSectionName(), section))
{
continue;
}
try
{
if (fee.getCalcParam() != null && new BigDecimal(fee.getCalcParam().trim()).compareTo(threshold) == 0)
{
return true;
}
}
catch (NumberFormatException ex)
{
continue;
}
}
return false;
}
private static PayWeightBand highestBand(List<PayWeightBand> bands)
{
PayWeightBand top = null;
for (PayWeightBand band : bands)
{
if (top == null || band.getWeightMax().compareTo(top.getWeightMax()) > 0)
{
top = band;
}
}
return top;
}
private static void addFlat(List<PayCostLine> fees, PaySurcharge fee, String currency, String remark)
{
PayCostLine line = matchedFee(fee, null, currency, remark);
@@ -0,0 +1,10 @@
package com.bms.price.pay;
/**
* 应付价卡解析进度。只用于界面展示,不参与识别。
*/
@FunctionalInterface
public interface PayParseProgressListener
{
void onProgress(int percent, String message);
}
@@ -26,6 +26,9 @@ public final class PaySurchargeRules
public static final String MANUAL = "MANUAL";
/** 超过重量段上限后,按超出的公斤数乘单价 */
public static final String EXTRA_KG = "EXTRA_KG";
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})");
@@ -112,6 +112,15 @@ public class PayWorkbookParser
* 单元格里用分号隔开的国家、分区或费用名称,拆成多条。
*/
public PayPreview parseMatched(byte[] bytes, String filename, String selected, Collection<String> dictValues)
{
return parseMatched(bytes, filename, selected, dictValues, null);
}
/**
* 与 {@link #parseMatched(byte[], String, String, Collection)} 相同。listener 只接收进度,不改变识别结果。
*/
public PayPreview parseMatched(byte[] bytes, String filename, String selected, Collection<String> dictValues,
PayParseProgressListener listener)
{
if (bytes == null || bytes.length == 0)
{
@@ -128,6 +137,7 @@ public class PayWorkbookParser
}
String safeName = filename == null ? "价卡.xlsx" : filename;
String wanted = norm(selected);
report(listener, 8, "正在打开工作簿");
try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes)))
{
if (workbook.getNumberOfSheets() == 0)
@@ -141,29 +151,43 @@ public class PayWorkbookParser
preview.setSupplierName(meta.supplier);
preview.setEffectiveDate(PayTexts.toDate(meta.effective));
List<String> skipped = new ArrayList<>();
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
int sheetCount = workbook.getNumberOfSheets();
for (int i = 0; i < sheetCount; i++)
{
Sheet sheet = workbook.getSheetAt(i);
String sheetName = sheet.getSheetName() == null ? "" : sheet.getSheetName().trim();
int start = 12 + i * 78 / sheetCount;
int end = 12 + (i + 1) * 78 / sheetCount;
String dictValue = dict.get(norm(sheetName));
boolean holdForFallback = sheetCount == 1 && !wanted.isEmpty();
if (dictValue == null)
{
skipped.add(sheetName);
if (!holdForFallback)
{
report(listener, end, "已跳过「" + sheetName + "」");
}
continue;
}
if (!wanted.isEmpty() && !wanted.equals(norm(dictValue)) && !wanted.equals(norm(sheetName)))
{
skipped.add(sheetName);
if (!holdForFallback)
{
report(listener, end, "已跳过「" + sheetName + "」");
}
continue;
}
preview.getProducts().add(readMatchedSheet(sheet, sheetName, dictValue, meta, preview));
report(listener, start, "正在解析「" + sheetName + "」");
preview.getProducts().add(readMatchedSheet(sheet, sheetName, dictValue, meta, preview, listener, start, end));
}
if (preview.getProducts().isEmpty() && !wanted.isEmpty() && workbook.getNumberOfSheets() == 1)
if (preview.getProducts().isEmpty() && !wanted.isEmpty() && sheetCount == 1)
{
Sheet sheet = workbook.getSheetAt(0);
String sheetName = sheet.getSheetName() == null ? "" : sheet.getSheetName().trim();
String dictValue = dict.containsValue(selected.trim()) ? selected.trim() : labelOf(dict, wanted);
preview.getProducts().add(readMatchedSheet(sheet, sheetName, dictValue, meta, preview));
report(listener, 12, "正在解析「" + sheetName + "」");
preview.getProducts().add(readMatchedSheet(sheet, sheetName, dictValue, meta, preview, listener, 12, 90));
skipped.clear();
}
if (preview.getProducts().isEmpty())
@@ -172,8 +196,10 @@ public class PayWorkbookParser
? "没有工作表能对上应付产品字典"
: "文件里没有「" + selected.trim() + "」这张工作表");
}
report(listener, 92, "正在整理附加费");
attachWorkbookRates(workbook, preview.getProducts());
preview.setSkippedSheets(skipped);
report(listener, 96, "工作表解析完成");
return preview;
}
catch (ServiceException ex)
@@ -186,7 +212,24 @@ public class PayWorkbookParser
}
}
private PayProduct readMatchedSheet(Sheet sheet, String sheetName, String dictValue, FileMeta meta, PayPreview preview)
private static void report(PayParseProgressListener listener, int percent, String message)
{
if (listener == null)
{
return;
}
try
{
listener.onProgress(percent, message);
}
catch (RuntimeException ignored)
{
// 进度写失败不影响价卡识别
}
}
private PayProduct readMatchedSheet(Sheet sheet, String sheetName, String dictValue, FileMeta meta, PayPreview preview,
PayParseProgressListener listener, int start, int end)
{
PayProduct product = new PayProduct();
product.setProductName(dictValue);
@@ -199,11 +242,30 @@ public class PayWorkbookParser
product.setWeightBands(new ArrayList<>());
product.setSurcharges(new ArrayList<>());
product.setExtraLines(new ArrayList<>());
fillSheet(ExcelGrid.read(sheet), product);
fillSheet(readGrid(sheet, listener, start, end, sheetName), product);
expandSeparated(product);
report(listener, end, "已解析「" + sheetName + "」");
return product;
}
/** 读取这一张表。前半段是统计列宽,后半段是读取单元格。 */
private ExcelGrid readGrid(Sheet sheet, PayParseProgressListener listener, int start, int end, String sheetName)
{
int span = Math.max(1, end - start);
int scanSpan = Math.max(1, span * 2 / 5);
return ExcelGrid.read(sheet, ExcelGrid.MAX_ROWS, (phase, row, total) -> {
if (total < 1)
{
return;
}
int done = Math.min(row + 1, total);
int percent = phase == 0
? start + scanSpan * done / total
: start + scanSpan + (span - scanSpan) * done / total;
report(listener, Math.min(end, percent), "正在解析「" + sheetName + "」");
});
}
/** 分号拆开的是多条数据,不是一句话。句子太长时保持原样。 */
private void expandSeparated(PayProduct product)
{
@@ -1848,7 +1910,8 @@ public class PayWorkbookParser
/**
* 岛屿价卡一张表有两级表头:上面是群岛,下面是具体岛屿。
* Up to 1 kg 是首重,Additional Kg 是续重,Cubic Capacity 是体积重系数,不是运费。
* Up to 1 kg 是首重,记入重量段。Additional Kg 是超出首重后的每公斤加收,记入附加费。
* Cubic Capacity 是体积重系数,不是运费。
*/
private int readInsularRates(ExcelGrid grid, int header, PayProduct product)
{
@@ -1869,6 +1932,7 @@ public class PayWorkbookParser
zoneCols.add(col);
}
}
BigDecimal firstLimit = null;
int row = header + 1;
while (row < grid.rows() && !grid.blankRow(row))
{
@@ -1887,6 +1951,10 @@ public class PayWorkbookParser
row++;
continue;
}
if (firstKg)
{
firstLimit = firstKgLimit(label);
}
for (int col : zoneCols)
{
String child = grid.text(header, col).replace('\n', ' ').trim();
@@ -1902,23 +1970,20 @@ public class PayWorkbookParser
{
continue;
}
if (extraKg)
{
addExtraKg(product, section, zone, label, price, firstLimit == null ? BigDecimal.ONE : firstLimit);
continue;
}
PayWeightBand band = new PayWeightBand();
band.setSectionName(section);
band.setZoneName(zone);
band.setWeightLabel(label);
band.setPrice(price);
if (extraKg)
{
band.setChargeName("每公斤");
band.setPriceUnit("EUR/公斤");
}
else
{
band.setWeightMin(BigDecimal.ZERO);
band.setWeightMax(firstKgLimit(label));
band.setChargeName("每件");
band.setPriceUnit("EUR/件");
}
band.setWeightMin(BigDecimal.ZERO);
band.setWeightMax(firstKgLimit(label));
band.setChargeName("每件");
band.setPriceUnit("EUR/件");
band.setSortNo(product.getWeightBands().size() + 1);
product.getWeightBands().add(band);
}
@@ -1928,7 +1993,8 @@ public class PayWorkbookParser
}
/**
* 西班牙 CTT:上面一行是大陆或群岛,下一行是密度和岛屿。1kg 到 5kg 是该重量的票价,Kg.ad. 是超出后的续重。
* 西班牙 CTT:上面一行是大陆或群岛,下一行是密度和岛屿。
* 1kg 到 5kg 是该重量的票价,记入重量段。Kg.ad. 是超出最高档后的每公斤加收,记入附加费。
*/
private int readKgStepTable(ExcelGrid grid, int header, PayProduct product)
{
@@ -1969,25 +2035,25 @@ public class PayWorkbookParser
}
String child = grid.text(header, col).replace('\n', ' ').trim();
String parent = groupLabel(grid, groupRow, col, service);
PayWeightBand band = new PayWeightBand();
band.setSectionName(service);
band.setZoneName(parent.isBlank() ? child : parent + " / " + child);
band.setWeightLabel(label);
band.setPrice(price);
String zone = parent.isBlank() ? child : parent + " / " + child;
if (extra)
{
band.setChargeName("每公斤");
band.setPriceUnit("EUR/公斤");
addExtraKg(product, service, zone, label, price, previousMax);
}
else
{
PayWeightBand band = new PayWeightBand();
band.setSectionName(service);
band.setZoneName(zone);
band.setWeightLabel(label);
band.setPrice(price);
band.setWeightMin(minKg);
band.setWeightMax(maxKg);
band.setChargeName("每件");
band.setPriceUnit("EUR/件");
band.setSortNo(product.getWeightBands().size() + 1);
product.getWeightBands().add(band);
}
band.setSortNo(product.getWeightBands().size() + 1);
product.getWeightBands().add(band);
}
if (!extra)
{
@@ -1998,6 +2064,26 @@ public class PayWorkbookParser
return row;
}
/** Kg.ad. 按地区各记一条每公斤续重,不进入重量段。 */
private static void addExtraKg(PayProduct product, String section, String zone, String label, BigDecimal price, BigDecimal threshold)
{
BigDecimal from = threshold == null ? BigDecimal.ZERO : threshold;
PaySurcharge fee = new PaySurcharge();
fee.setSectionName(section);
fee.setFeeName("每公斤续重");
fee.setZoneName(zone);
fee.setChargeText(label + " " + price.stripTrailingZeros().toPlainString() + " EUR/KG");
fee.setAmount(price);
fee.setCurrency("EUR");
fee.setPriceUnit("EUR/KG");
fee.setFeeDesc("超过" + from.stripTrailingZeros().toPlainString() + "kg后,按超出的重量每公斤加收");
fee.setChargeTiming("产生时收取");
fee.setCalcRule(PaySurchargeRules.EXTRA_KG);
fee.setCalcParam(from.stripTrailingZeros().toPlainString());
fee.setSortNo(product.getSurcharges().size() + 1);
product.getSurcharges().add(fee);
}
private static boolean isKgStepHeader(ExcelGrid grid, int row)
{
int names = 0;
@@ -2260,6 +2346,16 @@ public class PayWorkbookParser
itemFee = true;
}
}
if (product.getSurcharges() != null)
{
for (PaySurcharge fee : product.getSurcharges())
{
if (fee != null && PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()))
{
extraKg = true;
}
}
}
if (extraKg)
{
return "续重计费";
@@ -2297,6 +2393,24 @@ public class PayWorkbookParser
high = band.getPrice();
}
}
if (product.getSurcharges() != null)
{
for (PaySurcharge fee : product.getSurcharges())
{
if (fee == null || fee.getAmount() == null || !PaySurchargeRules.EXTRA_KG.equals(fee.getCalcRule()))
{
continue;
}
if (low == null || fee.getAmount().compareTo(low) < 0)
{
low = fee.getAmount();
}
if (high == null || fee.getAmount().compareTo(high) > 0)
{
high = fee.getAmount();
}
}
}
if (low == null)
{
return null;
@@ -3,7 +3,7 @@ package com.bms.price.service;
import java.io.OutputStream;
import java.util.List;
import java.util.Date;
import com.bms.price.domain.PayFeeRuleGroup;
import com.bms.price.domain.PayParseProgress;
import com.bms.price.domain.PayPreview;
import com.bms.price.domain.PayPriceCompare;
import com.bms.price.domain.PayProduct;
@@ -21,6 +21,14 @@ public interface IPayProductService
*/
PayPreview preview(byte[] bytes, String filename, String dictValue);
/**
* 与 {@link #preview(byte[], String, String)} 相同。progressId 有值时写入解析进度,供页面轮询。
*/
PayPreview preview(byte[] bytes, String filename, String dictValue, String progressId);
/** 读取一次导入的解析进度。没有对应任务时返回等待中。 */
PayParseProgress parseProgress(String progressId);
/** 把预览里勾选的产品写入数据库。同名字典产品再次导入时另存一版,旧价格留在有效期表里 */
String save(PaySaveCommand command);
@@ -75,10 +83,4 @@ public interface IPayProductService
void writeSource(Long productId, OutputStream output);
int deletePayProducts(Long[] productIds);
/** 按根产品编号带出附加费开关。没有配置过的产品返回空列表 */
List<PayFeeRuleGroup> listFeeRules(List<Long> productIds);
/** 按产品整组替换附加费开关。每个产品一份,互不影响 */
void saveFeeRules(List<PayFeeRuleGroup> products);
}
@@ -3,13 +3,11 @@ package com.bms.price.service.impl;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -28,8 +26,7 @@ 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.PayExtraLine;
import com.bms.price.domain.PayFeeRuleGroup;
import com.bms.price.domain.PayProductFeeRule;
import com.bms.price.domain.PayParseProgress;
import com.bms.price.domain.PayPreview;
import com.bms.price.domain.PayPriceCompare;
import com.bms.price.domain.PayPriceDiff;
@@ -39,9 +36,9 @@ import com.bms.price.domain.PaySaveCommand;
import com.bms.price.domain.PaySurcharge;
import com.bms.price.domain.PayWeightBand;
import com.bms.price.mapper.ChannelProductMapMapper;
import com.bms.price.mapper.PayProductFeeRuleMapper;
import com.bms.price.mapper.PayProductMapper;
import com.bms.price.mapper.PayProductTermMapper;
import com.bms.price.pay.PayParseProgressListener;
import com.bms.price.pay.PayWorkbookParser;
import com.bms.price.service.IPayProductService;
import com.bms.system.service.ISysDictTypeService;
@@ -69,11 +66,6 @@ public class PayProductServiceImpl implements IPayProductService
@Autowired
private PayProductTermMapper payProductTermMapper;
@Autowired
private PayProductFeeRuleMapper payProductFeeRuleMapper;
private static final Set<String> FEE_TRIGGERS = Set.of("SIZE", "VOLUME", "WEIGHT", "POSTCODE", "ALWAYS", "RETURN");
@Autowired
private ISysDictTypeService dictTypeService;
@@ -85,23 +77,110 @@ public class PayProductServiceImpl implements IPayProductService
/** 预览时已经传到 OSS 的文件。保存不再只靠前端把地址传回来。 */
private final Map<String, UploadedSource> uploadedSources = new ConcurrentHashMap<>();
/** 导入弹框轮询的解析进度。只留在本机内存,过期后丢掉。 */
private final Map<String, ParseSlot> parseProgress = new ConcurrentHashMap<>();
private static final long PARSE_PROGRESS_KEEP_MS = 15L * 60 * 1000;
private static final long SOURCE_REMEMBER_MS = 6L * 60 * 60 * 1000;
@Override
public PayPreview preview(byte[] bytes, String filename, String dictValue)
{
PayPreview preview = parser.parseMatched(bytes, filename, dictValue, loadDictValues());
String stored = storeSource(bytes, filename);
rememberSource(preview.getFileMd5(), stored);
preview.setFilePath(stored);
for (PayProduct product : preview.getProducts())
return preview(bytes, filename, dictValue, null);
}
@Override
public PayPreview preview(byte[] bytes, String filename, String dictValue, String progressId)
{
String progressKey = progressKey(progressId);
PayParseProgressListener listener = progressKey == null ? null : (percent, message) ->
writeProgress(progressKey, percent, message, PayParseProgress.RUNNING);
if (progressKey != null)
{
product.setFilePath(stored);
Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue());
product.setDuplicated(existsId != null);
product.setExistsId(existsId);
writeProgress(progressKey, 2, "正在准备解析", PayParseProgress.RUNNING);
}
return preview;
try
{
PayPreview preview = parser.parseMatched(bytes, filename, dictValue, loadDictValues(), listener);
writeProgress(progressKey, 98, "正在核对已有产品", PayParseProgress.RUNNING);
String stored = storeSource(bytes, filename);
rememberSource(preview.getFileMd5(), stored);
preview.setFilePath(stored);
for (PayProduct product : preview.getProducts())
{
product.setFilePath(stored);
Long existsId = isBlank(product.getDictValue()) ? null : payProductMapper.selectTemplateId(product.getDictValue());
product.setDuplicated(existsId != null);
product.setExistsId(existsId);
}
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;
}
private void writeProgress(String key, int percent, String message, String status)
{
if (key == null)
{
return;
}
int safePercent = Math.max(0, Math.min(100, percent));
ParseSlot current = parseProgress.get(key);
if (current != null && safePercent < current.progress.getPercent()
&& !PayParseProgress.FAIL.equals(status))
{
return;
}
if (current != null && safePercent == current.progress.getPercent()
&& message.equals(current.progress.getMessage())
&& status.equals(current.progress.getStatus()))
{
return;
}
parseProgress.put(key, new ParseSlot(new PayParseProgress(safePercent, message, status), System.currentTimeMillis()));
if (parseProgress.size() > 200)
{
long now = System.currentTimeMillis();
parseProgress.entrySet().removeIf(entry -> now - entry.getValue().at > PARSE_PROGRESS_KEEP_MS);
}
}
/** 只接受页面生成的短编号,避免把任意字符串当成缓存键。 */
private static String progressKey(String progressId)
{
if (progressId == null)
{
return null;
}
String key = progressId.trim();
if (key.length() < 8 || key.length() > 64 || !key.matches("[A-Za-z0-9-]+"))
{
return null;
}
return key;
}
@Override
@@ -680,7 +759,6 @@ public class PayProductServiceImpl implements IPayProductService
return 0;
}
payProductTermMapper.deleteByProductIds(ids);
payProductFeeRuleMapper.deleteByProductIds(ids);
payProductMapper.deleteWeightBands(ids);
payProductMapper.deleteSurcharges(ids);
payProductMapper.deleteExtraLines(ids);
@@ -1247,176 +1325,6 @@ public class PayProductServiceImpl implements IPayProductService
return rows == null ? 0 : rows.size();
}
@Override
public List<PayFeeRuleGroup> listFeeRules(List<Long> productIds)
{
List<PayFeeRuleGroup> groups = new ArrayList<>();
if (productIds == null)
{
return groups;
}
LinkedHashSet<Long> ordered = new LinkedHashSet<>();
for (Long productId : productIds)
{
if (productId != null)
{
ordered.add(productId);
}
}
for (Long productId : ordered)
{
PayProduct root = requireRoot(productId);
PayFeeRuleGroup group = new PayFeeRuleGroup();
group.setProductId(root.getProductId());
group.setProductName(root.getProductName());
List<PayProductFeeRule> rules = payProductFeeRuleMapper.selectByProductId(root.getProductId());
group.setRules(rules == null ? new ArrayList<>() : rules);
groups.add(group);
}
return groups;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void saveFeeRules(List<PayFeeRuleGroup> products)
{
if (products == null || products.isEmpty())
{
throw new ServiceException("请选择应付产品");
}
String operator = SecurityUtils.getUsername();
Date now = DateUtils.getNowDate();
for (PayFeeRuleGroup group : products)
{
if (group == null || group.getProductId() == null)
{
throw new ServiceException("请选择应付产品");
}
PayProduct root = requireRoot(group.getProductId());
List<PayProductFeeRule> rules = normalizeFeeRules(root, group.getRules(), operator, now);
payProductFeeRuleMapper.deleteByProductId(root.getProductId());
if (!rules.isEmpty())
{
payProductFeeRuleMapper.insertBatch(rules);
}
}
}
private PayProduct requireRoot(Long productId)
{
PayProduct product = payProductMapper.selectPayProductById(productId);
if (product == null)
{
throw new ServiceException("应付产品不存在");
}
if (product.getOriginId() != null)
{
product = payProductMapper.selectPayProductById(product.getOriginId());
if (product == null)
{
throw new ServiceException("应付产品不存在");
}
}
return product;
}
private List<PayProductFeeRule> normalizeFeeRules(PayProduct root, List<PayProductFeeRule> source, String operator, Date now)
{
List<PayProductFeeRule> rules = new ArrayList<>();
Set<String> names = new HashSet<>();
String productName = root.getProductName() == null ? String.valueOf(root.getProductId()) : root.getProductName();
if (source == null)
{
return rules;
}
for (PayProductFeeRule raw : source)
{
if (raw == null || isBlank(raw.getFeeName()))
{
if (raw != null && (feeRuleFilled(raw) || feeRuleChanged(raw)))
{
throw new ServiceException(productName + "有未选择名称的附加费");
}
continue;
}
String feeName = raw.getFeeName().trim();
if (feeName.length() > 100)
{
throw new ServiceException(productName + "的附加费名称过长");
}
if (!names.add(feeName))
{
throw new ServiceException(productName + "的附加费「" + feeName + "」重复了");
}
String trigger = isBlank(raw.getTriggerType()) ? "ALWAYS" : raw.getTriggerType().trim().toUpperCase(Locale.ROOT);
if (!FEE_TRIGGERS.contains(trigger))
{
throw new ServiceException(productName + "的触发类型不正确");
}
String joinMode = isBlank(raw.getJoinMode()) ? "ANY" : raw.getJoinMode().trim().toUpperCase(Locale.ROOT);
if (!"ANY".equals(joinMode) && !"ALL".equals(joinMode))
{
throw new ServiceException(productName + "的命中方式不正确");
}
String enabled = "1".equals(raw.getEnabled()) ? "1" : "0";
checkFeeNumber(raw.getSideMaxCm(), productName, "最长边");
checkFeeNumber(raw.getSumMaxCm(), productName, "长宽高之和");
checkFeeNumber(raw.getVolumeMaxCm3(), productName, "体积");
checkFeeNumber(raw.getMinLengthCm(), productName, "最小长度");
checkFeeNumber(raw.getMinWidthCm(), productName, "最小宽度");
checkFeeNumber(raw.getMinHeightCm(), productName, "最小高度");
checkFeeNumber(raw.getWeightMaxKg(), productName, "重量上限");
String remark = raw.getRemark() == null ? null : raw.getRemark().trim();
if (remark != null && remark.length() > 500)
{
throw new ServiceException(productName + "的说明过长");
}
PayProductFeeRule rule = new PayProductFeeRule();
rule.setProductId(root.getProductId());
rule.setFeeName(feeName);
rule.setEnabled(enabled);
rule.setTriggerType(trigger);
rule.setSideMaxCm(raw.getSideMaxCm());
rule.setSumMaxCm(raw.getSumMaxCm());
rule.setVolumeMaxCm3(raw.getVolumeMaxCm3());
rule.setMinLengthCm(raw.getMinLengthCm());
rule.setMinWidthCm(raw.getMinWidthCm());
rule.setMinHeightCm(raw.getMinHeightCm());
rule.setWeightMaxKg(raw.getWeightMaxKg());
rule.setJoinMode(joinMode);
rule.setRemark(isBlank(remark) ? null : remark);
rule.setCreateBy(operator);
rule.setCreateTime(now);
rule.setUpdateBy(operator);
rule.setUpdateTime(now);
rules.add(rule);
}
return rules;
}
private static boolean feeRuleChanged(PayProductFeeRule rule)
{
String trigger = rule.getTriggerType() == null ? "" : rule.getTriggerType().trim();
String joinMode = rule.getJoinMode() == null ? "" : rule.getJoinMode().trim();
return (!trigger.isEmpty() && !"ALWAYS".equalsIgnoreCase(trigger))
|| (!joinMode.isEmpty() && !"ANY".equalsIgnoreCase(joinMode));
}
private static boolean feeRuleFilled(PayProductFeeRule rule)
{
return rule.getSideMaxCm() != null || rule.getSumMaxCm() != null || rule.getVolumeMaxCm3() != null
|| rule.getMinLengthCm() != null || rule.getMinWidthCm() != null || rule.getMinHeightCm() != null
|| rule.getWeightMaxKg() != null || !isBlank(rule.getRemark()) || "1".equals(rule.getEnabled());
}
private static void checkFeeNumber(BigDecimal value, String productName, String label)
{
if (value != null && value.signum() < 0)
{
throw new ServiceException(productName + "的" + label + "不能小于0");
}
}
private static boolean isBlank(String value)
{
return value == null || value.trim().isEmpty();
@@ -1427,6 +1335,19 @@ public class PayProductServiceImpl implements IPayProductService
return isBlank(value) ? fallback : value;
}
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;
@@ -0,0 +1,184 @@
<?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.masterdata.mapper.LineProductMapper">
<resultMap id="LineProductResult" type="com.bms.masterdata.domain.LineProduct">
<result property="id" column="id"/>
<result property="lineCode" column="line_code"/>
<result property="lineName" column="line_name"/>
<result property="productType" column="product_type"/>
<result property="lineType" column="line_type"/>
<result property="blCargo" column="bl_cargo"/>
<result property="cargoName" column="cargo_name"/>
<result property="productStorage" column="product_storage"/>
<result property="productFirst" column="product_first"/>
<result property="productClearance" column="product_clearance"/>
<result property="productStorageCode" column="product_storage_code"/>
<result property="productFirstCode" column="product_first_code"/>
<result property="productClearanceCode" column="product_clearance_code"/>
<result property="blOpen" column="bl_open"/>
<result property="blSection" column="bl_section"/>
<result property="dispCode" column="disp_code"/>
<result property="dispName" column="disp_name"/>
<result property="feeClass" column="fee_class"/>
<result property="shipperId" column="shipper_id"/>
<result property="shipmentRule" column="shipment_rule"/>
<result property="weight" column="weight"/>
<result property="cargoType" column="cargo_type"/>
<result property="productCategory" column="product_category"/>
<result property="volumeBasis" column="volume_basis"/>
<result property="weightType" column="weight_type"/>
<result property="serviceType" column="service_type"/>
<result property="shipmentType" column="shipment_type"/>
<result property="taxAmount" column="tax_amount"/>
<result property="taxCurrency" column="tax_currency"/>
<result property="batteryType" column="battery_type"/>
<result property="remark" column="remark"/>
<result property="createName" column="create_name"/>
<result property="createManCode" column="create_man_code"/>
<result property="createDate" column="create_date"/>
<result property="modifier" column="modifier"/>
<result property="modifierCode" column="modifier_code"/>
<result property="modifyDate" column="modify_date"/>
<result property="gmtCreate" column="gmt_create"/>
<result property="gmtModified" column="gmt_modified"/>
<result property="syncTime" column="sync_time"/>
</resultMap>
<sql id="selectLineProductVo">
select id, line_code, line_name, product_type, line_type, bl_cargo, cargo_name,
product_storage, product_first, product_clearance,
product_storage_code, product_first_code, product_clearance_code,
bl_open, bl_section, disp_code, disp_name, fee_class, shipper_id, shipment_rule,
weight, cargo_type, product_category, volume_basis, weight_type, service_type, shipment_type,
tax_amount, tax_currency, battery_type, remark,
create_name, create_man_code, create_date, modifier, modifier_code, modify_date,
gmt_create, gmt_modified, sync_time
from srv_line_product
</sql>
<select id="selectLineProductList" parameterType="com.bms.masterdata.domain.LineProduct" resultMap="LineProductResult">
<include refid="selectLineProductVo"/>
<where>
<if test="lineCode != null and lineCode != ''">
AND line_code like concat('%', #{lineCode}, '%')
</if>
<if test="lineName != null and lineName != ''">
AND line_name like concat('%', #{lineName}, '%')
</if>
<if test="productType != null and productType != ''">
AND product_type like concat('%', #{productType}, '%')
</if>
<if test="lineType != null and lineType != ''">
AND line_type like concat('%', #{lineType}, '%')
</if>
<if test="blOpen != null and blOpen != ''">
AND bl_open = #{blOpen}
</if>
<if test="blCargo != null and blCargo != ''">
AND bl_cargo = #{blCargo}
</if>
<if test="dispName != null and dispName != ''">
AND disp_name like concat('%', #{dispName}, '%')
</if>
<if test="cargoType != null and cargoType != ''">
AND cargo_type like concat('%', #{cargoType}, '%')
</if>
<if test="productCategory != null and productCategory != ''">
AND product_category like concat('%', #{productCategory}, '%')
</if>
</where>
order by line_code
</select>
<select id="selectLineProductById" parameterType="String" resultMap="LineProductResult">
<include refid="selectLineProductVo"/>
where id = #{id}
</select>
<select id="selectLineProductByCode" parameterType="String" resultMap="LineProductResult">
<include refid="selectLineProductVo"/>
where line_code = #{lineCode}
limit 1
</select>
<select id="countProductChannelByProductId" parameterType="String" resultType="int">
select count(1) from srv_product_channel where product_id = #{productId}
</select>
<insert id="insertLineProduct" parameterType="com.bms.masterdata.domain.LineProduct">
insert into srv_line_product (
id, line_code, line_name, product_type, line_type, bl_cargo, cargo_name,
product_storage, product_first, product_clearance,
product_storage_code, product_first_code, product_clearance_code,
bl_open, bl_section, disp_code, disp_name, fee_class, shipper_id, shipment_rule,
weight, cargo_type, product_category, volume_basis, weight_type, service_type, shipment_type,
tax_amount, tax_currency, battery_type, remark,
create_name, create_date, modifier, modify_date, gmt_create, gmt_modified
) values (
#{id}, #{lineCode}, #{lineName}, #{productType}, #{lineType}, #{blCargo}, #{cargoName},
#{productStorage}, #{productFirst}, #{productClearance},
#{productStorageCode}, #{productFirstCode}, #{productClearanceCode},
#{blOpen}, #{blSection}, #{dispCode}, #{dispName}, #{feeClass}, #{shipperId}, #{shipmentRule},
#{weight}, #{cargoType}, #{productCategory}, #{volumeBasis}, #{weightType}, #{serviceType}, #{shipmentType},
#{taxAmount}, #{taxCurrency}, #{batteryType}, #{remark},
#{createName}, sysdate(), #{modifier}, sysdate(), sysdate(), sysdate()
)
</insert>
<update id="updateLineProduct" parameterType="com.bms.masterdata.domain.LineProduct">
update srv_line_product
set line_code = #{lineCode},
line_name = #{lineName},
product_type = #{productType},
line_type = #{lineType},
bl_cargo = #{blCargo},
cargo_name = #{cargoName},
product_storage = #{productStorage},
product_first = #{productFirst},
product_clearance = #{productClearance},
product_storage_code = #{productStorageCode},
product_first_code = #{productFirstCode},
product_clearance_code = #{productClearanceCode},
bl_open = #{blOpen},
bl_section = #{blSection},
disp_code = #{dispCode},
disp_name = #{dispName},
fee_class = #{feeClass},
shipper_id = #{shipperId},
shipment_rule = #{shipmentRule},
weight = #{weight},
cargo_type = #{cargoType},
product_category = #{productCategory},
volume_basis = #{volumeBasis},
weight_type = #{weightType},
service_type = #{serviceType},
shipment_type = #{shipmentType},
tax_amount = #{taxAmount},
tax_currency = #{taxCurrency},
battery_type = #{batteryType},
remark = #{remark},
modifier = #{modifier},
modify_date = sysdate(),
gmt_modified = sysdate()
where id = #{id}
</update>
<update id="updateLineProductStatus" parameterType="com.bms.masterdata.domain.LineProduct">
update srv_line_product
set bl_open = #{blOpen},
modifier = #{modifier},
modify_date = sysdate(),
gmt_modified = sysdate()
where id = #{id}
</update>
<delete id="deleteLineProductByIds" parameterType="String">
delete from srv_line_product where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
@@ -1,42 +0,0 @@
<?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.PayProductFeeRuleMapper">
<select id="selectByProductId" resultType="com.bms.price.domain.PayProductFeeRule">
select rule_id as ruleId, product_id as productId, fee_name as feeName, enabled,
trigger_type as triggerType, side_max_cm as sideMaxCm, sum_max_cm as sumMaxCm,
volume_max_cm3 as volumeMaxCm3, min_length_cm as minLengthCm, min_width_cm as minWidthCm,
min_height_cm as minHeightCm, weight_max_kg as weightMaxKg, join_mode as joinMode,
remark, created_by as createBy, create_time as createTime, updated_by as updateBy,
update_time as updateTime
from pay_product_fee_rule
where product_id = #{productId}
order by rule_id
</select>
<delete id="deleteByProductId">
delete from pay_product_fee_rule where product_id = #{productId}
</delete>
<delete id="deleteByProductIds">
delete from pay_product_fee_rule where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<insert id="insertBatch">
insert into pay_product_fee_rule (
product_id, fee_name, enabled, trigger_type, side_max_cm, sum_max_cm, volume_max_cm3,
min_length_cm, min_width_cm, min_height_cm, weight_max_kg, join_mode, remark,
created_by, create_time, updated_by, update_time
) values
<foreach collection="list" item="item" separator=",">
(#{item.productId}, #{item.feeName}, #{item.enabled}, #{item.triggerType},
#{item.sideMaxCm}, #{item.sumMaxCm}, #{item.volumeMaxCm3},
#{item.minLengthCm}, #{item.minWidthCm}, #{item.minHeightCm}, #{item.weightMaxKg},
#{item.joinMode}, #{item.remark}, #{item.createBy}, #{item.createTime},
#{item.updateBy}, #{item.updateTime})
</foreach>
</insert>
</mapper>