应付账单初始化

This commit is contained in:
caojia
2026-11-04 16:04:38 +08:00
committed by liuminghui
parent 04be15deab
commit 51a89b18e1
90 changed files with 15661 additions and 6 deletions
@@ -27,6 +27,11 @@ public interface ServiceChannelMapper
*/
ServiceChannel selectServiceChannelByCode(String channelCode);
/**
* 按渠道名称精确查询,重名时返回多条供调用方拒绝。
*/
List<ServiceChannel> selectServiceChannelByName(String channelName);
/**
* 产品渠道关系中引用该渠道编码的数量
*/
@@ -0,0 +1,35 @@
package com.bms.price.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/**
* 渠道和产品的收付关系。payType 取字典 bms_pay_type。
*/
@Data
public class ChannelProductMap
{
private Long id;
/** 渠道表主键 srv_service_channel.id */
private String channelId;
private Long productId;
/** 应收套餐 prc_price_set.price_set_id */
private Long priceSetId;
/** AR 应收,AP 应付 */
private String payType;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -0,0 +1,61 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 应付账单。作废后仍保留。 */
@Data
public class PayBill
{
private Long billId;
private String billNo;
private Long supplierId;
private String supplierName;
private String currency;
/** ORDER 订单号,TIME 计费日,LADING 提单 */
private String sourceType;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date periodStart;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date periodEnd;
private Integer orderCount;
private BigDecimal totalAmount;
/** DRAFT CONFIRMED PAID VOID */
private String status;
private String voidReason;
private String voidBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date voidTime;
private String createBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
private String updateBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
private String remark;
private List<PayBillLine> lines;
private List<PayBillLog> logs;
}
@@ -0,0 +1,27 @@
package com.bms.price.domain;
import lombok.Data;
/** 查询或生成应付账单。订单号和提单号可用逗号、空格、换行分隔。 */
@Data
public class PayBillCommand
{
/** ORDER、TIME、LADING */
private String sourceType;
private String orderText;
private String beginDate;
private String endDate;
private String ladingText;
private Long supplierId;
private String remark;
private String voidReason;
private Long billId;
}
@@ -0,0 +1,41 @@
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 PayBillLine
{
private Long lineId;
private Long billId;
private String orderBillId;
private String orderNo;
private String ladingNo;
private Long ladingFeeId;
private String feeName;
private BigDecimal amount;
private String currency;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date priceDate;
private Integer versionNo;
private String remoteFlag;
private String densityLevel;
/** ORDER 订单费用,LADING 留在主单上的费用 */
private String lineSource;
}
@@ -0,0 +1,27 @@
package com.bms.price.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
/** 应付账单操作记录。作废后不删除。 */
@Data
public class PayBillLog
{
private Long logId;
private Long billId;
private String operationType;
private String fromStatus;
private String toStatus;
private String operationDesc;
private String operatorName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
@@ -0,0 +1,48 @@
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 PayBillOrder
{
private String billId;
private String orderNo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date bizDate;
private BigDecimal payTotal;
private String currency;
private String ladingNo;
private String trackingNo;
private String channelName;
private String productName;
private String supplierName;
private Integer pieces;
private BigDecimal chargeWeight;
private String countryCode;
private String countryName;
private String destPostcode;
/** 已算出的应付费用,名称和金额用分号拼在一起 */
private String feeText;
/** 已挂在未作废账单上时的账单号 */
private String occupiedBillNo;
}
@@ -0,0 +1,18 @@
package com.bms.price.domain;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/** 应付账单生成前的查询结果。 */
@Data
public class PayBillPreview
{
private List<PayBillOrder> orders = new ArrayList<>();
private List<String> missingNos = new ArrayList<>();
private List<PayBillOrder> occupied = new ArrayList<>();
private String message;
}
@@ -0,0 +1,21 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import lombok.Data;
/** 一条算出的应付费用。 */
@Data
public class PayCostLine
{
private String feeType;
private String feeName;
private Long scId;
private BigDecimal amount;
private String currency;
private String remark;
}
@@ -0,0 +1,21 @@
package com.bms.price.domain;
import lombok.Data;
/**
* 产品报价表里的邮编或其他不能落进重量段、附加费的配置原文。
*/
@Data
public class PayExtraLine
{
private Long lineId;
private Long productId;
/** 邮编 或 配置 */
private String lineType;
private String lineText;
private Integer sortNo;
}
@@ -0,0 +1,32 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import java.util.List;
import lombok.Data;
/** 按订单生成应付的结果。skipped 是价卡里有、但这票算不了的附加费。 */
@Data
public class PayOrderCostResult
{
private String billId;
private String channelId;
private String channelName;
private Long productId;
private String productName;
private Integer versionNo;
private String currency;
private BigDecimal payTotal;
private String message;
private List<PayCostLine> fees;
private List<String> skipped;
}
@@ -0,0 +1,64 @@
package com.bms.price.domain;
import java.util.ArrayList;
import java.util.List;
import com.bms.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 应付邮编配置的一条分区或一条合并后的附加费。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PayPostcodeGroup extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long groupId;
private String sourceFile;
private String fileMd5;
private String sheetName;
private String blockTitle;
/** ZONE 邮编分区,FEE 附加费 */
private String configKind;
/** REACH 可达,UNREACH 不可达,REMOTE 偏远。普通分区为空 */
private String coverage;
/** 渠道名称,从表头识别,可在页面修改 */
private String channelName;
/** 目录里的服务类型,和报价产品对应 */
private String serviceType;
/** 目录里的国家。邮编目的国仍在 countryName,偏远表里两者可以不同 */
private String catalogCountry;
/** 目录里的销售产品 */
private String productName;
/** 目录里的报价工作表 */
private String quoteSheet;
private String countryName;
private String countryCode;
private String feeName;
private String feeText;
private Integer lineCount;
/** 查询条件:邮编 */
private String postcode;
/** 导入时挂上的明细,列表查询不填充 */
private List<PayPostcodeLine> lines = new ArrayList<>();
}
@@ -0,0 +1,23 @@
package com.bms.price.domain;
import java.io.Serializable;
import lombok.Data;
/** 一次 Excel 导入的汇总,不把全部邮编返回给页面。 */
@Data
public class PayPostcodeImportResult implements Serializable
{
private static final long serialVersionUID = 1L;
private String sourceFile;
private int groupCount;
private int feeCount;
private int zoneCount;
private int lineCount;
private String message;
}
@@ -0,0 +1,41 @@
package com.bms.price.domain;
import java.io.Serializable;
import lombok.Data;
/**
* 一条邮编。除末尾星号外,地区、分区和金额都保持 Excel 原文。
*/
@Data
public class PayPostcodeLine implements Serializable
{
private static final long serialVersionUID = 1L;
private Long lineId;
private Long groupId;
private String countryName;
private String countryCode;
private String regionName;
private String regionNameCn;
private String cityName;
private String regionCode;
private String zoneName;
private String postcode;
private String postcodeRaw;
private String postcodeEnd;
private String extraText;
private Integer sortNo;
}
@@ -0,0 +1,28 @@
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 PayPreview
{
private String sourceFile;
private String fileMd5;
private String supplierName;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date effectiveDate;
private List<PayProduct> products = new ArrayList<>();
/** 没有对上字典、因此没有读取的工作表 */
private List<String> skippedSheets = new ArrayList<>();
}
@@ -0,0 +1,17 @@
package com.bms.price.domain;
import java.util.List;
import lombok.Data;
/** 旧版和新版价格对照。 */
@Data
public class PayPriceCompare
{
private PayProduct older;
private PayProduct newer;
private List<PayPriceDiff> weightDiffs;
private List<PayPriceDiff> surchargeDiffs;
}
@@ -0,0 +1,19 @@
package com.bms.price.domain;
import lombok.Data;
/** 两个价格版本里的一行差异。 */
@Data
public class PayPriceDiff
{
private String name;
private String zoneName;
/** 相同、变更、新增、删除。新增和删除相对新版本而言 */
private String change;
private String oldValue;
private String newValue;
}
@@ -0,0 +1,146 @@
package com.bms.price.domain;
import java.util.Date;
import java.util.List;
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 PayProduct extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long productId;
@Excel(name = "产品名称", width = 24)
private String productName;
/** 支付类型,字典 bms_pay_type:AR应收 AP应付 */
private String productType;
/** 字典 payment_product_type 的键值,和报价工作表对应 */
private String dictValue;
/** 复制来源。空表示这是公共模板 */
private Long originId;
/** 服务渠道 ID。空表示公共模板 */
private String channelId;
/** 版本号。模板是 1,渠道另存从 2 起 */
private Integer versionNo;
/** 列表只看公共模板 */
private Boolean templateOnly;
/** 列表每个产品只显示当前生效版;没有生效版时显示最新一版 */
private Boolean currentOnly;
/** 当前价格版本对应的记录。列表上的 productId 是不变的产品编号 */
private Long versionProductId;
/** 当前版本的有效期记录 */
private Long termId;
/** 0 生效,1 失效 */
private String termStatus;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date termStart;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date termEnd;
@Excel(name = "国家/口岸", width = 18)
private String countryScope;
/** 字典 order_charge_type:续重计费、单票价、常规计重价 */
@Excel(name = "费用类型", width = 14)
private String chargeType;
/** 续重时每公斤加价。各分区相同是一个金额,不同是最小~最大 */
@Excel(name = "每公斤加价", width = 14)
private String extraPerKg;
@Excel(name = "服务类型", width = 16)
private String serviceType;
@Excel(name = "分类", width = 16)
private String classification;
@Excel(name = "目录备注", width = 28)
private String catalogRemark;
@Excel(name = "更新内容", width = 18)
private String updateContent;
@Excel(name = "重量限制", width = 14)
private String weightLimit;
@Excel(name = "报价工作表", width = 24)
private String sheetName;
@Excel(name = "关联分区表", width = 24)
private String relatedSheet;
@Excel(name = "来源文件", width = 36)
private String sourceFile;
private String fileMd5;
@Excel(name = "价卡归属", width = 12)
private String supplierName;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@Excel(name = "生效日期", width = 14, dateFormat = "yyyy-MM-dd")
private Date effectiveDate;
@Excel(name = "币种", width = 10)
private String currency;
@Excel(name = "服务说明", width = 40)
private String serviceNote;
@Excel(name = "识别结果", width = 12)
private String parseStatus;
@Excel(name = "重量段数", width = 12)
private Integer weightCount;
@Excel(name = "附加费数", width = 12)
private Integer surchargeCount;
@Excel(name = "其他配置数", width = 12)
private Integer extraCount;
/** 查询条件:生效日起,格式 yyyy-MM-dd */
private String effectiveBegin;
/** 查询条件:生效日止 */
private String effectiveEnd;
/** 查询条件:附加费名称、收费标准、费用说明或时效原文 */
private String surchargeName;
/** 查询条件:重量段、费用项、分区或国家 */
private String weightKeyword;
/** 预览时标记库里是否已有同一文件、同一工作表、同一产品 */
private Boolean duplicated;
/** 已存在记录的主键,覆盖保存时使用 */
private Long existsId;
private List<PayWeightBand> weightBands;
private List<PaySurcharge> surcharges;
private List<PayExtraLine> extraLines;
}
@@ -0,0 +1,45 @@
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 PayProductTerm extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long termId;
/** 字典 payment_product_type,同一产品的各版本用它归组 */
private String dictValue;
private Long productId;
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;
/** 0 生效,1 失效 */
private String status;
private String productName;
private String sourceFile;
private Integer weightCount;
private Integer surchargeCount;
private String currency;
}
@@ -0,0 +1,17 @@
package com.bms.price.domain;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
/**
* 预览确认后的保存请求。只保存前端勾选并回传的产品。
*/
@Data
public class PaySaveCommand
{
/** 为 true 时,同文件同表同名的旧产品先删后插 */
private boolean overwrite;
private List<PayProduct> products = new ArrayList<>();
}
@@ -0,0 +1,57 @@
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 PaySurcharge
{
private Long surchargeId;
private Long productId;
/** 所属价卡版本,跟产品版本一起变 */
private Integer versionNo;
private String sectionName;
private String feeName;
private String zoneName;
/** 收费标准原文 */
private String chargeText;
/** 从收费标准里解析出的金额;百分比时这里是百分数 */
private BigDecimal amount;
private String currency;
/** 费用说明 */
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;
/** ALWAYS、POSTCODE、WEIGHT、SIZE、RETURN、MANUAL */
private String calcRule;
/** 邮编列表、重量阈值或尺寸表达式 */
private String calcParam;
private Integer sortNo;
}
@@ -0,0 +1,48 @@
package com.bms.price.domain;
import java.math.BigDecimal;
import lombok.Data;
/**
* 应付产品上的一行重量段、首重续重或分区单价。
*/
@Data
public class PayWeightBand
{
private Long bandId;
private Long productId;
/** 所属价卡版本,跟产品版本一起变 */
private Integer versionNo;
/** 报价区块,例如派送费、偏远附加费 */
private String sectionName;
private String countryName;
private String countryCode;
/** 价格列名称,例如英国大陆、马德里高密度 */
private String zoneName;
/** 重量段原文 */
private String weightLabel;
private BigDecimal weightMin;
private BigDecimal weightMax;
/** 体积上限,数字加单位,如 10000cm³。空表示这档不看体积 */
private String volumeText;
/** 处理费、派送费、续重等 */
private String chargeName;
private BigDecimal price;
/** EUR/ITEM、GBP/KG 等 */
private String priceUnit;
private Integer sortNo;
}
@@ -23,6 +23,9 @@ public class PrcPayBillFee implements Serializable
private String currency;
private String roundMode;
private String remark;
/** CALC 计算,LADING 提单分摊,MANUAL 人工 */
private String feeSource;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
@@ -35,4 +35,6 @@ public class PrcProfitReportQuery extends BaseEntity
private String arBillStatus;
/** 是否按产品维度分组值下钻订单明细,允许分组字段为空。 */
private Boolean productDrilldown;
/** 仅用于拒绝已暂停的提成未计算订单下钻请求。 */
private String detailType;
}
@@ -0,0 +1,18 @@
package com.bms.price.mapper;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.ChannelProductMap;
/**
* 渠道产品关系映射
*/
public interface ChannelProductMapMapper
{
ChannelProductMap selectByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType);
int upsert(ChannelProductMap row);
int deleteByChannelAndType(@Param("channelId") String channelId, @Param("payType") String payType);
int deleteByProductId(@Param("productId") Long productId);
}
@@ -0,0 +1,33 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PayBill;
import com.bms.price.domain.PayBillLine;
import com.bms.price.domain.PayBillLog;
import com.bms.price.domain.PayBillOrder;
import com.bms.price.domain.PrcPayBillFee;
public interface PayBillMapper
{
List<PayBill> selectPayBillList(PayBill query);
PayBill selectPayBillById(Long billId);
List<PayBillLine> selectLines(Long billId);
List<PayBillLog> selectLogs(Long billId);
List<PayBillOrder> selectCandidateOrders(@Param("orderNos") List<String> orderNos, @Param("beginDate") String beginDate,
@Param("endDate") String endDate, @Param("ladingNos") List<String> ladingNos);
List<PrcPayBillFee> selectOrderFees(@Param("billIds") List<String> billIds);
int insertPayBill(PayBill bill);
int updatePayBillStatus(PayBill bill);
int insertLines(@Param("list") List<PayBillLine> list);
int insertLog(PayBillLog log);
}
@@ -0,0 +1,34 @@
package com.bms.price.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PayPostcodeGroup;
import com.bms.price.domain.PayPostcodeLine;
/** 应付邮编配置 */
public interface PayPostcodeMapper
{
List<PayPostcodeGroup> selectGroupList(PayPostcodeGroup query);
PayPostcodeGroup selectGroupById(Long groupId);
int updateGroup(PayPostcodeGroup group);
List<PayPostcodeLine> selectLineList(PayPostcodeLine query);
int insertGroup(PayPostcodeGroup group);
int insertLines(@Param("list") List<PayPostcodeLine> list);
List<Long> selectGroupIds(@Param("fileMd5") String fileMd5, @Param("sheetName") String sheetName);
List<PayPostcodeGroup> selectGroupsByTokens(@Param("tokens") List<String> tokens);
List<PayPostcodeLine> selectRegionKeys(@Param("groupIds") List<Long> groupIds);
List<PayPostcodeLine> selectLinesByGroups(@Param("groupIds") List<Long> groupIds);
int deleteLines(@Param("groupIds") List<Long> groupIds);
int deleteGroups(@Param("groupIds") List<Long> groupIds);
}
@@ -0,0 +1,60 @@
package com.bms.price.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PayExtraLine;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PaySurcharge;
import com.bms.price.domain.PayWeightBand;
/**
* 应付价卡产品、重量段和附加费
*/
public interface PayProductMapper
{
List<PayProduct> selectPayProductList(PayProduct query);
PayProduct selectPayProductById(Long productId);
Long selectExistingId(@Param("fileMd5") String fileMd5, @Param("sheetName") String sheetName,
@Param("productName") String productName);
Long selectTemplateId(@Param("dictValue") String dictValue);
List<PayProduct> selectTemplateOptions();
Integer selectMaxVersion(@Param("originId") Long originId);
int insertPayProduct(PayProduct product);
int updatePayProduct(PayProduct product);
int insertWeightBands(@Param("list") List<PayWeightBand> list);
int insertSurcharges(@Param("list") List<PaySurcharge> list);
int insertExtraLines(@Param("list") List<PayExtraLine> list);
List<PayWeightBand> selectWeightBands(@Param("productIds") List<Long> productIds);
List<PaySurcharge> selectSurcharges(@Param("productIds") List<Long> productIds);
List<PayExtraLine> selectExtraLines(@Param("productIds") List<Long> productIds);
int deleteWeightBands(@Param("productIds") List<Long> productIds);
int deleteSurcharges(@Param("productIds") List<Long> productIds);
int deleteExtraLines(@Param("productIds") List<Long> productIds);
int deletePayProducts(@Param("productIds") List<Long> productIds);
List<Long> selectFamilyIds(@Param("rootId") Long rootId);
Long selectEffectiveVersionId(@Param("stableId") Long stableId, @Param("bizDate") Date bizDate);
PayProduct selectInactiveCovering(@Param("stableId") Long stableId, @Param("bizDate") Date bizDate);
Long selectLatestVersionId(@Param("stableId") Long stableId);
}
@@ -0,0 +1,31 @@
package com.bms.price.mapper;
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.bms.price.domain.PayProductTerm;
/**
* 应付产品价格有效期
*/
public interface PayProductTermMapper
{
List<PayProductTerm> selectByDict(@Param("dictValue") String dictValue);
PayProductTerm selectById(Long termId);
PayProductTerm selectByProductId(Long productId);
int insertTerm(PayProductTerm term);
int updatePeriod(PayProductTerm term);
int updateStatus(PayProductTerm term);
/** 新版本生效时,把同一产品仍生效的旧版本关掉,结束日收到新版本开始的前一天 */
int closeOpenTerms(@Param("dictValue") String dictValue, @Param("endDate") Date endDate, @Param("updateBy") String updateBy);
int deleteByProductIds(@Param("productIds") List<Long> productIds);
int updateProductEffectiveDate(@Param("productId") Long productId, @Param("effectiveDate") Date effectiveDate);
}
@@ -134,4 +134,7 @@ public interface PrcOrderBillMapper
@Param("surchargeAmount") BigDecimal surchargeAmount,
@Param("incomeTotal") BigDecimal incomeTotal,
@Param("payTotal") BigDecimal payTotal);
/** 只更新应付合计,不改应收。 */
int updatePayTotal(@Param("billId") String billId, @Param("payTotal") BigDecimal payTotal);
}
@@ -0,0 +1,444 @@
package com.bms.price.pay;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import com.bms.price.domain.PayCostLine;
import com.bms.price.domain.PayOrderCostResult;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PaySurcharge;
import com.bms.price.domain.PayWeightBand;
/**
* 用渠道上的应付产品,按订单重量、尺寸、邮编和是否退件计算应付。
* 重量段价格默认是该段的一票价格;单位写明每公斤时才乘重量。
*/
public final class PayCostCalculator
{
private static final ZoneId SHANGHAI = ZoneId.of("GMT+8");
private PayCostCalculator()
{
}
public static PayOrderCostResult calculate(PayProduct product, BigDecimal weight, BigDecimal length, BigDecimal width,
BigDecimal height, String postcode, Date bizDate, Integer orderStatus)
{
return calculate(product, weight, length, width, height, postcode, null, bizDate, orderStatus);
}
public static PayOrderCostResult calculate(PayProduct 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<>());
result.setSkipped(new ArrayList<>());
if (product == null)
{
result.setMessage("没有应付产品");
return result;
}
result.setProductId(product.getProductId());
result.setProductName(product.getProductName());
if (weight == null || weight.compareTo(BigDecimal.ZERO) <= 0)
{
result.setMessage("订单没有计费重");
return result;
}
List<PayWeightBand> bands = forZone(usableBands(product.getWeightBands()), zoneName);
if (zoneName != null && !zoneName.isBlank() && bands.isEmpty())
{
result.setMessage("邮编对应的地区「" + zoneName + "」不在价卡重量段里");
return result;
}
Set<String> zones = zones(bands);
if (zones.size() > 1)
{
result.setMessage("产品「" + product.getProductName() + "」有多个地区(" + String.join("、", zones)
+ "),邮编没有对上其中的地区,无法确定运费");
return result;
}
PayWeightBand band = matchBand(bands, weight);
if (band == null)
{
result.setMessage(outOfRange(product, bands, weight));
return result;
}
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())
{
freightRemark = freightRemark + ",地区 " + zoneName.replace('\n', ' ');
}
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);
BigDecimal total = BigDecimal.ZERO;
for (PayCostLine fee : result.getFees())
{
total = total.add(fee.getAmount());
}
result.setPayTotal(total.setScale(2, RoundingMode.HALF_UP));
result.setMessage(summary(result));
return result;
}
private static List<PayWeightBand> forZone(List<PayWeightBand> bands, String zoneName)
{
if (zoneName == null || zoneName.isBlank())
{
return bands;
}
List<PayWeightBand> kept = new ArrayList<>();
for (PayWeightBand band : bands)
{
if (PayZoneMatcher.sameZone(band.getZoneName(), zoneName))
{
kept.add(band);
}
}
return kept;
}
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)
{
if (product.getSurcharges() == null)
{
return;
}
List<PayCostLine> sized = new ArrayList<>();
for (PaySurcharge fee : product.getSurcharges())
{
if (fee == null || !inForce(fee, bizDate))
{
continue;
}
if (zoneName != null && !zoneName.isBlank() && fee.getZoneName() != null && !fee.getZoneName().isBlank()
&& !PayZoneMatcher.sameZone(fee.getZoneName(), zoneName))
{
continue;
}
String rule = fee.getCalcRule() == null || fee.getCalcRule().isBlank() ? PaySurchargeRules.MANUAL : fee.getCalcRule();
String name = blank(fee.getFeeName());
if (PaySurchargeRules.POSTCODE.equals(rule))
{
if (postcode == null || postcode.isBlank())
{
result.getSkipped().add(name + ":订单没有邮编");
}
else if (PaySurchargeRules.matchesPostcode(fee.getCalcParam(), postcode))
{
addFlat(result.getFees(), fee, currency, "邮编 " + postcode.trim());
}
}
else if (PaySurchargeRules.WEIGHT.equals(rule))
{
if (PaySurchargeRules.matchesWeight(fee.getCalcParam(), weight))
{
addFlat(result.getFees(), fee, currency, "计费重超过 " + fee.getCalcParam() + "kg");
}
}
else if (PaySurchargeRules.SIZE.equals(rule))
{
if (length == null || width == null || height == null)
{
result.getSkipped().add(name + ":订单没有长宽高");
}
else if (PaySurchargeRules.matchesSize(fee.getCalcParam(), length, width, height))
{
PayCostLine matched = matchedFee(fee, freight, currency, "尺寸命中 " + fee.getCalcParam());
if (matched != null)
{
sized.add(matched);
}
}
}
else if (PaySurchargeRules.RETURN.equals(rule))
{
if (orderStatus != null && orderStatus == 9)
{
addFlat(result.getFees(), fee, currency, "订单已退件");
}
}
else if (PaySurchargeRules.ALWAYS.equals(rule))
{
PayCostLine matched = matchedFee(fee, freight, currency, blank(fee.getChargeText()));
if (matched != null)
{
result.getFees().add(matched);
}
}
else if (!name.isEmpty() && !name.contains("报价说明"))
{
result.getSkipped().add(name + ":价卡没有可计算的条件,需人工确认");
}
}
result.getFees().addAll(keepHighestByName(sized));
}
/** 同一费用名的尺寸档同时命中时,只收较高的一档,例如 70% 覆盖 35%。 */
private static List<PayCostLine> keepHighestByName(List<PayCostLine> lines)
{
List<PayCostLine> kept = new ArrayList<>();
for (PayCostLine line : lines)
{
PayCostLine same = null;
for (PayCostLine exists : kept)
{
if (exists.getFeeName().equals(line.getFeeName()))
{
same = exists;
break;
}
}
if (same == null)
{
kept.add(line);
}
else if (line.getAmount().compareTo(same.getAmount()) > 0)
{
kept.set(kept.indexOf(same), line);
}
}
return kept;
}
private static void addFlat(List<PayCostLine> fees, PaySurcharge fee, String currency, String remark)
{
PayCostLine line = matchedFee(fee, null, currency, remark);
if (line != null && !percent(fee))
{
fees.add(line);
}
}
private static PayCostLine matchedFee(PaySurcharge fee, BigDecimal freight, String currency, String remark)
{
if (fee.getAmount() == null)
{
return null;
}
BigDecimal amount = fee.getAmount();
if (percent(fee))
{
if (freight == null)
{
return null;
}
amount = freight.multiply(fee.getAmount()).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP);
}
else
{
amount = amount.setScale(2, RoundingMode.HALF_UP);
String feeCurrency = normalize(fee.getCurrency());
if (!feeCurrency.isEmpty() && !feeCurrency.equals(currency))
{
return null;
}
}
long scId = fee.getSurchargeId() == null ? 0L : fee.getSurchargeId();
return line("SURCHARGE", blank(fee.getFeeName()), scId, amount, currency, remark);
}
private static boolean percent(PaySurcharge fee)
{
String charge = fee.getChargeText() == null ? "" : fee.getChargeText();
return charge.contains("%") || "%".equals(fee.getCurrency());
}
private static boolean inForce(PaySurcharge fee, Date bizDate)
{
if (bizDate == null || (fee.getEffectiveStart() == null && fee.getEffectiveEnd() == null))
{
return true;
}
LocalDate day = toLocal(bizDate);
if (fee.getEffectiveStart() != null && day.isBefore(toLocal(fee.getEffectiveStart())))
{
return false;
}
if (fee.getEffectiveEnd() != null && day.isAfter(toLocal(fee.getEffectiveEnd())))
{
return false;
}
return true;
}
private static List<PayWeightBand> usableBands(List<PayWeightBand> bands)
{
List<PayWeightBand> usable = new ArrayList<>();
if (bands == null)
{
return usable;
}
for (PayWeightBand band : bands)
{
if (band != null && band.getPrice() != null && band.getWeightMin() != null && band.getWeightMax() != null)
{
usable.add(band);
}
}
usable.sort((left, right) -> left.getWeightMin().compareTo(right.getWeightMin()));
return usable;
}
private static Set<String> zones(List<PayWeightBand> bands)
{
Set<String> zones = new LinkedHashSet<>();
for (PayWeightBand band : bands)
{
if (band.getZoneName() != null && !band.getZoneName().isBlank())
{
zones.add(band.getZoneName().trim());
}
}
return zones;
}
private static PayWeightBand matchBand(List<PayWeightBand> bands, BigDecimal weight)
{
for (PayWeightBand band : bands)
{
if (weight.compareTo(band.getWeightMin()) >= 0 && weight.compareTo(band.getWeightMax()) <= 0)
{
return band;
}
}
for (int i = 0; i < bands.size() - 1; i++)
{
PayWeightBand current = bands.get(i);
PayWeightBand next = bands.get(i + 1);
BigDecimal gap = next.getWeightMin().subtract(current.getWeightMax());
if (weight.compareTo(current.getWeightMax()) > 0 && weight.compareTo(next.getWeightMin()) < 0
&& gap.compareTo(new BigDecimal("0.01")) <= 0)
{
return next;
}
}
return null;
}
private static String outOfRange(PayProduct product, List<PayWeightBand> bands, BigDecimal weight)
{
String name = blank(product.getProductName());
if (bands.isEmpty())
{
return "产品「" + name + "」没有可匹配的重量段";
}
BigDecimal max = bands.get(bands.size() - 1).getWeightMax();
if (weight.compareTo(max) > 0)
{
return "计费重 " + plain(weight) + "kg 超出「" + name + "」的重量段,最大 " + plain(max) + "kg";
}
return "计费重 " + plain(weight) + "kg 没有落在「" + name + "」的重量段里";
}
private static BigDecimal bandAmount(PayWeightBand band, BigDecimal weight)
{
BigDecimal price = band.getPrice();
if (perKilogram(band))
{
return price.multiply(weight).setScale(2, RoundingMode.HALF_UP);
}
return price.setScale(2, RoundingMode.HALF_UP);
}
private static boolean perKilogram(PayWeightBand band)
{
String blob = (blank(band.getPriceUnit()) + " " + blank(band.getChargeName())).toLowerCase(Locale.ROOT);
return blob.contains("/kg") || blob.contains("每公斤") || blob.contains("每千克") || blob.contains("/公斤");
}
private static String currencyOf(PayProduct product, PayWeightBand band)
{
String unit = blank(band.getPriceUnit()).toUpperCase(Locale.ROOT);
if (unit.contains("GBP") || unit.contains("英镑"))
{
return "GBP";
}
if (unit.contains("EUR") || unit.contains("欧元"))
{
return "EUR";
}
if (unit.contains("USD") || unit.contains("美元"))
{
return "USD";
}
if (unit.contains("CNY") || unit.contains("RMB") || unit.contains("人民币"))
{
return "CNY";
}
String currency = blank(product.getCurrency());
return currency.isEmpty() ? "CNY" : currency;
}
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.setScale(2, RoundingMode.HALF_UP));
line.setCurrency(currency);
line.setRemark(remark == null ? "" : remark.trim());
return line;
}
private static String summary(PayOrderCostResult result)
{
String text = "应付 " + plain(result.getPayTotal()) + " " + result.getCurrency();
if (result.getSkipped() != null && !result.getSkipped().isEmpty())
{
text = text + ",另有 " + result.getSkipped().size() + " 项附加费未自动计算";
}
return text;
}
private static LocalDate toLocal(Date date)
{
if (date instanceof java.sql.Date sql)
{
return sql.toLocalDate();
}
return date.toInstant().atZone(SHANGHAI).toLocalDate();
}
private static String plain(BigDecimal value)
{
return value == null ? "" : value.stripTrailingZeros().toPlainString();
}
private static String blank(String value)
{
return value == null ? "" : value.trim();
}
private static String normalize(String currency)
{
if (currency == null || currency.isBlank() || "%".equals(currency.trim()))
{
return "";
}
String value = currency.trim().toUpperCase(Locale.ROOT);
if ("英镑".equals(currency))
{
return "GBP";
}
if ("欧元".equals(currency))
{
return "EUR";
}
return value;
}
}
@@ -0,0 +1,182 @@
package com.bms.price.pay;
import java.io.IOException;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import com.bms.price.domain.PayExtraLine;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PaySurcharge;
import com.bms.price.domain.PayWeightBand;
/**
* 把已保存的应付配置导出成产品、重量段、附加费、其他配置四张表。
*/
public final class PayDetailExporter
{
private static final int MAX_ROWS = 50000;
private PayDetailExporter()
{
}
public static void write(OutputStream output, List<PayProduct> products) throws IOException
{
try (XSSFWorkbook workbook = new XSSFWorkbook())
{
Sheet productSheet = workbook.createSheet("产品");
header(productSheet, "产品名称", "价卡归属", "国家/口岸", "生效日期", "报价工作表", "关联分区表", "币种",
"识别结果", "重量段数", "附加费数", "其他配置数", "来源文件", "服务说明");
Sheet weightSheet = workbook.createSheet("重量段");
header(weightSheet, "产品名称", "报价工作表", "区块", "国家", "国家代码", "分区", "重量段", "下限KG", "上限KG",
"体积", "费用项", "单价", "单位");
Sheet feeSheet = workbook.createSheet("附加费");
header(feeSheet, "产品名称", "报价工作表", "区块", "费用名称", "分区", "收费标准", "金额", "币种", "收取时点",
"生效开始", "生效结束", "时效原文", "费用说明");
Sheet extraSheet = workbook.createSheet("其他配置");
header(extraSheet, "产品名称", "报价工作表", "类型", "配置内容");
int weightRows = 1;
int feeRows = 1;
int extraRows = 1;
if (products != null)
{
int rowIndex = 1;
for (PayProduct product : products)
{
Row row = productSheet.createRow(rowIndex++);
cell(row, 0, product.getProductName());
cell(row, 1, product.getSupplierName());
cell(row, 2, product.getCountryScope());
cell(row, 3, day(product.getEffectiveDate()));
cell(row, 4, product.getSheetName());
cell(row, 5, product.getRelatedSheet());
cell(row, 6, product.getCurrency());
cell(row, 7, product.getParseStatus());
cell(row, 8, String.valueOf(product.getWeightCount()));
cell(row, 9, String.valueOf(product.getSurchargeCount()));
cell(row, 10, String.valueOf(product.getExtraCount()));
cell(row, 11, product.getSourceFile());
cell(row, 12, product.getServiceNote());
weightRows = writeWeights(weightSheet, weightRows, product);
feeRows = writeFees(feeSheet, feeRows, product);
extraRows = writeExtras(extraSheet, extraRows, product);
}
}
workbook.write(output);
}
}
private static int writeWeights(Sheet sheet, int rowIndex, PayProduct product)
{
if (product.getWeightBands() == null)
{
return rowIndex;
}
for (PayWeightBand band : product.getWeightBands())
{
if (rowIndex >= MAX_ROWS)
{
return rowIndex;
}
Row row = sheet.createRow(rowIndex++);
cell(row, 0, product.getProductName());
cell(row, 1, product.getSheetName());
cell(row, 2, band.getSectionName());
cell(row, 3, band.getCountryName());
cell(row, 4, band.getCountryCode());
cell(row, 5, band.getZoneName());
cell(row, 6, band.getWeightLabel());
cell(row, 7, plain(band.getWeightMin()));
cell(row, 8, plain(band.getWeightMax()));
cell(row, 9, band.getVolumeText());
cell(row, 10, band.getChargeName());
cell(row, 11, plain(band.getPrice()));
cell(row, 12, band.getPriceUnit());
}
return rowIndex;
}
private static int writeFees(Sheet sheet, int rowIndex, PayProduct product)
{
if (product.getSurcharges() == null)
{
return rowIndex;
}
for (PaySurcharge fee : product.getSurcharges())
{
if (rowIndex >= MAX_ROWS)
{
return rowIndex;
}
Row row = sheet.createRow(rowIndex++);
cell(row, 0, product.getProductName());
cell(row, 1, product.getSheetName());
cell(row, 2, fee.getSectionName());
cell(row, 3, fee.getFeeName());
cell(row, 4, fee.getZoneName());
cell(row, 5, fee.getChargeText());
cell(row, 6, plain(fee.getAmount()));
cell(row, 7, fee.getCurrency());
cell(row, 8, fee.getChargeTiming());
cell(row, 9, day(fee.getEffectiveStart()));
cell(row, 10, day(fee.getEffectiveEnd()));
cell(row, 11, fee.getPeriodText());
cell(row, 12, fee.getFeeDesc());
}
return rowIndex;
}
private static int writeExtras(Sheet sheet, int rowIndex, PayProduct product)
{
if (product.getExtraLines() == null)
{
return rowIndex;
}
for (PayExtraLine line : product.getExtraLines())
{
if (rowIndex >= MAX_ROWS)
{
return rowIndex;
}
Row row = sheet.createRow(rowIndex++);
cell(row, 0, product.getProductName());
cell(row, 1, product.getSheetName());
cell(row, 2, line.getLineType());
cell(row, 3, line.getLineText());
}
return rowIndex;
}
private static void header(Sheet sheet, String... titles)
{
Row row = sheet.createRow(0);
for (int i = 0; i < titles.length; i++)
{
row.createCell(i).setCellValue(titles[i]);
sheet.setColumnWidth(i, 18 * 256);
}
}
private static void cell(Row row, int col, String value)
{
row.createCell(col).setCellValue(value == null ? "" : value);
}
private static String plain(java.math.BigDecimal value)
{
return value == null ? "" : value.stripTrailingZeros().toPlainString();
}
private static String day(Date value)
{
if (value == null)
{
return "";
}
return new SimpleDateFormat("yyyy-MM-dd").format(value);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,355 @@
package com.bms.price.pay;
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.PaySurcharge;
/**
* 把附加费说明收成可计算的条件。认不出的保持人工,避免把签名、重复单号当成每票必收。
*/
public final class PaySurchargeRules
{
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";
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 PaySurchargeRules()
{
}
public static void apply(PaySurcharge 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)
{
UkPostcode parsed = parseUk(postcode);
if (parsed == null || param == null || param.isBlank())
{
return false;
}
for (String token : param.split(","))
{
if (tokenMatches(token.trim().toUpperCase(Locale.ROOT), parsed))
{
return true;
}
}
return false;
}
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.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.pay;
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 PayTexts
{
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 PayTexts()
{
}
/** 从重量段原文取出上下限。每增加一公斤只保留原文,不编造上下限。 */
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,315 @@
package com.bms.price.pay;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.bms.price.domain.PayPostcodeLine;
/**
* 价卡有多个地区时,用邮编在分区表里确定价格列。
* 邮编不在分区表里、价卡又有「大陆」列时,按大陆计费。
*/
public final class PayZoneMatcher
{
private static final Pattern TOKEN = Pattern.compile("[a-z0-9]+|[\\u4e00-\\u9fa5]+");
private static final Set<String> GENERIC_TOKENS = Set.of("英国", "美国", "德国", "法国", "西班牙", "意大利", "挂号", "普货", "标快", "快递", "专线", "末端", "包裹");
private PayZoneMatcher()
{
}
/** 中英文连在一起时拆开,例如「英国evri」取出 evri,用来对邮编分区标题。 */
public static List<String> searchTokens(String... texts)
{
Set<String> tokens = new LinkedHashSet<>();
if (texts == null)
{
return new ArrayList<>();
}
for (String text : texts)
{
if (text == null || text.isBlank())
{
continue;
}
Matcher matcher = TOKEN.matcher(text.toLowerCase(Locale.ROOT));
while (matcher.find())
{
String token = matcher.group();
if (token.length() >= 3 && !GENERIC_TOKENS.contains(token))
{
tokens.add(token);
}
}
}
return new ArrayList<>(tokens);
}
public static final class Decision
{
private final String zoneName;
private final String message;
private Decision(String zoneName, String message)
{
this.zoneName = zoneName;
this.message = message;
}
public static Decision zone(String zoneName)
{
return new Decision(zoneName, null);
}
public static Decision fail(String message)
{
return new Decision(null, message);
}
public static Decision unknown()
{
return new Decision(null, null);
}
public String getZoneName()
{
return zoneName;
}
public String getMessage()
{
return message;
}
}
public static Decision resolve(List<String> zoneNames, List<PayPostcodeLine> lines, String postcode)
{
if (zoneNames == null || zoneNames.size() <= 1)
{
return Decision.unknown();
}
if (postcode == null || postcode.isBlank())
{
return Decision.fail("订单没有邮编,无法从多个地区里确定运费");
}
String bfpo = bfpoZone(zoneNames, postcode);
if (bfpo != null)
{
return Decision.zone(bfpo);
}
if (lines == null || lines.isEmpty())
{
return Decision.unknown();
}
String bestZone = null;
int bestLength = -1;
boolean tie = false;
boolean unmapped = false;
String unmappedName = "";
for (PayPostcodeLine line : lines)
{
int length = matchedLength(line, postcode);
if (length < 0)
{
continue;
}
String region = firstText(line.getZoneName(), line.getRegionName());
String zone = mapRegion(zoneNames, region);
if (zone == null)
{
unmapped = true;
unmappedName = region;
continue;
}
if (length > bestLength)
{
bestLength = length;
bestZone = zone;
tie = false;
}
else if (length == bestLength && !zone.equals(bestZone))
{
tie = true;
}
}
if (tie)
{
return Decision.fail("邮编 " + postcode.trim() + " 对上了多个地区,无法确定运费");
}
if (bestZone != null)
{
return Decision.zone(bestZone);
}
if (unmapped)
{
return Decision.fail("邮编 " + postcode.trim() + " 落在「" + unmappedName + "」,价卡没有同名地区");
}
String mainland = mainlandZone(zoneNames);
if (mainland != null)
{
return Decision.zone(mainland);
}
return Decision.fail("邮编 " + postcode.trim() + " 没有对上价卡地区");
}
public static String mapRegion(List<String> zoneNames, String region)
{
if (region == null || region.isBlank() || zoneNames == null)
{
return null;
}
String normRegion = norm(region);
List<String> hits = new ArrayList<>();
for (String zone : zoneNames)
{
String normZone = norm(zone);
if (!normRegion.isEmpty() && (normZone.contains(normRegion) || normRegion.contains(normZone)))
{
hits.add(zone);
}
}
if (hits.size() == 1)
{
return hits.get(0);
}
hits.clear();
for (String word : region.toLowerCase(Locale.ROOT).split("[^a-z0-9\\u4e00-\\u9fa5]+"))
{
if (word.length() < 5)
{
continue;
}
for (String zone : zoneNames)
{
if (norm(zone).contains(word) && !hits.contains(zone))
{
hits.add(zone);
}
}
}
if (hits.size() == 1)
{
return hits.get(0);
}
if (containsAny(region, "scottish", "shetland", "highland", "高地"))
{
return uniqueKeyword(zoneNames, "highlands", "高地");
}
if (containsAny(region, "scilly", "锡利"))
{
return uniqueKeyword(zoneNames, "scilly", "锡利", "海峡");
}
return null;
}
public static boolean sameZone(String left, String right)
{
String a = norm(left);
String b = norm(right);
return !a.isEmpty() && (a.equals(b) || a.contains(b) || b.contains(a));
}
private static String bfpoZone(List<String> zoneNames, String postcode)
{
String compact = postcode.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]", "");
if (!compact.startsWith("BFPO") && !compact.startsWith("BF"))
{
return null;
}
return uniqueKeyword(zoneNames, "bfpo");
}
private static String mainlandZone(List<String> zoneNames)
{
List<String> hits = new ArrayList<>();
for (String zone : zoneNames)
{
String text = norm(zone);
if (text.contains("mainland") || text.contains("大陆"))
{
hits.add(zone);
}
}
return hits.size() == 1 ? hits.get(0) : null;
}
private static int matchedLength(PayPostcodeLine line, String postcode)
{
int best = -1;
best = longer(best, line.getPostcode(), postcode);
best = longer(best, line.getPostcodeEnd(), postcode);
return best;
}
private static int longer(int best, String raw, String postcode)
{
if (raw == null || raw.isBlank())
{
return best;
}
for (String token : raw.split("[,,、|;;/]+"))
{
String text = token.trim();
if (text.isEmpty())
{
continue;
}
if (PaySurchargeRules.matchesPostcode(text, postcode) && text.length() > best)
{
best = text.length();
}
}
return best;
}
private static String uniqueKeyword(List<String> zoneNames, String... keywords)
{
List<String> hits = new ArrayList<>();
for (String zone : zoneNames)
{
String text = norm(zone);
if (containsAny(text, keywords) && !hits.contains(zone))
{
hits.add(zone);
}
}
return hits.size() == 1 ? hits.get(0) : null;
}
private static boolean containsAny(String text, String... keywords)
{
String value = text == null ? "" : text.toLowerCase(Locale.ROOT);
for (String keyword : keywords)
{
if (value.contains(keyword.toLowerCase(Locale.ROOT)))
{
return true;
}
}
return false;
}
private static String firstText(String first, String second)
{
if (first != null && !first.isBlank())
{
return first.trim();
}
return second == null ? "" : second.trim();
}
static String norm(String value)
{
if (value == null)
{
return "";
}
return value.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9\\u4e00-\\u9fa5]", "");
}
}
@@ -0,0 +1,23 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PayBill;
import com.bms.price.domain.PayBillCommand;
import com.bms.price.domain.PayBillPreview;
public interface IPayBillService
{
List<PayBill> selectPayBillList(PayBill query);
PayBill selectPayBillDetail(Long billId);
PayBillPreview preview(PayBillCommand command);
PayBill generate(PayBillCommand command);
int confirm(Long billId);
int markPaid(Long billId);
int voidBill(PayBillCommand command);
}
@@ -0,0 +1,9 @@
package com.bms.price.service;
import com.bms.price.domain.PayOrderCostResult;
/** 按订单生成应付:渠道服务商上的应付产品,用订单基础数据计算。 */
public interface IPayOrderCostService
{
PayOrderCostResult generate(String billId);
}
@@ -0,0 +1,22 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PayPostcodeGroup;
import com.bms.price.domain.PayPostcodeImportResult;
import com.bms.price.domain.PayPostcodeLine;
/** 数据管理下的应付邮编配置 */
public interface IPayPostcodeService
{
List<PayPostcodeGroup> selectGroupList(PayPostcodeGroup query);
PayPostcodeGroup selectGroupById(Long groupId);
int updateGroup(PayPostcodeGroup group);
List<PayPostcodeLine> selectLineList(PayPostcodeLine query);
PayPostcodeImportResult importExcel(byte[] bytes, String filename, String username);
int deleteByIds(Long[] groupIds);
}
@@ -0,0 +1,70 @@
package com.bms.price.service;
import java.util.List;
import java.util.Date;
import com.bms.price.domain.PayPreview;
import com.bms.price.domain.PayPriceCompare;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PayProductTerm;
import com.bms.price.domain.PaySaveCommand;
/**
* 应付价卡配置:预览后保存,列表查询和导出。
*/
public interface IPayProductService
{
/**
* 按字典匹配工作表并识别,不写数据库。
* dictValue 有值时只读这一张,为空时读取文件里所有能对上的工作表。
*/
PayPreview preview(byte[] bytes, String filename, String dictValue);
/** 把预览里勾选的产品写入数据库。同名字典产品再次导入时另存一版,旧价格留在有效期表里 */
String save(PaySaveCommand command);
/** 一个产品的全部价格版本 */
List<PayProductTerm> listTerms(Long productId);
/** 修改某一版的生效起止 */
int updateTerm(PayProductTerm term);
/** 0 生效,1 失效 */
int changeTermStatus(PayProductTerm term);
/** 修改已保存的产品,重量段和附加费按提交内容整表替换 */
int updateDetail(PayProduct product);
/** 公共模板下拉。payType 为空时按应付 AP */
List<PayProduct> selectTemplateOptions(String payType);
/** 渠道绑定的产品。返回的 originId 是不变的产品编号,价格是当前有效版 */
PayProduct selectChannelPrice(String channelId);
/** 只绑定产品编号,不产生新版本。payType 为空时按应付 AP */
void bindChannel(String channelId, Long productId, Long priceSetId, String payType);
/** 取消渠道上指定收付类型的产品。payType 为空时按应付 AP */
void unbindChannel(String channelId, String payType);
/** 按订单时间取这一产品当时有效的价格版本 */
PayProduct selectPriceForDate(String channelId, Date bizDate);
/** 对照两个版本的重量段和附加费 */
PayPriceCompare compare(Long leftId, Long rightId);
/** 按编辑结果给同一产品编号追加一版,渠道绑定保持原产品编号 */
PayProduct saveChannelVersion(String channelId, PayProduct edited);
/** 重新导入价格,仍记在原来的产品编号下 */
PayProduct reimportChannel(String channelId, byte[] bytes, String filename);
List<PayProduct> selectPayProductList(PayProduct query);
/** 列表条件对应的产品,并带上重量段、附加费和其他配置 */
List<PayProduct> selectPayProductDetails(PayProduct query);
/** 产品及其重量段、附加费、邮编配置 */
PayProduct selectPayProductDetail(Long productId);
int deletePayProducts(Long[] productIds);
}
@@ -0,0 +1,399 @@
package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.SecurityUtils;
import com.bms.common.utils.uuid.IdUtils;
import com.bms.customer.supplierinfo.domain.BmsSupplier;
import com.bms.customer.supplierinfo.mapper.BmsSupplierMapper;
import com.bms.price.domain.PayBill;
import com.bms.price.domain.PayBillCommand;
import com.bms.price.domain.PayBillLine;
import com.bms.price.domain.PayBillLog;
import com.bms.price.domain.PayBillOrder;
import com.bms.price.domain.PayBillPreview;
import com.bms.price.domain.PayOrderCostResult;
import com.bms.price.domain.PrcPayBillFee;
import com.bms.price.mapper.PayBillMapper;
import com.bms.price.service.IPayBillService;
import com.bms.price.service.IPayOrderCostService;
/**
* 应付账单。先按订单号、计费日或提单查出订单,再把当时的费用抄进账单。作废只改状态。
*/
@Service
public class PayBillServiceImpl implements IPayBillService
{
private static final int MAX_ORDERS = 2000;
@Autowired
private PayBillMapper payBillMapper;
@Autowired
private IPayOrderCostService payOrderCostService;
@Autowired
private BmsSupplierMapper supplierMapper;
@Override
public List<PayBill> selectPayBillList(PayBill query)
{
return payBillMapper.selectPayBillList(query);
}
@Override
public PayBill selectPayBillDetail(Long billId)
{
PayBill bill = payBillMapper.selectPayBillById(billId);
if (bill == null)
{
throw new ServiceException("应付账单不存在");
}
bill.setLines(payBillMapper.selectLines(billId));
bill.setLogs(payBillMapper.selectLogs(billId));
return bill;
}
@Override
public PayBillPreview preview(PayBillCommand command)
{
return collect(command, false);
}
@Override
@Transactional(rollbackFor = Exception.class)
public PayBill generate(PayBillCommand command)
{
if (command.getSupplierId() == null)
{
throw new ServiceException("请选择供应商");
}
BmsSupplier supplier = supplierMapper.selectBmsSupplierBySupplierId(command.getSupplierId());
if (supplier == null)
{
throw new ServiceException("供应商不存在");
}
PayBillPreview preview = collect(command, true);
List<PayBillOrder> orders = freeOrders(preview);
if (orders.isEmpty())
{
throw new ServiceException(preview.getMissingNos().isEmpty() ? "没有可生成的订单" : "订单号没有对上:" + String.join("、", preview.getMissingNos()));
}
String currency = currencyOf(orders);
Map<String, Integer> versions = new HashMap<>();
for (PayBillOrder order : orders)
{
PayOrderCostResult cost = payOrderCostService.generate(order.getBillId());
if (cost != null)
{
versions.put(order.getBillId(), cost.getVersionNo());
}
}
List<String> billIds = new ArrayList<>();
for (PayBillOrder order : orders)
{
billIds.add(order.getBillId());
}
List<PrcPayBillFee> fees = billIds.isEmpty() ? new ArrayList<>() : payBillMapper.selectOrderFees(billIds);
if (!fees.isEmpty() && fees.get(0).getCurrency() != null && !fees.get(0).getCurrency().isBlank())
{
currency = fees.get(0).getCurrency();
}
List<PayBillLine> lines = new ArrayList<>();
for (PrcPayBillFee fee : fees)
{
PayBillOrder order = findOrder(orders, fee.getBillId());
PayBillLine line = new PayBillLine();
line.setOrderBillId(fee.getBillId());
line.setOrderNo(order == null ? "" : order.getOrderNo());
line.setLadingNo(order == null ? "" : order.getLadingNo());
line.setFeeName(fee.getFeeName() == null || fee.getFeeName().isBlank() ? fee.getFeeType() : fee.getFeeName());
line.setAmount(fee.getAmount());
line.setCurrency(fee.getCurrency());
line.setPriceDate(order == null || order.getBizDate() == null ? null : order.getBizDate());
line.setVersionNo(versions.get(fee.getBillId()));
line.setLineSource("ORDER");
lines.add(line);
}
if (lines.isEmpty())
{
throw new ServiceException("订单没有应付费用");
}
BigDecimal total = BigDecimal.ZERO;
for (PayBillLine line : lines)
{
String lineCurrency = line.getCurrency() == null ? "" : line.getCurrency();
if (!currency.equals(lineCurrency))
{
throw new ServiceException("所选订单币种不一致,请按币种分开生成");
}
total = total.add(line.getAmount() == null ? BigDecimal.ZERO : line.getAmount());
}
String operator = SecurityUtils.getUsername();
PayBill bill = new PayBill();
bill.setBillNo(nextNo());
bill.setSupplierId(supplier.getSupplierId());
bill.setSupplierName(supplier.getSupplierName());
bill.setCurrency(currency);
bill.setSourceType(mode(command));
bill.setPeriodStart(parseDay(command.getBeginDate()));
bill.setPeriodEnd(parseDay(command.getEndDate()));
bill.setOrderCount(orders.size());
bill.setTotalAmount(total.setScale(2, RoundingMode.HALF_UP));
bill.setCreateBy(operator);
bill.setRemark(command.getRemark());
payBillMapper.insertPayBill(bill);
for (PayBillLine line : lines)
{
line.setBillId(bill.getBillId());
}
payBillMapper.insertLines(lines);
writeLog(bill.getBillId(), "CREATE", null, "DRAFT", "生成应付账单,订单 " + orders.size() + " 票", operator);
return selectPayBillDetail(bill.getBillId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public int confirm(Long billId)
{
PayBill bill = required(billId);
if (!"DRAFT".equals(bill.getStatus()))
{
throw new ServiceException("只有草稿可以确认");
}
String operator = SecurityUtils.getUsername();
bill.setStatus("CONFIRMED");
bill.setUpdateBy(operator);
payBillMapper.updatePayBillStatus(bill);
writeLog(billId, "CONFIRM", "DRAFT", "CONFIRMED", "确认应付账单", operator);
return 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int markPaid(Long billId)
{
PayBill bill = required(billId);
if (!"CONFIRMED".equals(bill.getStatus()))
{
throw new ServiceException("请先确认账单,再登记付款");
}
String operator = SecurityUtils.getUsername();
bill.setStatus("PAID");
bill.setUpdateBy(operator);
payBillMapper.updatePayBillStatus(bill);
writeLog(billId, "PAY", "CONFIRMED", "PAID", "登记付款", operator);
return 1;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int voidBill(PayBillCommand command)
{
if (command == null || command.getBillId() == null)
{
throw new ServiceException("请选择应付账单");
}
if (command.getVoidReason() == null || command.getVoidReason().isBlank())
{
throw new ServiceException("请填写作废原因");
}
PayBill bill = required(command.getBillId());
if ("VOID".equals(bill.getStatus()))
{
throw new ServiceException("账单已经作废");
}
if ("PAID".equals(bill.getStatus()))
{
throw new ServiceException("已付款的账单不能直接作废,请先冲回付款");
}
String operator = SecurityUtils.getUsername();
String from = bill.getStatus();
bill.setStatus("VOID");
bill.setVoidReason(command.getVoidReason().trim());
bill.setVoidBy(operator);
bill.setVoidTime(new Date());
bill.setUpdateBy(operator);
payBillMapper.updatePayBillStatus(bill);
writeLog(bill.getBillId(), "VOID", from, "VOID", command.getVoidReason().trim(), operator);
return 1;
}
private PayBillPreview collect(PayBillCommand command, boolean forGenerate)
{
String mode = mode(command);
List<String> orderNos = "ORDER".equals(mode) ? tokens(command.getOrderText()) : new ArrayList<>();
List<String> ladingNos = "LADING".equals(mode) ? tokens(command.getLadingText()) : new ArrayList<>();
if ("ORDER".equals(mode) && orderNos.isEmpty())
{
throw new ServiceException("请填写订单号");
}
if ("TIME".equals(mode) && (command.getBeginDate() == null || command.getBeginDate().isBlank()
|| command.getEndDate() == null || command.getEndDate().isBlank()))
{
throw new ServiceException("请选择计费日起止");
}
if ("LADING".equals(mode) && ladingNos.isEmpty())
{
throw new ServiceException("请填写提单号");
}
List<PayBillOrder> found = payBillMapper.selectCandidateOrders(
orderNos.isEmpty() ? null : orderNos,
"TIME".equals(mode) ? command.getBeginDate() : null,
"TIME".equals(mode) ? command.getEndDate() : null,
ladingNos.isEmpty() ? null : ladingNos);
if ("TIME".equals(mode))
{
found.removeIf(order -> order.getBizDate() == null);
}
PayBillPreview preview = new PayBillPreview();
if ("ORDER".equals(mode))
{
for (String no : orderNos)
{
boolean hit = false;
for (PayBillOrder order : found)
{
if (no.equals(order.getOrderNo()))
{
hit = true;
break;
}
}
if (!hit)
{
preview.getMissingNos().add(no);
}
}
}
for (PayBillOrder order : found)
{
if (order.getOccupiedBillNo() != null && !order.getOccupiedBillNo().isBlank())
{
preview.getOccupied().add(order);
}
else
{
preview.getOrders().add(order);
}
}
if (preview.getOrders().size() >= MAX_ORDERS)
{
preview.setMessage("一次最多处理 " + MAX_ORDERS + " 票,请缩小范围");
}
if (!forGenerate && !preview.getMissingNos().isEmpty())
{
preview.setMessage("未找到订单:" + String.join("、", preview.getMissingNos()));
}
return preview;
}
private List<PayBillOrder> freeOrders(PayBillPreview preview)
{
return preview.getOrders();
}
private String currencyOf(List<PayBillOrder> orders)
{
String currency = "";
for (PayBillOrder order : orders)
{
if (order.getCurrency() != null && !order.getCurrency().isBlank())
{
currency = order.getCurrency();
break;
}
}
return currency.isEmpty() ? "CNY" : currency;
}
private PayBillOrder findOrder(List<PayBillOrder> orders, String billId)
{
for (PayBillOrder order : orders)
{
if (order.getBillId().equals(billId))
{
return order;
}
}
return null;
}
private PayBill required(Long billId)
{
PayBill bill = payBillMapper.selectPayBillById(billId);
if (bill == null)
{
throw new ServiceException("应付账单不存在");
}
return bill;
}
private void writeLog(Long billId, String type, String from, String to, String desc, String operator)
{
PayBillLog log = new PayBillLog();
log.setBillId(billId);
log.setOperationType(type);
log.setFromStatus(from);
log.setToStatus(to);
log.setOperationDesc(desc);
log.setOperatorName(operator);
payBillMapper.insertLog(log);
}
private String nextNo()
{
return "AP" + new SimpleDateFormat("yyyyMM").format(new Date())
+ IdUtils.fastSimpleUUID().substring(0, 8).toUpperCase();
}
private Date parseDay(String text)
{
if (text == null || text.isBlank())
{
return null;
}
LocalDate day = LocalDate.parse(text.trim());
return java.sql.Date.valueOf(day);
}
private String mode(PayBillCommand command)
{
if (command == null || command.getSourceType() == null || command.getSourceType().isBlank())
{
throw new ServiceException("请选择查询方式");
}
return command.getSourceType().trim().toUpperCase(Locale.ROOT);
}
static List<String> tokens(String text)
{
LinkedHashSet<String> values = new LinkedHashSet<>();
if (text == null)
{
return new ArrayList<>();
}
for (String part : text.split("[,,、;;\\s]+"))
{
String token = part.trim();
if (!token.isEmpty())
{
values.add(token);
}
}
return new ArrayList<>(values);
}
}
@@ -0,0 +1,317 @@
package com.bms.price.service.impl;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.SecurityUtils;
import com.bms.masterdata.domain.ServiceChannel;
import com.bms.masterdata.mapper.ServiceChannelMapper;
import com.bms.price.domain.PayCostLine;
import com.bms.price.domain.PayOrderCostResult;
import com.bms.price.domain.PayPostcodeGroup;
import com.bms.price.domain.PayPostcodeLine;
import com.bms.price.domain.PayProduct;
import com.bms.price.domain.PayWeightBand;
import com.bms.price.domain.PrcOrderBill;
import com.bms.price.domain.PrcOrderBillExt;
import com.bms.price.domain.PrcPayBill;
import com.bms.price.domain.PrcPayBillFee;
import com.bms.price.mapper.PayPostcodeMapper;
import com.bms.price.mapper.PrcOrderBillExtMapper;
import com.bms.price.mapper.PrcOrderBillMapper;
import com.bms.price.mapper.PrcPayBillFeeMapper;
import com.bms.price.mapper.PrcPayBillMapper;
import com.bms.price.pay.PayCostCalculator;
import com.bms.price.pay.PayZoneMatcher;
import com.bms.price.service.IPayOrderCostService;
import com.bms.price.service.IPayProductService;
/**
* 订单找到渠道,渠道找到当前应付产品,再按重量、尺寸、邮编计算应付并落库。
*/
@Service
public class PayOrderCostServiceImpl implements IPayOrderCostService
{
@Autowired
private PrcOrderBillMapper orderBillMapper;
@Autowired
private PrcOrderBillExtMapper orderBillExtMapper;
@Autowired
private ServiceChannelMapper serviceChannelMapper;
@Autowired
private IPayProductService payProductService;
@Autowired
private PrcPayBillMapper payBillMapper;
@Autowired
private PrcPayBillFeeMapper payBillFeeMapper;
@Autowired
private PayPostcodeMapper payPostcodeMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public PayOrderCostResult generate(String billId)
{
if (billId == null || billId.isBlank())
{
throw new ServiceException("请选择订单");
}
PrcOrderBill bill = orderBillMapper.selectPrcOrderBillById(billId.trim());
if (bill == null)
{
throw new ServiceException("订单不存在");
}
PrcOrderBillExt ext = orderBillExtMapper.selectByBillId(bill.getBillId());
ServiceChannel channel = resolveChannel(ext);
java.util.Date bizDate = bill.getBizDate() != null ? bill.getBizDate() : bill.getOrderTime();
PayProduct product = payProductService.selectPriceForDate(channel.getId(), bizDate);
BigDecimal weight = firstPositive(bill.getChargeWeight(), bill.getWeight(), bill.getActualWeight(), bill.getReceiveWeight());
Integer orderStatus = bill.getOrderStatus() != null ? bill.getOrderStatus() : (ext == null ? null : ext.getOrderStatus());
String zoneName = resolveZone(channel, product, bill.getDestPostcode(), bill.getCountryCode(), bill.getCountryName());
PayOrderCostResult result = PayCostCalculator.calculate(product, weight, bill.getPkgLength(), bill.getPkgWidth(),
bill.getPkgHeight(), bill.getDestPostcode(), zoneName, bizDate, orderStatus);
result.setBillId(bill.getBillId());
result.setChannelId(channel.getId());
result.setChannelName(channel.getChannelName());
result.setVersionNo(product.getVersionNo());
if (result.getFees() == null || result.getFees().isEmpty())
{
throw new ServiceException(result.getMessage() == null ? "没有算出应付费用" : result.getMessage());
}
String operator = SecurityUtils.getUsername();
PrcPayBill payBill = saveBill(bill, result, operator);
saveFees(bill.getBillId(), payBill.getPayBillId(), result.getFees());
orderBillMapper.updatePayTotal(bill.getBillId(), result.getPayTotal());
String channelName = channel.getChannelName() == null ? channel.getChannelCode() : channel.getChannelName();
String end = product.getTermEnd() == null ? "不限" : day(product.getTermEnd());
result.setMessage("渠道「" + channelName + "」产品编号 " + product.getOriginId() + " 第 " + product.getVersionNo()
+ " 版(" + day(product.getTermStart()) + " 至 " + end + ")," + result.getMessage());
return result;
}
/** 价卡分成多个地区时,用邮编在对应分区表里选出价格列。 */
private String resolveZone(ServiceChannel channel, PayProduct product, String postcode, String countryCode, String countryName)
{
List<String> zoneNames = zoneNames(product);
if (zoneNames.size() <= 1)
{
return null;
}
List<String> tokens = PayZoneMatcher.searchTokens(product.getProductName(), product.getSheetName());
if (tokens.isEmpty())
{
tokens = PayZoneMatcher.searchTokens(channel.getChannelName(), channel.getChannelCode());
}
if (tokens.isEmpty())
{
throw new ServiceException("产品「" + product.getProductName() + "」有多个地区,订单邮编无法对应到产品");
}
List<PayPostcodeGroup> groups = new ArrayList<>();
for (PayPostcodeGroup group : payPostcodeMapper.selectGroupsByTokens(tokens))
{
if (sameCountry(group, countryCode, countryName))
{
groups.add(group);
}
}
if (groups.isEmpty())
{
throw new ServiceException("产品「" + product.getProductName() + "」有多个地区,没有找到对应的邮编分区");
}
List<Long> groupIds = new ArrayList<>();
for (PayPostcodeGroup group : groups)
{
groupIds.add(group.getGroupId());
}
Set<Long> usable = new LinkedHashSet<>();
for (PayPostcodeLine key : payPostcodeMapper.selectRegionKeys(groupIds))
{
String region = key.getZoneName() != null && !key.getZoneName().isBlank() ? key.getZoneName() : key.getRegionName();
if (PayZoneMatcher.mapRegion(zoneNames, region) != null)
{
usable.add(key.getGroupId());
}
}
if (usable.isEmpty())
{
throw new ServiceException("产品「" + product.getProductName() + "」有多个地区,邮编分区对不上价卡上的地区");
}
PayZoneMatcher.Decision decision = PayZoneMatcher.resolve(zoneNames, payPostcodeMapper.selectLinesByGroups(new ArrayList<>(usable)), postcode);
if (decision.getMessage() != null)
{
throw new ServiceException(decision.getMessage());
}
if (decision.getZoneName() == null)
{
throw new ServiceException("邮编没有对上产品「" + product.getProductName() + "」的地区");
}
return decision.getZoneName();
}
private static List<String> zoneNames(PayProduct product)
{
Set<String> names = new LinkedHashSet<>();
if (product.getWeightBands() == null)
{
return new ArrayList<>();
}
for (PayWeightBand band : product.getWeightBands())
{
if (band != null && band.getZoneName() != null && !band.getZoneName().isBlank())
{
names.add(band.getZoneName().trim());
}
}
return new ArrayList<>(names);
}
private static boolean sameCountry(PayPostcodeGroup group, String countryCode, String countryName)
{
if (blank(countryCode) && blank(countryName))
{
return true;
}
if (blank(group.getCountryCode()) && blank(group.getCountryName()))
{
return true;
}
if (!blank(countryCode) && countryCode.equalsIgnoreCase(group.getCountryCode()))
{
return true;
}
return !blank(countryName) && countryName.equals(group.getCountryName());
}
private ServiceChannel resolveChannel(PrcOrderBillExt ext)
{
if (ext == null)
{
throw new ServiceException("订单没有渠道服务商");
}
ServiceChannel channel = blank(ext.getProductChannelId()) ? null : serviceChannelMapper.selectServiceChannelById(ext.getProductChannelId().trim());
if (channel == null && !blank(ext.getProductFloorCode()))
{
channel = serviceChannelMapper.selectServiceChannelByCode(ext.getProductFloorCode().trim());
}
if (channel == null && !blank(ext.getProductFloor()))
{
List<ServiceChannel> named = serviceChannelMapper.selectServiceChannelByName(ext.getProductFloor().trim());
if (named != null && named.size() > 1)
{
throw new ServiceException("渠道名称「" + ext.getProductFloor().trim() + "」对应了多条渠道,请先在订单上指定渠道");
}
channel = named == null || named.isEmpty() ? null : named.get(0);
}
if (channel == null)
{
throw new ServiceException("订单没有渠道服务商,或渠道尚未同步");
}
return channel;
}
private PrcPayBill saveBill(PrcOrderBill bill, PayOrderCostResult result, String operator)
{
PrcPayBill existed = payBillMapper.selectByBillId(bill.getBillId());
PrcPayBill row = existed == null ? new PrcPayBill() : existed;
if (existed == null)
{
row.setPayBillId(UUID.randomUUID().toString());
row.setBillId(bill.getBillId());
row.setCreateBy(operator);
}
row.setCurrency(result.getCurrency());
row.setCostAmount(result.getPayTotal());
row.setPayTotal(result.getPayTotal());
row.setBelongSaler(bill.getBelongSaler());
row.setBelongSalerCode(bill.getBelongSalerCode());
row.setSettlementMan(bill.getSettlementMan());
row.setSettlementManCode(bill.getSettlementManCode());
row.setUpdateBy(operator);
if (existed == null)
{
payBillMapper.insertPrcPayBill(row);
}
else
{
payBillMapper.updatePrcPayBill(row);
}
return row;
}
private void saveFees(String billId, String payBillId, List<PayCostLine> fees)
{
payBillFeeMapper.deleteByBillId(billId);
List<PrcPayBillFee> rows = new java.util.ArrayList<>();
long fallback = 1L;
for (PayCostLine fee : fees)
{
PrcPayBillFee row = new PrcPayBillFee();
row.setFeeId(UUID.randomUUID().toString());
row.setBillId(billId);
row.setPayBillId(payBillId);
row.setFeeType(fee.getFeeType());
row.setFeeName(fee.getFeeName());
long scId = fee.getScId() == null ? 0L : fee.getScId();
if ("SURCHARGE".equals(fee.getFeeType()) && scId == 0L)
{
scId = fallback++;
}
row.setScId(scId);
row.setAmount(fee.getAmount());
row.setCurrency(fee.getCurrency());
row.setRoundMode("HALF_UP");
row.setRemark(fee.getRemark());
rows.add(row);
}
if (!rows.isEmpty())
{
payBillFeeMapper.insertBatch(rows);
}
}
private static String day(java.util.Date date)
{
if (date == null)
{
return "不限";
}
if (date instanceof java.sql.Date sql)
{
return sql.toLocalDate().toString();
}
return date.toInstant().atZone(java.time.ZoneId.of("GMT+8")).toLocalDate().toString();
}
private static BigDecimal firstPositive(BigDecimal... values)
{
if (values == null)
{
return null;
}
for (BigDecimal value : values)
{
if (value != null && value.compareTo(BigDecimal.ZERO) > 0)
{
return value;
}
}
return null;
}
private static boolean blank(String value)
{
return value == null || value.isBlank();
}
}
@@ -0,0 +1,177 @@
package com.bms.price.service.impl;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.bms.common.exception.ServiceException;
import com.bms.price.domain.PayPostcodeGroup;
import com.bms.price.domain.PayPostcodeImportResult;
import com.bms.price.domain.PayPostcodeLine;
import com.bms.price.mapper.PayPostcodeMapper;
import com.bms.price.pay.PayPostcodeImporter;
import com.bms.price.service.IPayPostcodeService;
/**
* 导入时按文件和工作表覆盖。附加费在解析阶段已经按国家和金额合并。
*/
@Service
public class PayPostcodeServiceImpl implements IPayPostcodeService
{
private static final int BATCH = 400;
@Autowired
private PayPostcodeMapper payPostcodeMapper;
private final PayPostcodeImporter importer = new PayPostcodeImporter();
@Override
public List<PayPostcodeGroup> selectGroupList(PayPostcodeGroup query)
{
return payPostcodeMapper.selectGroupList(query);
}
@Override
public PayPostcodeGroup selectGroupById(Long groupId)
{
return payPostcodeMapper.selectGroupById(groupId);
}
@Override
public int updateGroup(PayPostcodeGroup group)
{
if (group == null || group.getGroupId() == null)
{
throw new ServiceException("请选择要修改的邮编配置");
}
if (payPostcodeMapper.selectGroupById(group.getGroupId()) == null)
{
throw new ServiceException("邮编配置不存在或已删除");
}
group.setCoverage(normalizeCoverage(group.getCoverage()));
group.setChannelName(trimToEmpty(group.getChannelName()));
group.setCountryName(trimToEmpty(group.getCountryName()));
group.setCountryCode(trimToEmpty(group.getCountryCode()));
group.setServiceType(trimToEmpty(group.getServiceType()));
group.setCatalogCountry(trimToEmpty(group.getCatalogCountry()));
group.setProductName(trimToEmpty(group.getProductName()));
group.setQuoteSheet(trimToEmpty(group.getQuoteSheet()));
group.setFeeName(trimToEmpty(group.getFeeName()));
group.setFeeText(trimToEmpty(group.getFeeText()));
group.setBlockTitle(trimToEmpty(group.getBlockTitle()));
group.setRemark(trimToEmpty(group.getRemark()));
if (!"FEE".equals(group.getConfigKind()) && !"ZONE".equals(group.getConfigKind()))
{
throw new ServiceException("类型只能是附加费或邮编分区");
}
return payPostcodeMapper.updateGroup(group);
}
@Override
public List<PayPostcodeLine> selectLineList(PayPostcodeLine query)
{
return payPostcodeMapper.selectLineList(query);
}
@Override
@Transactional(rollbackFor = Exception.class)
public PayPostcodeImportResult importExcel(byte[] bytes, String filename, String username)
{
List<PayPostcodeGroup> groups = importer.parse(bytes, filename);
if (groups.isEmpty())
{
throw new ServiceException("没有识别到邮编或附加费邮编,请确认表头里有邮编、ZIP 或附加费");
}
Set<String> sheets = new LinkedHashSet<>();
for (PayPostcodeGroup group : groups)
{
sheets.add(group.getSheetName());
}
String md5 = groups.get(0).getFileMd5();
for (String sheet : sheets)
{
List<Long> oldIds = payPostcodeMapper.selectGroupIds(md5, sheet);
if (oldIds != null && !oldIds.isEmpty())
{
payPostcodeMapper.deleteLines(oldIds);
payPostcodeMapper.deleteGroups(oldIds);
}
}
int feeCount = 0;
int zoneCount = 0;
int lineCount = 0;
for (PayPostcodeGroup group : groups)
{
group.setCreateBy(username);
if ("FEE".equals(group.getConfigKind()))
{
feeCount++;
}
else
{
zoneCount++;
}
payPostcodeMapper.insertGroup(group);
List<PayPostcodeLine> lines = group.getLines();
for (PayPostcodeLine line : lines)
{
line.setGroupId(group.getGroupId());
}
for (int i = 0; i < lines.size(); i += BATCH)
{
payPostcodeMapper.insertLines(new ArrayList<>(lines.subList(i, Math.min(i + BATCH, lines.size()))));
}
lineCount += lines.size();
}
PayPostcodeImportResult result = new PayPostcodeImportResult();
result.setSourceFile(groups.get(0).getSourceFile());
result.setGroupCount(groups.size());
result.setFeeCount(feeCount);
result.setZoneCount(zoneCount);
result.setLineCount(lineCount);
result.setMessage("已导入 " + groups.size() + " 条配置,其中附加费 " + feeCount + " 条、邮编分区 " + zoneCount + " 条,邮编 " + lineCount + " 行");
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteByIds(Long[] groupIds)
{
if (groupIds == null || groupIds.length == 0)
{
return 0;
}
List<Long> ids = new ArrayList<>();
for (Long id : groupIds)
{
if (id != null)
{
ids.add(id);
}
}
if (ids.isEmpty())
{
return 0;
}
payPostcodeMapper.deleteLines(ids);
return payPostcodeMapper.deleteGroups(ids);
}
private static String trimToEmpty(String value)
{
return value == null ? "" : value.trim();
}
private static String normalizeCoverage(String value)
{
String coverage = trimToEmpty(value).toUpperCase(java.util.Locale.ROOT);
if (coverage.isEmpty() || "REACH".equals(coverage) || "UNREACH".equals(coverage) || "REMOTE".equals(coverage))
{
return coverage;
}
throw new ServiceException("邮编属性只能是可达、不可达或偏远");
}
}
@@ -107,6 +107,10 @@ public class PrcProfitReportServiceImpl implements IPrcProfitReportService
{
return;
}
if ("COMMISSION_MISSING".equals(query.getDetailType()))
{
throw new ServiceException("提成功能已暂停使用");
}
query.setOrderNos(splitFilterValues(query.getOrderNo(), "订单号"));
query.setCustomerCodes(splitFilterValues(query.getCustomerCode(), "客户代码"));
query.setCustomerNames(splitFilterValues(query.getCustomerName(), "客户名称"));
@@ -85,6 +85,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
limit 1
</select>
<select id="selectServiceChannelByName" parameterType="String" resultMap="ServiceChannelResult">
<include refid="selectChannelVo"/>
where channel_name = #{channelName}
limit 2
</select>
<select id="countProductChannelByCode" parameterType="String" resultType="int">
select count(1) from srv_product_channel where channel_code = #{channelCode}
</select>
@@ -0,0 +1,37 @@
<?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.ChannelProductMapMapper">
<select id="selectByChannelAndType" resultType="com.bms.price.domain.ChannelProductMap">
select id, channel_id as channelId, product_id as productId, price_set_id as priceSetId,
pay_type as payType, create_by as createBy, create_time as createTime,
update_by as updateBy, update_time as updateTime
from srv_channel_product_map
where channel_id = #{channelId} and pay_type = #{payType}
limit 1
</select>
<insert id="upsert">
insert into srv_channel_product_map (
channel_id, product_id, price_set_id, pay_type, create_by, create_time, update_by, update_time
) values (
#{channelId}, #{productId}, #{priceSetId}, #{payType}, #{createBy}, sysdate(), #{updateBy}, sysdate()
)
on duplicate key update
product_id = values(product_id),
price_set_id = values(price_set_id),
update_by = values(update_by),
update_time = sysdate()
</insert>
<delete id="deleteByChannelAndType">
delete from srv_channel_product_map
where channel_id = #{channelId} and pay_type = #{payType}
</delete>
<delete id="deleteByProductId">
delete from srv_channel_product_map where product_id = #{productId}
</delete>
</mapper>
@@ -0,0 +1,128 @@
<?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.PayBillMapper">
<select id="selectPayBillList" resultType="com.bms.price.domain.PayBill">
select bill_id as billId, bill_no as billNo, supplier_id as supplierId, supplier_name as supplierName,
currency, source_type as sourceType, period_start as periodStart, period_end as periodEnd,
order_count as orderCount, total_amount as totalAmount, status, void_reason as voidReason,
void_by as voidBy, void_time as voidTime, create_by as createBy, create_time as createTime, remark
from pay_bill
<where>
<if test="billNo != null and billNo != ''">and bill_no like concat('%', #{billNo}, '%')</if>
<if test="supplierName != null and supplierName != ''">and supplier_name like concat('%', #{supplierName}, '%')</if>
<if test="status != null and status != ''">and status = #{status}</if>
<if test="currency != null and currency != ''">and currency = #{currency}</if>
</where>
order by bill_id desc
</select>
<select id="selectPayBillById" resultType="com.bms.price.domain.PayBill">
select bill_id as billId, bill_no as billNo, supplier_id as supplierId, supplier_name as supplierName,
currency, source_type as sourceType, period_start as periodStart, period_end as periodEnd,
order_count as orderCount, total_amount as totalAmount, status, void_reason as voidReason,
void_by as voidBy, void_time as voidTime, create_by as createBy, create_time as createTime, remark
from pay_bill where bill_id = #{billId}
</select>
<select id="selectLines" resultType="com.bms.price.domain.PayBillLine">
select line_id as lineId, bill_id as billId, order_bill_id as orderBillId, order_no as orderNo,
lading_no as ladingNo, lading_fee_id as ladingFeeId, fee_name as feeName, amount, currency,
price_date as priceDate, version_no as versionNo, remote_flag as remoteFlag,
density_level as densityLevel, line_source as lineSource
from pay_bill_line where bill_id = #{billId} order by line_id
</select>
<select id="selectLogs" resultType="com.bms.price.domain.PayBillLog">
select log_id as logId, bill_id as billId, operation_type as operationType, from_status as fromStatus,
to_status as toStatus, operation_desc as operationDesc, operator_name as operatorName, create_time as createTime
from pay_bill_log where bill_id = #{billId} order by log_id
</select>
<select id="selectCandidateOrders" resultType="com.bms.price.domain.PayBillOrder">
select o.bill_id as billId, o.order_no as orderNo, o.tracking_no as trackingNo, o.biz_date as bizDate,
o.pay_total as payTotal, o.currency as currency, o.pieces as pieces, o.charge_weight as chargeWeight,
o.product_name as productName, o.country_code as countryCode, o.country_name as countryName,
o.dest_postcode as destPostcode, e.air_no_old as ladingNo, e.product_floor as channelName,
e.supplier_name as supplierName,
(select group_concat(concat(ifnull(nullif(trim(f.fee_name), ''), ifnull(f.fee_type, '费用')), ' ', format(f.amount, 2))
order by field(f.fee_type, 'BASIC_FREIGHT', 'REGISTRATION_FEE', 'SURCHARGE', 'REFUND'), f.fee_id separator ';')
from prc_pay_bill_fee f where f.bill_id = o.bill_id) as feeText,
(select b.bill_no from pay_bill_line l
inner join pay_bill b on b.bill_id = l.bill_id
where l.order_bill_id = o.bill_id and b.status &lt;&gt; 'VOID'
limit 1) as occupiedBillNo
from prc_order_bill o
left join prc_order_bill_ext e on e.bill_id = o.bill_id
<where>
<if test="orderNos != null and orderNos.size() &gt; 0">
and o.order_no in
<foreach collection="orderNos" item="no" open="(" separator="," close=")">#{no}</foreach>
</if>
<if test="beginDate != null and beginDate != ''">
and o.biz_date &gt;= #{beginDate}
</if>
<if test="endDate != null and endDate != ''">
and o.biz_date &lt; date_add(#{endDate}, interval 1 day)
</if>
<if test="ladingNos != null and ladingNos.size() &gt; 0">
and (
e.air_no_old in
<foreach collection="ladingNos" item="no" open="(" separator="," close=")">#{no}</foreach>
or e.lading_id in (
select lading_id from pay_lading where lading_no in
<foreach collection="ladingNos" item="no" open="(" separator="," close=")">#{no}</foreach>
)
)
</if>
</where>
order by o.biz_date, o.order_no
limit 2000
</select>
<select id="selectOrderFees" resultType="com.bms.price.domain.PrcPayBillFee">
select fee_id as feeId, bill_id as billId, fee_name as feeName, amount, currency, fee_source as feeSource
from prc_pay_bill_fee
where bill_id in
<foreach collection="billIds" item="id" open="(" separator="," close=")">#{id}</foreach>
order by bill_id, fee_id
</select>
<insert id="insertPayBill" useGeneratedKeys="true" keyProperty="billId">
insert into pay_bill (
bill_no, supplier_id, supplier_name, currency, source_type, period_start, period_end,
order_count, total_amount, status, create_by, create_time, remark
) values (
#{billNo}, #{supplierId}, #{supplierName}, #{currency}, #{sourceType}, #{periodStart}, #{periodEnd},
#{orderCount}, #{totalAmount}, 'DRAFT', #{createBy}, sysdate(), #{remark}
)
</insert>
<update id="updatePayBillStatus">
update pay_bill
set status = #{status},
void_reason = #{voidReason},
void_by = #{voidBy},
void_time = #{voidTime},
update_by = #{updateBy},
update_time = sysdate()
where bill_id = #{billId}
</update>
<insert id="insertLines">
insert into pay_bill_line (
bill_id, order_bill_id, order_no, lading_no, lading_fee_id, fee_name, amount, currency,
price_date, version_no, remote_flag, density_level, line_source
) values
<foreach collection="list" item="item" separator=",">
(#{item.billId}, #{item.orderBillId}, #{item.orderNo}, #{item.ladingNo}, #{item.ladingFeeId},
#{item.feeName}, #{item.amount}, #{item.currency}, #{item.priceDate}, #{item.versionNo},
#{item.remoteFlag}, #{item.densityLevel}, #{item.lineSource})
</foreach>
</insert>
<insert id="insertLog">
insert into pay_bill_log (bill_id, operation_type, from_status, to_status, operation_desc, operator_name, create_time)
values (#{billId}, #{operationType}, #{fromStatus}, #{toStatus}, #{operationDesc}, #{operatorName}, sysdate())
</insert>
</mapper>
@@ -0,0 +1,140 @@
<?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.PayPostcodeMapper">
<select id="selectGroupList" parameterType="com.bms.price.domain.PayPostcodeGroup" resultType="com.bms.price.domain.PayPostcodeGroup">
select group_id as groupId, source_file as sourceFile, file_md5 as fileMd5, sheet_name as sheetName,
block_title as blockTitle, config_kind as configKind, coverage, channel_name as channelName,
country_name as countryName, country_code as countryCode,
service_type as serviceType, catalog_country as catalogCountry, product_name as productName, quote_sheet as quoteSheet,
fee_name as feeName, fee_text as feeText, line_count as lineCount, create_by as createBy, create_time as createTime, remark
from pay_postcode_group
<where>
<if test="sourceFile != null and sourceFile != ''">and source_file like concat('%', #{sourceFile}, '%')</if>
<if test="sheetName != null and sheetName != ''">and sheet_name like concat('%', #{sheetName}, '%')</if>
<if test="countryName != null and countryName != ''">and (country_name like concat('%', #{countryName}, '%') or country_code like concat('%', #{countryName}, '%') or catalog_country like concat('%', #{countryName}, '%'))</if>
<if test="serviceType != null and serviceType != ''">and service_type like concat('%', #{serviceType}, '%')</if>
<if test="productName != null and productName != ''">and product_name like concat('%', #{productName}, '%')</if>
<if test="quoteSheet != null and quoteSheet != ''">and quote_sheet like concat('%', #{quoteSheet}, '%')</if>
<if test="configKind != null and configKind != ''">and config_kind = #{configKind}</if>
<if test="coverage != null and coverage != ''">and coverage = #{coverage}</if>
<if test="channelName != null and channelName != ''">and channel_name like concat('%', #{channelName}, '%')</if>
<if test="feeName != null and feeName != ''">and fee_name like concat('%', #{feeName}, '%')</if>
<if test="postcode != null and postcode != ''">
and exists (
select 1 from pay_postcode_line l
where l.group_id = pay_postcode_group.group_id and l.postcode like concat('%', #{postcode}, '%')
)
</if>
</where>
order by group_id desc
</select>
<select id="selectLineList" parameterType="com.bms.price.domain.PayPostcodeLine" resultType="com.bms.price.domain.PayPostcodeLine">
select line_id as lineId, group_id as groupId, country_name as countryName, country_code as countryCode,
region_name as regionName, region_name_cn as regionNameCn, city_name as cityName, region_code as regionCode,
zone_name as zoneName, postcode, postcode_raw as postcodeRaw, postcode_end as postcodeEnd,
extra_text as extraText, sort_no as sortNo
from pay_postcode_line
<where>
<if test="groupId != null">and group_id = #{groupId}</if>
<if test="postcode != null and postcode != ''">and postcode like concat('%', #{postcode}, '%')</if>
<if test="regionName != null and regionName != ''">and (region_name like concat('%', #{regionName}, '%') or region_name_cn like concat('%', #{regionName}, '%') or zone_name like concat('%', #{regionName}, '%'))</if>
</where>
order by sort_no asc, line_id asc
</select>
<insert id="insertGroup" parameterType="com.bms.price.domain.PayPostcodeGroup" useGeneratedKeys="true" keyProperty="groupId">
insert into pay_postcode_group (
source_file, file_md5, sheet_name, block_title, config_kind, coverage, channel_name, country_name, country_code,
service_type, catalog_country, product_name, quote_sheet,
fee_name, fee_text, line_count, create_by, create_time, remark
) values (
#{sourceFile}, #{fileMd5}, #{sheetName}, #{blockTitle}, #{configKind}, #{coverage}, #{channelName}, #{countryName}, #{countryCode},
#{serviceType}, #{catalogCountry}, #{productName}, #{quoteSheet},
#{feeName}, #{feeText}, #{lineCount}, #{createBy}, sysdate(), #{remark}
)
</insert>
<insert id="insertLines">
insert into pay_postcode_line (
group_id, country_name, country_code, region_name, region_name_cn, city_name, region_code,
zone_name, postcode, postcode_raw, postcode_end, extra_text, sort_no
) values
<foreach collection="list" item="item" separator=",">
(#{item.groupId}, #{item.countryName}, #{item.countryCode}, #{item.regionName}, #{item.regionNameCn},
#{item.cityName}, #{item.regionCode}, #{item.zoneName}, #{item.postcode}, #{item.postcodeRaw},
#{item.postcodeEnd}, #{item.extraText}, #{item.sortNo})
</foreach>
</insert>
<select id="selectGroupById" resultType="com.bms.price.domain.PayPostcodeGroup">
select group_id as groupId, source_file as sourceFile, file_md5 as fileMd5, sheet_name as sheetName,
block_title as blockTitle, config_kind as configKind, coverage, channel_name as channelName,
country_name as countryName, country_code as countryCode,
service_type as serviceType, catalog_country as catalogCountry, product_name as productName, quote_sheet as quoteSheet,
fee_name as feeName, fee_text as feeText, line_count as lineCount, create_by as createBy, create_time as createTime, remark
from pay_postcode_group
where group_id = #{groupId}
</select>
<update id="updateGroup" parameterType="com.bms.price.domain.PayPostcodeGroup">
update pay_postcode_group
set coverage = #{coverage},
channel_name = #{channelName},
country_name = #{countryName},
country_code = #{countryCode},
service_type = #{serviceType},
catalog_country = #{catalogCountry},
product_name = #{productName},
quote_sheet = #{quoteSheet},
config_kind = #{configKind},
fee_name = #{feeName},
fee_text = #{feeText},
block_title = #{blockTitle},
remark = #{remark}
where group_id = #{groupId}
</update>
<select id="selectGroupIds" resultType="long">
select group_id from pay_postcode_group where file_md5 = #{fileMd5} and sheet_name = #{sheetName}
</select>
<select id="selectGroupsByTokens" resultType="com.bms.price.domain.PayPostcodeGroup">
select group_id as groupId, block_title as blockTitle, channel_name as channelName,
product_name as productName, quote_sheet as quoteSheet, sheet_name as sheetName,
config_kind as configKind, fee_name as feeName, country_name as countryName, country_code as countryCode
from pay_postcode_group
where
<foreach collection="tokens" item="token" separator=" or ">
lower(concat_ws(' ', ifnull(channel_name, ''), ifnull(block_title, ''), ifnull(product_name, ''),
ifnull(quote_sheet, ''), ifnull(sheet_name, ''))) like concat('%', #{token}, '%')
</foreach>
</select>
<select id="selectRegionKeys" resultType="com.bms.price.domain.PayPostcodeLine">
select group_id as groupId, region_name as regionName, zone_name as zoneName
from pay_postcode_line
where group_id in
<foreach collection="groupIds" item="id" open="(" separator="," close=")">#{id}</foreach>
group by group_id, region_name, zone_name
</select>
<select id="selectLinesByGroups" resultType="com.bms.price.domain.PayPostcodeLine">
select line_id as lineId, group_id as groupId, region_name as regionName, zone_name as zoneName,
postcode, postcode_end as postcodeEnd
from pay_postcode_line
where group_id in
<foreach collection="groupIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</select>
<delete id="deleteLines">
delete from pay_postcode_line where group_id in
<foreach collection="groupIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<delete id="deleteGroups">
delete from pay_postcode_group where group_id in
<foreach collection="groupIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
</mapper>
@@ -0,0 +1,355 @@
<?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.PayProductMapper">
<resultMap type="com.bms.price.domain.PayProduct" id="PayProductResult">
<id property="productId" column="product_id"/>
<result property="productName" column="product_name"/>
<result property="productType" column="product_type"/>
<result property="dictValue" column="dict_value"/>
<result property="originId" column="origin_id"/>
<result property="channelId" column="channel_id"/>
<result property="versionNo" column="version_no"/>
<result property="countryScope" column="country_scope"/>
<result property="chargeType" column="charge_type"/>
<result property="extraPerKg" column="extra_per_kg"/>
<result property="serviceType" column="service_type"/>
<result property="classification" column="classification"/>
<result property="catalogRemark" column="catalog_remark"/>
<result property="updateContent" column="update_content"/>
<result property="weightLimit" column="weight_limit"/>
<result property="sheetName" column="sheet_name"/>
<result property="relatedSheet" column="related_sheet"/>
<result property="sourceFile" column="source_file"/>
<result property="fileMd5" column="file_md5"/>
<result property="supplierName" column="supplier_name"/>
<result property="effectiveDate" column="effective_date"/>
<result property="currency" column="currency"/>
<result property="serviceNote" column="service_note"/>
<result property="parseStatus" column="parse_status"/>
<result property="weightCount" column="weight_count"/>
<result property="surchargeCount" column="surcharge_count"/>
<result property="extraCount" column="extra_count"/>
<result property="versionProductId" column="version_product_id"/>
<result property="termId" column="term_id"/>
<result property="termStatus" column="term_status"/>
<result property="termStart" column="term_start"/>
<result property="termEnd" column="term_end"/>
<result property="createBy" column="created_by"/>
<result property="createTime" column="create_time"/>
<result property="updateBy" column="updated_by"/>
<result property="updateTime" column="update_time"/>
<result property="remark" column="remark"/>
</resultMap>
<sql id="payProductWhere">
<where>
<if test="templateOnly != null and templateOnly">
and (pay_product.channel_id is null or pay_product.channel_id = '')
</if>
<if test="currentOnly != null and currentOnly">
and pay_product.origin_id is null
</if>
<if test="productName != null and productName != ''">
and pay_product.product_name like concat('%', #{productName}, '%')
</if>
<if test="countryScope != null and countryScope != ''">
and pay_product.country_scope like concat('%', #{countryScope}, '%')
</if>
<if test="sheetName != null and sheetName != ''">
and pay_product.sheet_name like concat('%', #{sheetName}, '%')
</if>
<if test="sourceFile != null and sourceFile != ''">
and pay_product.source_file like concat('%', #{sourceFile}, '%')
</if>
<if test="supplierName != null and supplierName != ''">
and pay_product.supplier_name like concat('%', #{supplierName}, '%')
</if>
<if test="serviceType != null and serviceType != ''">
and pay_product.service_type like concat('%', #{serviceType}, '%')
</if>
<if test="parseStatus != null and parseStatus != ''">
and pay_product.parse_status = #{parseStatus}
</if>
<if test="effectiveBegin != null and effectiveBegin != ''">
and ifnull(t.effective_start, pay_product.effective_date) &gt;= #{effectiveBegin}
</if>
<if test="effectiveEnd != null and effectiveEnd != ''">
and ifnull(t.effective_start, pay_product.effective_date) &lt;= #{effectiveEnd}
</if>
<if test="surchargeName != null and surchargeName != ''">
and exists (
select 1 from pay_surcharge s
where s.product_id = pay_product.product_id
and (s.fee_name like concat('%', #{surchargeName}, '%')
or s.charge_text like concat('%', #{surchargeName}, '%')
or s.fee_desc like concat('%', #{surchargeName}, '%')
or s.period_text like concat('%', #{surchargeName}, '%'))
)
</if>
<if test="weightKeyword != null and weightKeyword != ''">
and exists (
select 1 from pay_weight_band b
where b.product_id = pay_product.product_id
and (b.weight_label like concat('%', #{weightKeyword}, '%')
or b.charge_name like concat('%', #{weightKeyword}, '%')
or b.zone_name like concat('%', #{weightKeyword}, '%')
or b.country_name like concat('%', #{weightKeyword}, '%')
or b.section_name like concat('%', #{weightKeyword}, '%')
or b.volume_text like concat('%', #{weightKeyword}, '%'))
)
</if>
</where>
</sql>
<select id="selectPayProductList" parameterType="com.bms.price.domain.PayProduct" resultMap="PayProductResult">
select pay_product.product_id, pay_product.product_name,
ifnull(cur.product_type, pay_product.product_type) as product_type, pay_product.dict_value, pay_product.origin_id,
pay_product.channel_id, ifnull(cur.version_no, pay_product.version_no) as version_no,
pay_product.country_scope, ifnull(cur.charge_type, pay_product.charge_type) as charge_type,
ifnull(cur.extra_per_kg, pay_product.extra_per_kg) as extra_per_kg, pay_product.service_type,
pay_product.classification, pay_product.catalog_remark, pay_product.update_content, pay_product.weight_limit,
pay_product.sheet_name, pay_product.related_sheet, ifnull(cur.source_file, pay_product.source_file) as source_file,
pay_product.file_md5,
pay_product.supplier_name, pay_product.effective_date, ifnull(cur.currency, pay_product.currency) as currency,
pay_product.service_note,
pay_product.parse_status, ifnull(cur.weight_count, pay_product.weight_count) as weight_count,
ifnull(cur.surcharge_count, pay_product.surcharge_count) as surcharge_count, pay_product.extra_count,
pay_product.created_by, pay_product.create_time, pay_product.updated_by, pay_product.update_time, pay_product.remark,
t.product_id as version_product_id,
t.term_id, t.status as term_status, t.effective_start as term_start, t.effective_end as term_end
from pay_product
left join pay_product_term t on t.term_id = (
select t2.term_id
from pay_product_term t2
inner join pay_product v on v.product_id = t2.product_id
where (v.channel_id is null or v.channel_id = '')
and (v.product_id = pay_product.product_id or v.origin_id = pay_product.product_id)
order by case when t2.status = '0' then 0 else 1 end, ifnull(v.version_no, 1) desc, t2.term_id desc
limit 1
)
left join pay_product cur on cur.product_id = t.product_id
<include refid="payProductWhere"/>
order by ifnull(t.effective_start, pay_product.effective_date) desc, pay_product.product_id desc
</select>
<select id="selectPayProductById" parameterType="long" resultMap="PayProductResult">
select product_id, product_name, product_type, dict_value, origin_id, channel_id, version_no, country_scope, charge_type, extra_per_kg, service_type,
classification, catalog_remark, update_content, weight_limit, sheet_name, related_sheet, source_file,
file_md5, supplier_name, effective_date, currency, service_note, parse_status, weight_count,
surcharge_count, extra_count, created_by, create_time, updated_by, update_time, remark
from pay_product
where product_id = #{productId}
</select>
<select id="selectExistingId" resultType="long">
select product_id from pay_product
where file_md5 = #{fileMd5} and sheet_name = #{sheetName} and product_name = #{productName}
limit 1
</select>
<insert id="insertPayProduct" parameterType="com.bms.price.domain.PayProduct" useGeneratedKeys="true" keyProperty="productId">
insert into pay_product (
product_name, product_type, dict_value, origin_id, channel_id, version_no, country_scope, charge_type, extra_per_kg, service_type, classification,
catalog_remark, update_content, weight_limit, sheet_name, related_sheet, source_file, file_md5,
supplier_name, effective_date, currency, service_note, parse_status, weight_count, surcharge_count,
extra_count, created_by, create_time, remark
) values (
#{productName}, #{productType}, #{dictValue}, #{originId}, #{channelId}, ifnull(#{versionNo}, 1), #{countryScope}, #{chargeType}, #{extraPerKg},
#{serviceType}, #{classification}, #{catalogRemark}, #{updateContent}, #{weightLimit}, #{sheetName},
#{relatedSheet}, #{sourceFile}, #{fileMd5}, #{supplierName}, #{effectiveDate}, #{currency},
#{serviceNote}, #{parseStatus}, #{weightCount}, #{surchargeCount}, #{extraCount}, #{createBy}, sysdate(),
#{remark}
)
</insert>
<update id="updatePayProduct" parameterType="com.bms.price.domain.PayProduct">
update pay_product
set product_name = #{productName},
product_type = #{productType},
dict_value = #{dictValue},
origin_id = #{originId},
channel_id = #{channelId},
version_no = ifnull(#{versionNo}, version_no),
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_md5 = #{fileMd5},
supplier_name = #{supplierName},
effective_date = #{effectiveDate},
currency = #{currency},
service_note = #{serviceNote},
parse_status = #{parseStatus},
weight_count = #{weightCount},
surcharge_count = #{surchargeCount},
extra_count = #{extraCount},
updated_by = #{updateBy},
update_time = sysdate(),
remark = #{remark}
where product_id = #{productId}
</update>
<select id="selectTemplateId" resultType="long">
select product_id from pay_product
where dict_value = #{dictValue}
and (channel_id is null or channel_id = '')
and origin_id is null
order by product_id
limit 1
</select>
<select id="selectTemplateOptions" resultMap="PayProductResult">
select p.product_id, p.product_name, p.dict_value, p.sheet_name, p.supplier_name, p.effective_date,
p.currency, ifnull(cur.version_no, p.version_no) as version_no
from pay_product p
left join pay_product_term t on t.term_id = (
select t2.term_id
from pay_product_term t2
inner join pay_product v on v.product_id = t2.product_id
where (v.channel_id is null or v.channel_id = '')
and (v.product_id = p.product_id or v.origin_id = p.product_id)
order by case when t2.status = '0' then 0 else 1 end, ifnull(v.version_no, 1) desc, t2.term_id desc
limit 1
)
left join pay_product cur on cur.product_id = t.product_id
where (p.channel_id is null or p.channel_id = '')
and p.origin_id is null
order by p.product_name
</select>
<select id="selectMaxVersion" resultType="int">
select ifnull(max(version_no), 1) from pay_product
where product_id = #{originId} or origin_id = #{originId}
</select>
<insert id="insertWeightBands">
insert into pay_weight_band (
product_id, version_no, section_name, country_name, country_code, zone_name, weight_label, weight_min, weight_max,
volume_text, charge_name, price, price_unit, sort_no
) values
<foreach collection="list" item="row" separator=",">
(#{row.productId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.countryName}, #{row.countryCode}, #{row.zoneName},
#{row.weightLabel}, #{row.weightMin}, #{row.weightMax}, #{row.volumeText}, #{row.chargeName}, #{row.price},
#{row.priceUnit}, #{row.sortNo})
</foreach>
</insert>
<insert id="insertSurcharges">
insert into pay_surcharge (
product_id, version_no, section_name, fee_name, zone_name, charge_text, amount, currency, fee_desc, period_text,
effective_start, effective_end, charge_timing, calc_rule, calc_param, sort_no
) values
<foreach collection="list" item="row" separator=",">
(#{row.productId}, ifnull(#{row.versionNo}, 1), #{row.sectionName}, #{row.feeName}, #{row.zoneName}, #{row.chargeText}, #{row.amount},
#{row.currency}, #{row.feeDesc}, #{row.periodText}, #{row.effectiveStart}, #{row.effectiveEnd},
#{row.chargeTiming}, ifnull(#{row.calcRule}, 'MANUAL'), #{row.calcParam}, #{row.sortNo})
</foreach>
</insert>
<insert id="insertExtraLines">
insert into pay_extra_line (product_id, line_type, line_text, sort_no) values
<foreach collection="list" item="row" separator=",">
(#{row.productId}, #{row.lineType}, #{row.lineText}, #{row.sortNo})
</foreach>
</insert>
<select id="selectWeightBands" resultType="com.bms.price.domain.PayWeightBand">
select band_id as bandId, product_id as productId, version_no as versionNo, section_name as sectionName, country_name as countryName,
country_code as countryCode, zone_name as zoneName, weight_label as weightLabel, weight_min as weightMin,
weight_max as weightMax, volume_text as volumeText, charge_name as chargeName, price, price_unit as priceUnit, sort_no as sortNo
from pay_weight_band
where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
order by product_id, sort_no, band_id
</select>
<select id="selectSurcharges" resultType="com.bms.price.domain.PaySurcharge">
select surcharge_id as surchargeId, product_id as productId, version_no as versionNo, section_name as sectionName, fee_name as feeName,
zone_name as zoneName, charge_text as chargeText, amount, currency, 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, sort_no as sortNo
from pay_surcharge
where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
order by product_id, sort_no, surcharge_id
</select>
<select id="selectExtraLines" resultType="com.bms.price.domain.PayExtraLine">
select line_id as lineId, product_id as productId, line_type as lineType, line_text as lineText, sort_no as sortNo
from pay_extra_line
where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
order by product_id, sort_no, line_id
</select>
<delete id="deleteWeightBands">
delete from pay_weight_band where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<delete id="deleteSurcharges">
delete from pay_surcharge where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<delete id="deleteExtraLines">
delete from pay_extra_line where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<delete id="deletePayProducts">
delete from pay_product where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<select id="selectFamilyIds" resultType="long">
select product_id from pay_product
where product_id = #{rootId} or origin_id = #{rootId}
</select>
<select id="selectEffectiveVersionId" resultType="long">
select v.product_id
from pay_product v
inner join pay_product_term t on t.product_id = v.product_id
where (v.channel_id is null or v.channel_id = '')
and (v.product_id = #{stableId} or v.origin_id = #{stableId})
and t.status = '0'
and (t.effective_start is null or t.effective_start &lt;= date(#{bizDate}))
and (t.effective_end is null or t.effective_end &gt;= date(#{bizDate}))
order by ifnull(v.version_no, 1) desc, v.product_id desc
limit 1
</select>
<select id="selectInactiveCovering" resultMap="PayProductResult">
select v.product_id, v.product_name, t.version_no
from pay_product v
inner join pay_product_term t on t.product_id = v.product_id
where (v.channel_id is null or v.channel_id = '')
and (v.product_id = #{stableId} or v.origin_id = #{stableId})
and ifnull(t.status, '1') &lt;&gt; '0'
and (t.effective_start is null or t.effective_start &lt;= date(#{bizDate}))
and (t.effective_end is null or t.effective_end &gt;= date(#{bizDate}))
order by ifnull(t.version_no, 1) desc, v.product_id desc
limit 1
</select>
<select id="selectLatestVersionId" resultType="long">
select v.product_id
from pay_product v
where (v.channel_id is null or v.channel_id = '')
and (v.product_id = #{stableId} or v.origin_id = #{stableId})
order by ifnull(v.version_no, 1) desc, v.product_id desc
limit 1
</select>
</mapper>
@@ -0,0 +1,86 @@
<?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.PayProductTermMapper">
<select id="selectByDict" resultType="com.bms.price.domain.PayProductTerm">
select t.term_id as termId, t.dict_value as dictValue, t.product_id as productId, t.version_no as versionNo,
t.effective_start as effectiveStart, t.effective_end as effectiveEnd, t.status,
t.created_by as createBy, t.create_time as createTime, t.remark,
p.product_name as productName, p.source_file as sourceFile, p.weight_count as weightCount,
p.surcharge_count as surchargeCount, p.currency
from pay_product_term t
join pay_product p on p.product_id = t.product_id
where t.dict_value = #{dictValue}
and (p.channel_id is null or p.channel_id = '')
order by t.version_no desc, t.term_id desc
</select>
<select id="selectById" resultType="com.bms.price.domain.PayProductTerm">
select term_id as termId, dict_value as dictValue, product_id as productId, version_no as versionNo,
effective_start as effectiveStart, effective_end as effectiveEnd, status
from pay_product_term
where term_id = #{termId}
</select>
<select id="selectByProductId" resultType="com.bms.price.domain.PayProductTerm">
select term_id as termId, dict_value as dictValue, product_id as productId, version_no as versionNo,
effective_start as effectiveStart, effective_end as effectiveEnd, status
from pay_product_term
where product_id = #{productId}
limit 1
</select>
<insert id="insertTerm" parameterType="com.bms.price.domain.PayProductTerm" useGeneratedKeys="true" keyProperty="termId">
insert into pay_product_term (
dict_value, product_id, version_no, effective_start, effective_end, status, created_by, create_time, remark
) values (
#{dictValue}, #{productId}, ifnull(#{versionNo}, 1), #{effectiveStart}, #{effectiveEnd},
ifnull(#{status}, '0'), #{createBy}, sysdate(), #{remark}
)
</insert>
<update id="updatePeriod">
update pay_product_term
set effective_start = #{effectiveStart},
effective_end = #{effectiveEnd},
updated_by = #{updateBy},
update_time = sysdate()
where term_id = #{termId}
</update>
<update id="updateStatus">
update pay_product_term
set status = #{status},
updated_by = #{updateBy},
update_time = sysdate()
where term_id = #{termId}
</update>
<update id="closeOpenTerms">
update pay_product_term
set status = '1',
effective_end = case
when effective_end is not null and effective_end &lt; #{endDate} then effective_end
when effective_start is not null and effective_start &gt; #{endDate} then effective_start
else #{endDate}
end,
updated_by = #{updateBy},
update_time = sysdate()
where dict_value = #{dictValue}
and status = '0'
</update>
<delete id="deleteByProductIds">
delete from pay_product_term where product_id in
<foreach collection="productIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
<update id="updateProductEffectiveDate">
update pay_product
set effective_date = #{effectiveDate},
update_time = sysdate()
where product_id = #{productId}
</update>
</mapper>
@@ -942,6 +942,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where bill_id = #{billId}
</update>
<update id="updatePayTotal">
update prc_order_bill
set pay_total = #{payTotal},
update_time = sysdate()
where bill_id = #{billId}
</update>
<update id="updateOrderBillAmounts">
update prc_order_bill
set surcharge_amount = #{surchargeAmount},
@@ -44,6 +44,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</insert>
<delete id="deleteByBillId" parameterType="String">
delete from prc_pay_bill_fee where bill_id = #{billId}
delete from prc_pay_bill_fee
where bill_id = #{billId}
and (fee_source is null or fee_source = '' or fee_source = 'CALC')
</delete>
</mapper>