统一修改ruoyi->bms

This commit is contained in:
caojia
2026-09-28 10:02:38 +08:00
parent 341afc4111
commit 0408b6c35e
639 changed files with 2776 additions and 2807 deletions
@@ -0,0 +1,57 @@
package com.bms.price.service;
import java.util.Date;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.bms.price.domain.BmsProduct;
import com.bms.price.domain.BmsProductCountry;
/**
* 产品列表 服务层
*
* @author bms
*/
public interface IBmsProductService
{
public BmsProduct selectBmsProductById(Long productId);
public List<BmsProduct> selectBmsProductList(BmsProduct product);
public int insertBmsProduct(BmsProduct product);
public int updateBmsProduct(BmsProduct product);
/**
* 修改产品国家明细行(重量段/运费/挂号费/尺寸/尾程/时效/生效失效日期),状态按日期重算
*/
public int updateBmsProductCountry(BmsProductCountry country);
public int deleteBmsProductByIds(Long[] productIds);
public List<BmsProductCountry> selectCountryListByProductCode(String productCode);
public List<BmsProductCountry> selectCountryListByProductCode(String productCode, Date effectiveDate);
public List<BmsProductCountry> selectCountryListByProductCode(String productCode, Date effectiveDate, Long productId);
/**
* 导入威狮B2C小包价表:产品目录 + 各产品 sheet 国家×重量段(含运费/挂号费/尺寸/尾程/时效)
*
* @param file 上传的 xlsx/xls
* @param updateSupport unused; same code+date never overwrites, different date always inserts
* @param operName 操作人
* @return 导入结果说明(可含 HTML 换行)
*/
public String importProductExcel(MultipartFile file, boolean updateSupport, String operName) throws Exception;
/**
* Import a single-country cost sheet (catalog + product weight rows).
* The destination country is chosen by the user, not read from the sheet.
*
* @param file uploaded xlsx/xls
* @param countryCode ISO country code (bms_region.country_code_2)
* @param operName operator
* @return HTML result message
*/
public String importCountryProductExcel(MultipartFile file, String countryCode, String operName) throws Exception;
}
@@ -0,0 +1,25 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PrcBillingTask;
import com.bms.price.domain.vo.BillingTaskSubmitRequest;
public interface IPrcBillingTaskService
{
/** Warehouse inbound: create task + publish MQ. */
PrcBillingTask submitInbound(BillingTaskSubmitRequest request);
/** MQ consumer entry: Oracle query -> bill -> writeback success status. */
void processTask(Long taskId);
PrcBillingTask getById(Long taskId);
/** Admin list (failed / waiting / filter). */
List<PrcBillingTask> selectList(PrcBillingTask query);
/**
* Manual resync: reset to WAITING then process again.
* @return task after retry
*/
PrcBillingTask retryTask(Long taskId);
}
@@ -0,0 +1,68 @@
package com.bms.price.service;
import java.math.BigDecimal;
import java.util.List;
import com.bms.price.domain.PrcOrderBill;
import com.bms.price.domain.vo.AddIncomeBillFeeRequest;
import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest;
import com.bms.price.domain.vo.TmsPriceBillRequest;
import com.bms.price.domain.vo.TmsPriceQuoteResult;
import com.bms.price.domain.vo.TmsOrderBillStatusResult;
/** 订单计费:落库 prc_order_bill + prc_income_bill/+fee + prc_pay_bill/+fee。 */
public interface IPrcOrderBillService
{
TmsPriceQuoteResult bill(TmsPriceBillRequest request);
/**
* 退费:保留原费用明细,在 prc_income_bill_fee 增加一条负金额(feeType=REFUND),重算应收并回补余额。
*
* @param orderNo 订单号
*/
TmsPriceQuoteResult refund(String orderNo);
List<PrcOrderBill> selectBillList(PrcOrderBill query);
/** 订单号下拉搜索选项(模糊匹配,去重限量) */
List<String> listOrderNoOptions(String keyword);
/** 运单号下拉搜索选项(模糊匹配,去重限量) */
List<String> listTrackingNoOptions(String keyword);
/** 转单号下拉搜索选项(模糊匹配,去重限量) */
List<String> listTransferNoOptions(String keyword);
TmsPriceQuoteResult getBill(String billId);
TmsPriceQuoteResult getBillByOrderNo(String orderNo);
/** Manual add income fee from surcharge config; refresh receivable totals. */
TmsPriceQuoteResult addIncomeFee(AddIncomeBillFeeRequest request);
/** Update manual UNBILLED income fee; refresh receivable totals. */
TmsPriceQuoteResult updateIncomeFee(UpdateIncomeBillFeeRequest request);
/** Delete manual UNBILLED income fee; refresh receivable totals. */
TmsPriceQuoteResult deleteIncomeFee(String feeId);
/** 提供给 TMS 判断订单是否可以继续处理的客户状态查询。 */
TmsOrderBillStatusResult queryTmsOrderBillStatus(String identifier);
/**
* 余额充值等审核入账后调用(异步):按客户代码查询欠费(ARREARS)订单,
* 按下单时间升序依次更为 NORMAL;更新日志写入 ES(logType=ARREARS)。
*
* @param customerCode 客户代码
* @param amount 入账金额(依次抵扣订单 income_total,通常为本位币CNY)
*/
void clearCreditArrearsByAmount(String customerCode, BigDecimal amount);
/**
* 计费列表手动更新欠费状态:仅 ARREARS 可更为 NORMAL。
* 校验逻辑与计费同步额度一致(客户正常 + 有足够可用额度),不通过则拒绝。
*
* @param billId 订单计费ID
* @return 更新后的计费结果
*/
TmsPriceQuoteResult updateCreditArrearsStatus(String billId);
}
@@ -0,0 +1,99 @@
package com.bms.price.service;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.bms.price.domain.PrcCustomerPriceSet;
import com.bms.price.domain.PrcPriceSet;
import com.bms.price.domain.PrcPriceSetCheckItem;
import com.bms.price.domain.PrcPriceSetDetail;
import com.bms.price.domain.PrcPriceSetSaveTask;
/**
* 价格套餐 服务层
*
* @author bms
*/
public interface IPrcPriceSetService
{
public PrcPriceSet selectPrcPriceSetById(Long priceSetId);
public List<PrcPriceSet> selectPrcPriceSetList(PrcPriceSet set);
public Long savePrcPriceSet(PrcPriceSet set);
public int updatePrcPriceSet(PrcPriceSet set);
public int deletePrcPriceSetByIds(Long[] priceSetIds);
public int submit(Long priceSetId, String updateBy);
public int approve(Long priceSetId, String updateBy);
public int reject(Long priceSetId, String updateBy);
public int disable(Long priceSetId, String updateBy);
/** Re-enable a stopped package (S -> E). */
public int enable(Long priceSetId, String updateBy);
/**
* Activate a draft or reschedule a pending version with user-specified effectiveTime.
* effectiveTime &lt;= now -&gt; EFFECT; future -&gt; PENDING(3). Then refresh 0/3/1/2.
*/
public int activateVersion(Long psvId, Date effectiveTime, String updateBy);
/**
* Copy source version into a new draft version under the same package:
* insert prc_price_set_version + copy prc_price_set_detail (+ surcharge snapshots).
* @return new psvId
*/
public Long copyVersion(Long sourcePsvId, String createBy);
/** Delete a draft version and its details / surcharge snapshots */
public int deleteVersion(Long psvId, String updateBy);
public Long copy(Long priceSetId, String createBy);
public List<PrcPriceSetCheckItem> validate(PrcPriceSet set);
public List<PrcPriceSetDetail> selectDetailList(PrcPriceSetDetail detail);
/**
* 版本明细搜索下拉选项:按版本去重后的 产品/国家/分区 列表。
* 返回 { products: [{code,name}], countries: [{code,name}], zones: [{code,name}] }
*/
public Map<String, Object> selectDetailOptions(Long psvId);
public int insertDetail(PrcPriceSetDetail detail);
public int updateDetail(PrcPriceSetDetail detail);
public int deleteDetailByIds(Long[] psdIds);
public List<PrcCustomerPriceSet> selectCustomerList(PrcCustomerPriceSet customer);
public int insertCustomer(PrcCustomerPriceSet customer);
public int updateCustomer(PrcCustomerPriceSet customer);
public int deleteCustomerByIds(Long[] customerPricesetIds);
/** chain expire_time = next version effective_time, then refresh 0/3/1/2 */
public int refreshAllVersionStatus();
/** submit async save, return taskId immediately */
public String submitSaveAsync(PrcPriceSet set);
/**
* Submit async batch write of detail surcharge snapshots (product-level pick can hit many rows).
* @return taskId for polling getSaveTask
*/
public String submitDetailSurchargeAsync(List<PrcPriceSetDetail> details);
/** Sync batch update of detail surcharge snapshots */
public int batchUpdateDetailSurcharges(List<PrcPriceSetDetail> details);
/** poll async save progress */
public PrcPriceSetSaveTask getSaveTask(String taskId);
}
@@ -0,0 +1,24 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PrcSurchargeRel;
/**
* 附加费与产品/明细关联 服务层
*
* @author bms
*/
public interface IPrcSurchargeRelService
{
/** 查询某产品已关联的附加费(含附加费与明细展示字段) */
public List<PrcSurchargeRel> selectRelByProductCode(String productCode);
public List<PrcSurchargeRel> selectPrcSurchargeRelList(PrcSurchargeRel rel);
public int insertPrcSurchargeRel(PrcSurchargeRel rel);
public int deletePrcSurchargeRelByIds(Long[] relIds);
/** 删除产品时清理其关联 */
public int deleteRelByProductCodes(String[] productCodes);
}
@@ -0,0 +1,29 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PrcSurcharge;
/**
* 附加费配置 服务层
*
* 附加费为计费主数据:编辑=原记录作废+插入新记录(版本化),删除=仅作废不物理删除。
*
* @author bms
*/
public interface IPrcSurchargeService
{
public PrcSurcharge selectPrcSurchargeById(Long scId);
public List<PrcSurcharge> selectPrcSurchargeList(PrcSurcharge surcharge);
public int insertPrcSurcharge(PrcSurcharge surcharge);
/** 版本化更新:原记录作废(ss_code=vd),插入新记录,产品关联重指向新记录;状态继承原记录 */
public int updatePrcSurcharge(PrcSurcharge surcharge);
/** 停用(原地修改 ss_code=of,不新增数据) */
public int disablePrcSurchargeByIds(Long[] scIds, String operator);
/** 启用(原地修改 ss_code=on,不新增数据) */
public int enablePrcSurchargeByIds(Long[] scIds, String operator);
}
@@ -0,0 +1,33 @@
package com.bms.price.service;
import java.util.List;
import com.bms.price.domain.PrcWeightSectionDetail;
import com.bms.price.domain.PrcWeightSectionTemplate;
/**
* 重量段管理 服务层
*
* @author bms
*/
public interface IPrcWeightService
{
public PrcWeightSectionTemplate selectPrcWeightSectionTemplateById(Long wstId);
public List<PrcWeightSectionTemplate> selectPrcWeightSectionTemplateList(PrcWeightSectionTemplate template);
public int insertPrcWeightSectionTemplate(PrcWeightSectionTemplate template);
public int updatePrcWeightSectionTemplate(PrcWeightSectionTemplate template);
public int deletePrcWeightSectionTemplateByIds(Long[] wstIds);
public PrcWeightSectionDetail selectPrcWeightSectionDetailById(Long wsdId);
public List<PrcWeightSectionDetail> selectPrcWeightSectionDetailList(PrcWeightSectionDetail detail);
public int insertPrcWeightSectionDetail(PrcWeightSectionDetail detail);
public int updatePrcWeightSectionDetail(PrcWeightSectionDetail detail);
public int deletePrcWeightSectionDetailByIds(Long[] wsdIds);
}
@@ -0,0 +1,39 @@
package com.bms.price.service;
import java.util.List;
import org.springframework.web.multipart.MultipartFile;
import com.bms.price.domain.PrcZone;
import com.bms.price.domain.PrcZoneDetail;
import com.bms.price.domain.PrcZoneRegion;
public interface IPrcZoneService
{
public PrcZoneRegion selectZoneRegionById(Long regionId);
public List<PrcZoneRegion> selectZoneRegionList(PrcZoneRegion query);
/** Delete all zones (and details) for the given bms_region ids */
public int deleteZonesByRegionIds(Long[] regionIds);
public PrcZone selectPrcZoneById(Long zoneId);
public List<PrcZone> selectPrcZoneList(PrcZone zone);
public int insertPrcZone(PrcZone zone);
public int updatePrcZone(PrcZone zone);
public int deletePrcZoneByIds(Long[] zoneIds);
public PrcZoneDetail selectPrcZoneDetailById(Long detailId);
public List<PrcZoneDetail> selectPrcZoneDetailList(PrcZoneDetail detail);
public int insertPrcZoneDetail(PrcZoneDetail detail);
public int updatePrcZoneDetail(PrcZoneDetail detail);
public int deletePrcZoneDetailByIds(Long[] detailIds);
public String importZoneExcel(MultipartFile file, String operName) throws Exception;
}
@@ -0,0 +1,30 @@
package com.bms.price.service;
import com.bms.price.domain.PrcQuote;
import com.bms.price.domain.vo.TmsPriceQuoteRequest;
import com.bms.price.domain.vo.TmsPriceQuoteResult;
import java.util.List;
/**
* TMS freight quote / bill calc service (shared by console trial and upstream bill).
*
* Match order:
* 1) Customer binds (not stopped), bind_priority ASC
* 2) Active package version at bill-time end-of-day (effective_time / expire_time)
* 3) FIXED detail override, else DISCOUNT on that version
* 4) Else bms_product_country default
* Package hit uses version surcharge snapshot when present; else product library.
*
* @author bms
*/
public interface ITmsPriceQuoteService
{
TmsPriceQuoteResult quote(TmsPriceQuoteRequest request);
/** Calc only, no prc_quote persist. Used by receivable billing. */
TmsPriceQuoteResult calculate(TmsPriceQuoteRequest request);
List<PrcQuote> selectQuoteList(PrcQuote query);
TmsPriceQuoteResult getQuote(String quoteId);
}
@@ -0,0 +1,410 @@
package com.bms.price.service.impl;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.StringUtils;
import com.bms.price.config.BillingProperties;
import com.bms.price.domain.PrcBillingTask;
import com.bms.price.domain.PrcOrderBill;
import com.bms.price.domain.vo.BillingTaskSubmitRequest;
import com.bms.price.domain.vo.TmsPriceBillRequest;
import com.bms.price.domain.vo.TmsPriceQuoteResult;
import com.bms.price.enums.BillTaskSuccessStatus;
import com.bms.price.enums.BillTaskType;
import com.bms.price.mapper.PrcBillingTaskMapper;
import com.bms.price.mapper.PrcOrderBillMapper;
import com.bms.price.mq.BillingTaskMessage;
import com.bms.price.mq.IBillingTaskMqPublisher;
import com.bms.price.oracle.OracleOrderBillMapper;
import com.bms.price.oracle.OracleUpstreamOrderService;
import com.bms.price.service.IPrcBillingTaskService;
import com.bms.price.service.IPrcOrderBillService;
@Service
public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
{
private static final Logger log = LoggerFactory.getLogger(PrcBillingTaskServiceImpl.class);
@Autowired
private PrcBillingTaskMapper billingTaskMapper;
@Autowired
private PrcOrderBillMapper orderBillMapper;
@Autowired
private IBillingTaskMqPublisher billingTaskMqPublisher;
@Autowired
private OracleUpstreamOrderService oracleUpstreamOrderService;
@Autowired
private OracleOrderBillMapper oracleOrderBillMapper;
@Autowired
private IPrcOrderBillService prcOrderBillService;
@Autowired
private BillingProperties billingProperties;
@Override
@Transactional(rollbackFor = Exception.class)
public PrcBillingTask submitInbound(BillingTaskSubmitRequest request)
{
if (request == null)
{
throw new ServiceException("billing task request required");
}
String transferNo = trim(request.getTransferNo());
String trackingNo = trim(request.getTrackingNo());
String orderNo = trim(request.getOrderNo());
Date billTime = request.getBillTime();
if (StringUtils.isEmpty(transferNo) && StringUtils.isEmpty(trackingNo) && StringUtils.isEmpty(orderNo))
{
throw new ServiceException("transferNo or trackingNo or orderNo required");
}
if (billTime == null)
{
throw new ServiceException("billTime required (billing time from mobile)");
}
BillTaskType type = BillTaskType.fromCode(request.getBillType());
if (type == null)
{
type = resolveAutoType(transferNo, trackingNo, orderNo);
}
PrcBillingTask task = new PrcBillingTask();
task.setTransferNo(transferNo);
task.setTrackingNo(trackingNo);
task.setOrderNo(orderNo);
task.setBillTime(billTime);
task.setBillType(type.getCode());
task.setSuccessStatus(BillTaskSuccessStatus.WAITING.getCode());
task.setCreateBy("tms");
billingTaskMapper.insertPrcBillingTask(task);
billingTaskMqPublisher.publish(new BillingTaskMessage(task.getTaskId()));
log.info("billing task submitted taskId={} type={} billTime={} transferNo={} trackingNo={} orderNo={}",
task.getTaskId(), task.getBillType(), billTime, transferNo, trackingNo, orderNo);
return task;
}
private static final int BILL_DB_RETRY_MAX = 3;
private static final long BILL_DB_RETRY_BASE_MS = 80L;
@Override
public void processTask(Long taskId)
{
if (taskId == null)
{
return;
}
PrcBillingTask task = billingTaskMapper.selectPrcBillingTaskById(taskId);
if (task == null)
{
log.warn("billing task not found taskId={}", taskId);
return;
}
if (BillTaskSuccessStatus.SUCCESS.getCode().equals(task.getSuccessStatus()))
{
log.info("billing task already success taskId={}", taskId);
return;
}
try
{
if (task.getBillTime() == null)
{
throw new ServiceException("task billTime empty (billing time from mobile required)");
}
BillTaskType type = BillTaskType.fromCode(task.getBillType());
if (BillTaskType.REFUND == type)
{
String refundOrderNo = resolveRefundOrderNo(task);
log.info("billing task REFUND path taskId={} orderNo={}", taskId, refundOrderNo);
TmsPriceQuoteResult result = refundWithDbRetry(taskId, refundOrderNo);
writeback(task, BillTaskSuccessStatus.SUCCESS, result == null ? null : result.getBillId(),
null);
return;
}
Map<String, Object> row = oracleUpstreamOrderService.findMainOrder(
task.getTransferNo(), task.getOrderNo(), task.getTrackingNo());
if (row == null || row.isEmpty())
{
throw new ServiceException("Oracle main order not found");
}
TmsPriceBillRequest billReq = oracleOrderBillMapper.toBillRequest(row,
billingProperties.getOracle().getDefaultProductCode());
if (billReq == null || StringUtils.isEmpty(billReq.getOrderNo()))
{
throw new ServiceException("Oracle order mapped orderNo empty");
}
if (StringUtils.isEmpty(billReq.getCustomerCode()))
{
throw new ServiceException("Oracle order mapped customerCode empty");
}
if (StringUtils.isEmpty(billReq.getProductCode()))
{
throw new ServiceException("Oracle order mapped productCode empty");
}
if (billReq.getWeight() == null || billReq.getWeight().signum() <= 0)
{
throw new ServiceException("Oracle order mapped weight invalid");
}
// Identity keys from mobile inbound are source of truth; do not replace with Oracle remap.
if (StringUtils.isNotEmpty(task.getTransferNo()))
{
billReq.setTransferNo(task.getTransferNo());
}
if (StringUtils.isNotEmpty(task.getTrackingNo()))
{
billReq.setTrackingNo(task.getTrackingNo());
}
if (StringUtils.isNotEmpty(task.getOrderNo()))
{
billReq.setOrderNo(task.getOrderNo());
}
// Billing date from task.bill_time (mobile), not Oracle CREATE_DATE.
billReq.setBizDate(new SimpleDateFormat("yyyy-MM-dd").format(task.getBillTime()));
TmsPriceQuoteResult result = billWithDbRetry(taskId, billReq);
writeback(task, BillTaskSuccessStatus.SUCCESS, result == null ? null : result.getBillId(),
null);
}
catch (RuntimeException e)
{
log.error("billing task failed taskId={}", taskId, e);
writeback(task, BillTaskSuccessStatus.FAILED, null, cut(e.getMessage(), 900));
}
}
/** Resolve orderNo for REFUND from task keys or existing bill (no Oracle quote required). */
private String resolveRefundOrderNo(PrcBillingTask task)
{
if (StringUtils.isNotEmpty(task.getOrderNo()))
{
return task.getOrderNo().trim();
}
if (StringUtils.isNotEmpty(task.getTransferNo()))
{
PrcOrderBill byTransfer = orderBillMapper.selectPrcOrderBillByTransferNo(task.getTransferNo().trim());
if (byTransfer != null && StringUtils.isNotEmpty(byTransfer.getOrderNo()))
{
return byTransfer.getOrderNo();
}
}
if (StringUtils.isNotEmpty(task.getTrackingNo()))
{
PrcOrderBill q = new PrcOrderBill();
q.setTrackingNo(task.getTrackingNo().trim());
List<PrcOrderBill> list = orderBillMapper.selectPrcOrderBillList(q);
if (list != null && !list.isEmpty() && StringUtils.isNotEmpty(list.get(0).getOrderNo()))
{
return list.get(0).getOrderNo();
}
}
throw new ServiceException("退费订单号无法解析");
}
private TmsPriceQuoteResult refundWithDbRetry(Long taskId, String orderNo)
{
RuntimeException last = null;
for (int attempt = 1; attempt <= BILL_DB_RETRY_MAX; attempt++)
{
try
{
return prcOrderBillService.refund(orderNo);
}
catch (RuntimeException e)
{
last = e;
if (attempt < BILL_DB_RETRY_MAX && isRetryableDbConflict(e))
{
long sleepMs = BILL_DB_RETRY_BASE_MS * attempt
+ (long) (Math.random() * BILL_DB_RETRY_BASE_MS);
log.warn("billing refund db conflict, retry taskId={} attempt={}/{} sleepMs={} msg={}",
taskId, attempt, BILL_DB_RETRY_MAX, sleepMs, cut(e.getMessage(), 200));
try
{
Thread.sleep(sleepMs);
}
catch (InterruptedException ie)
{
Thread.currentThread().interrupt();
throw e;
}
continue;
}
throw e;
}
}
throw last != null ? last : new ServiceException("billing refund retry exhausted");
}
/**
* Retry bill() on MySQL deadlock / lock wait (common when many tasks share one ctr_customer_account).
*/
private TmsPriceQuoteResult billWithDbRetry(Long taskId, TmsPriceBillRequest billReq)
{
RuntimeException last = null;
for (int attempt = 1; attempt <= BILL_DB_RETRY_MAX; attempt++)
{
try
{
return prcOrderBillService.bill(billReq);
}
catch (RuntimeException e)
{
last = e;
if (attempt < BILL_DB_RETRY_MAX && isRetryableDbConflict(e))
{
long sleepMs = BILL_DB_RETRY_BASE_MS * attempt
+ (long) (Math.random() * BILL_DB_RETRY_BASE_MS);
log.warn("billing task db conflict, retry taskId={} attempt={}/{} sleepMs={} msg={}",
taskId, attempt, BILL_DB_RETRY_MAX, sleepMs, cut(e.getMessage(), 200));
try
{
Thread.sleep(sleepMs);
}
catch (InterruptedException ie)
{
Thread.currentThread().interrupt();
throw e;
}
continue;
}
throw e;
}
}
throw last != null ? last : new ServiceException("billing retry exhausted");
}
/** Deadlock / lock wait / duplicate account insert — safe to retry whole bill txn. */
static boolean isRetryableDbConflict(Throwable e)
{
Throwable t = e;
while (t != null)
{
String name = t.getClass().getName();
if (name.indexOf("DeadlockLoser") >= 0
|| name.indexOf("CannotAcquireLock") >= 0
|| name.indexOf("MySQLTransactionRollback") >= 0)
{
return true;
}
String msg = t.getMessage();
if (msg != null)
{
String m = msg.toLowerCase();
if (m.indexOf("deadlock") >= 0
|| m.indexOf("try restarting transaction") >= 0
|| m.indexOf("lock wait timeout") >= 0
|| m.indexOf("duplicate entry") >= 0)
{
return true;
}
}
t = t.getCause();
}
return false;
}
@Override
public PrcBillingTask getById(Long taskId)
{
return billingTaskMapper.selectPrcBillingTaskById(taskId);
}
@Override
public List<PrcBillingTask> selectList(PrcBillingTask query)
{
return billingTaskMapper.selectPrcBillingTaskList(query);
}
@Override
public PrcBillingTask retryTask(Long taskId)
{
if (taskId == null)
{
throw new ServiceException("taskId required");
}
PrcBillingTask task = billingTaskMapper.selectPrcBillingTaskById(taskId);
if (task == null)
{
throw new ServiceException("billing task not found");
}
String st = task.getSuccessStatus();
if (BillTaskSuccessStatus.SUCCESS.getCode().equals(st))
{
throw new ServiceException("SUCCESS task cannot retry");
}
// Reset to WAITING so UI reflects re-queue; then sync process (admin expects immediate result).
PrcBillingTask reset = new PrcBillingTask();
reset.setTaskId(taskId);
reset.setSuccessStatus(BillTaskSuccessStatus.WAITING.getCode());
reset.setBillId(null);
reset.setErrorMessage(null);
billingTaskMapper.updateBillingResult(reset);
processTask(taskId);
return billingTaskMapper.selectPrcBillingTaskById(taskId);
}
private BillTaskType resolveAutoType(String transferNo, String trackingNo, String orderNo)
{
PrcOrderBill existed = null;
if (StringUtils.isNotEmpty(orderNo))
{
existed = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
}
if (existed == null && StringUtils.isNotEmpty(transferNo))
{
existed = orderBillMapper.selectPrcOrderBillByTransferNo(transferNo);
}
if (existed == null && StringUtils.isNotEmpty(trackingNo))
{
PrcOrderBill q = new PrcOrderBill();
q.setTrackingNo(trackingNo);
java.util.List<PrcOrderBill> list = orderBillMapper.selectPrcOrderBillList(q);
if (list != null && !list.isEmpty())
{
existed = list.get(0);
}
}
return existed == null ? BillTaskType.PENDING : BillTaskType.REBILL;
}
/** Write back status/billId/error only; keep inbound transfer/tracking/order/billTime untouched. */
private void writeback(PrcBillingTask task, BillTaskSuccessStatus status, String billId,
String errorMessage)
{
PrcBillingTask upd = new PrcBillingTask();
upd.setTaskId(task.getTaskId());
upd.setSuccessStatus(status.getCode());
upd.setBillId(billId);
upd.setErrorMessage(errorMessage);
billingTaskMapper.updateBillingResult(upd);
}
private static String trim(String s)
{
return s == null ? null : s.trim();
}
private static String cut(String s, int max)
{
if (s == null)
{
return null;
}
return s.length() <= max ? s : s.substring(0, max);
}
}
@@ -0,0 +1,80 @@
package com.bms.price.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.MessageUtils;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.PrcSurchargeRel;
import com.bms.price.mapper.PrcSurchargeRelMapper;
import com.bms.price.service.IPrcSurchargeRelService;
/**
* 附加费与产品/明细关联 服务层实现
*
* @author bms
*/
@Service
public class PrcSurchargeRelServiceImpl implements IPrcSurchargeRelService
{
@Autowired
private PrcSurchargeRelMapper relMapper;
@Override
public List<PrcSurchargeRel> selectRelByProductCode(String productCode)
{
PrcSurchargeRel query = new PrcSurchargeRel();
query.setProductCode(productCode);
return relMapper.selectPrcSurchargeRelList(query);
}
@Override
public List<PrcSurchargeRel> selectPrcSurchargeRelList(PrcSurchargeRel rel)
{
return relMapper.selectPrcSurchargeRelList(rel);
}
@Override
public int insertPrcSurchargeRel(PrcSurchargeRel rel)
{
validate(rel);
return relMapper.insertPrcSurchargeRel(rel);
}
@Override
public int deletePrcSurchargeRelByIds(Long[] relIds)
{
if (relIds == null || relIds.length == 0)
{
return 0;
}
return relMapper.deletePrcSurchargeRelByIds(relIds);
}
@Override
public int deleteRelByProductCodes(String[] productCodes)
{
if (productCodes == null || productCodes.length == 0)
{
return 0;
}
return relMapper.deleteRelByProductCodes(productCodes);
}
private void validate(PrcSurchargeRel rel)
{
if (rel.getScId() == null)
{
throw new ServiceException(MessageUtils.message("price.surchargeRel.scId.empty"));
}
if (rel.getProductId() == null)
{
throw new ServiceException(MessageUtils.message("price.surchargeRel.productId.empty"));
}
if (StringUtils.isEmpty(rel.getProductCode()))
{
throw new ServiceException(MessageUtils.message("price.surchargeRel.productCode.empty"));
}
}
}
@@ -0,0 +1,185 @@
package com.bms.price.service.impl;
import java.util.List;
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.MessageUtils;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.PrcSurcharge;
import com.bms.price.mapper.PrcSurchargeMapper;
import com.bms.price.mapper.PrcSurchargeRelMapper;
import com.bms.price.service.IPrcSurchargeService;
/**
* 附加费配置 服务层实现
*
* 附加费为计费主数据,不做物理修改/删除:
* - 编辑 = 原记录作废(ss_code=vd)+ 插入新记录(状态继承),产品关联重指向新记录(保留变更历史)
* - 停用 = 原地修改 ss_code=of,不新增数据
*
* @author bms
*/
@Service
public class PrcSurchargeServiceImpl implements IPrcSurchargeService
{
/** 作废状态:区别于启用(on)/停用(of),列表默认不显示 */
public static final String SS_CODE_VOID = "vd";
@Autowired
private PrcSurchargeMapper surchargeMapper;
@Autowired
private PrcSurchargeRelMapper surchargeRelMapper;
@Override
public PrcSurcharge selectPrcSurchargeById(Long scId)
{
return surchargeMapper.selectPrcSurchargeById(scId);
}
@Override
public List<PrcSurcharge> selectPrcSurchargeList(PrcSurcharge surcharge)
{
return surchargeMapper.selectPrcSurchargeList(surcharge);
}
@Override
public int insertPrcSurcharge(PrcSurcharge surcharge)
{
validate(surcharge);
applyInsertDefaults(surcharge);
return surchargeMapper.insertPrcSurcharge(surcharge);
}
/**
* 版本化更新:内容无变化时返回 -1(不产生新版本),否则原记录作废 + 插入新记录 + 产品关联重指向。
*/
@Override
@Transactional
public int updatePrcSurcharge(PrcSurcharge surcharge)
{
validate(surcharge);
PrcSurcharge old = surchargeMapper.selectPrcSurchargeById(surcharge.getScId());
if (old == null)
{
throw new ServiceException("附加费不存在或已被作废,无法修改");
}
// 无实际变化:不置作废、不插新记录,返回 -1 由 Controller 提示"未修改"
if (isUnchanged(old, surcharge))
{
return -1;
}
String operator = StringUtils.isNotEmpty(surcharge.getUpdateBy()) ? surcharge.getUpdateBy() : surcharge.getCreateBy();
// 1. 原记录作废(保留历史)
surchargeMapper.updateSsCodeByIds(new Long[] { surcharge.getScId() }, SS_CODE_VOID, operator);
// 2. 按表单内容插入新记录(状态继承原记录,状态变更只能走列表停用操作)
PrcSurcharge fresh = new PrcSurcharge();
fresh.setScName(surcharge.getScName());
fresh.setFeeType(StringUtils.isNotEmpty(surcharge.getFeeType()) ? surcharge.getFeeType() : old.getFeeType());
fresh.setChargeType(surcharge.getChargeType());
fresh.setChargeValue(surcharge.getChargeValue());
fresh.setCurrency(surcharge.getCurrency());
fresh.setRoundMode(surcharge.getRoundMode());
fresh.setSsCode(old.getSsCode());
fresh.setCountryCode(surcharge.getCountryCode());
fresh.setScNote(surcharge.getScNote());
applyInsertDefaults(fresh);
fresh.setCreateBy(operator);
int rows = surchargeMapper.insertPrcSurcharge(fresh);
// 3. 产品关联重指向新记录,保持产品附加费配置不失效
surchargeRelMapper.rebindRelScId(surcharge.getScId(), fresh.getScId(), operator);
return rows;
}
/** 逐字段比对编辑内容与原记录是否一致(字符串 trim 后比较,数值统一 BigDecimal 比较) */
private boolean isUnchanged(PrcSurcharge old, PrcSurcharge updated)
{
return eq(old.getScName(), updated.getScName())
&& eq(old.getFeeType(), updated.getFeeType())
&& eq(old.getChargeType(), updated.getChargeType())
&& eqDecimal(old.getChargeValue(), updated.getChargeValue())
&& eq(old.getCurrency(), updated.getCurrency())
&& eq(old.getRoundMode(), updated.getRoundMode())
&& eq(old.getCountryCode(), updated.getCountryCode())
&& eq(old.getScNote(), updated.getScNote());
}
private boolean eq(String a, String b)
{
String sa = a == null ? "" : a.trim();
String sb = b == null ? "" : b.trim();
return sa.equals(sb);
}
private boolean eqDecimal(java.math.BigDecimal a, java.math.BigDecimal b)
{
if (a == null && b == null) return true;
if (a == null || b == null) return false;
return a.compareTo(b) == 0;
}
/**
* 停用(原地修改 ss_code=of,不新增数据、不动产品关联)。
*/
@Override
public int disablePrcSurchargeByIds(Long[] scIds, String operator)
{
return updateStatusByIds(scIds, "of", operator);
}
/**
* 启用(原地修改 ss_code=on,不新增数据)。
*/
@Override
public int enablePrcSurchargeByIds(Long[] scIds, String operator)
{
return updateStatusByIds(scIds, "on", operator);
}
private int updateStatusByIds(Long[] scIds, String ssCode, String operator)
{
if (scIds == null || scIds.length == 0)
{
return 0;
}
return surchargeMapper.updateSsCodeByIds(scIds, ssCode, operator);
}
private void applyInsertDefaults(PrcSurcharge surcharge)
{
if (StringUtils.isEmpty(surcharge.getSsCode()))
{
surcharge.setSsCode("on");
}
if (StringUtils.isEmpty(surcharge.getFeeType()))
{
surcharge.setFeeType("SURCHARGE");
}
if (StringUtils.isEmpty(surcharge.getRoundMode()))
{
surcharge.setRoundMode("HALF_UP");
}
}
private void validate(PrcSurcharge surcharge)
{
if (StringUtils.isEmpty(surcharge.getScName()))
{
throw new ServiceException(MessageUtils.message("price.surcharge.scName.empty"));
}
if (StringUtils.isEmpty(surcharge.getChargeType()))
{
throw new ServiceException(MessageUtils.message("price.surcharge.chargeType.empty"));
}
if (surcharge.getChargeValue() == null)
{
throw new ServiceException(MessageUtils.message("price.surcharge.chargeValue.empty"));
}
if (surcharge.getChargeValue().compareTo(java.math.BigDecimal.ZERO) < 0)
{
throw new ServiceException(MessageUtils.message("price.surcharge.chargeValue.negative"));
}
}
}
@@ -0,0 +1,117 @@
package com.bms.price.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.MessageUtils;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.PrcWeightSectionDetail;
import com.bms.price.domain.PrcWeightSectionTemplate;
import com.bms.price.mapper.PrcWeightSectionDetailMapper;
import com.bms.price.mapper.PrcWeightSectionTemplateMapper;
import com.bms.price.service.IPrcWeightService;
/**
* 重量段管理 服务层实现
*
* @author bms
*/
@Service
public class PrcWeightServiceImpl implements IPrcWeightService
{
@Autowired
private PrcWeightSectionTemplateMapper templateMapper;
@Autowired
private PrcWeightSectionDetailMapper detailMapper;
@Override
public PrcWeightSectionTemplate selectPrcWeightSectionTemplateById(Long wstId)
{
PrcWeightSectionTemplate template = templateMapper.selectPrcWeightSectionTemplateById(wstId);
if (template != null)
{
template.setDetailCount(detailMapper.countByWstId(wstId));
}
return template;
}
@Override
public List<PrcWeightSectionTemplate> selectPrcWeightSectionTemplateList(PrcWeightSectionTemplate template)
{
List<PrcWeightSectionTemplate> list = templateMapper.selectPrcWeightSectionTemplateList(template);
for (PrcWeightSectionTemplate item : list)
{
item.setDetailCount(detailMapper.countByWstId(item.getWstId()));
}
return list;
}
@Override
public int insertPrcWeightSectionTemplate(PrcWeightSectionTemplate template)
{
if (StringUtils.isEmpty(template.getSsCode()))
{
template.setSsCode("on");
}
return templateMapper.insertPrcWeightSectionTemplate(template);
}
@Override
public int updatePrcWeightSectionTemplate(PrcWeightSectionTemplate template)
{
// ss_code=on 时仍允许编辑(当前阶段)
return templateMapper.updatePrcWeightSectionTemplate(template);
}
@Override
public int deletePrcWeightSectionTemplateByIds(Long[] wstIds)
{
return templateMapper.deletePrcWeightSectionTemplateByIds(wstIds);
}
@Override
public PrcWeightSectionDetail selectPrcWeightSectionDetailById(Long wsdId)
{
return detailMapper.selectPrcWeightSectionDetailById(wsdId);
}
@Override
public List<PrcWeightSectionDetail> selectPrcWeightSectionDetailList(PrcWeightSectionDetail detail)
{
return detailMapper.selectPrcWeightSectionDetailList(detail);
}
@Override
public int insertPrcWeightSectionDetail(PrcWeightSectionDetail detail)
{
validateWeightRange(detail);
return detailMapper.insertPrcWeightSectionDetail(detail);
}
@Override
public int updatePrcWeightSectionDetail(PrcWeightSectionDetail detail)
{
validateWeightRange(detail);
return detailMapper.updatePrcWeightSectionDetail(detail);
}
@Override
public int deletePrcWeightSectionDetailByIds(Long[] wsdIds)
{
return detailMapper.deletePrcWeightSectionDetailByIds(wsdIds);
}
private void validateWeightRange(PrcWeightSectionDetail detail)
{
if (detail.getWlStartweight() == null || detail.getWlEndweight() == null)
{
throw new ServiceException(MessageUtils.message("price.weight.range.empty"));
}
if (detail.getWlStartweight().compareTo(detail.getWlEndweight()) >= 0)
{
throw new ServiceException(MessageUtils.message("price.weight.range.invalid"));
}
}
}
@@ -0,0 +1,892 @@
package com.bms.price.service.impl;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import com.bms.common.exception.ServiceException;
import com.bms.common.utils.MessageUtils;
import com.bms.common.utils.StringUtils;
import com.bms.price.domain.PrcZone;
import com.bms.price.domain.PrcZoneDetail;
import com.bms.price.domain.PrcZoneRegion;
import com.bms.price.mapper.PrcZoneDetailMapper;
import com.bms.price.mapper.PrcZoneMapper;
import com.bms.price.service.IPrcZoneService;
import com.bms.price.util.ZoneCodeAbbrevs;
import com.bms.system.domain.BmsRegion;
import com.bms.system.mapper.BmsRegionMapper;
/**
* Zone management: bms_region -> prc_zone -> postcode detail
*/
@Service
public class PrcZoneServiceImpl implements IPrcZoneService
{
private static final String SHEET_SUFFIX = "\u5206\u533a";
private static final String COL_POSTCODE = "\u90ae\u7f16";
private static final String COL_ZONE = "\u5206\u533a";
private static final Pattern ZONE_NUM = Pattern.compile("(\\d+)");
private static final int BATCH_SIZE = 500;
private static final DataFormatter CELL_FMT = new DataFormatter(Locale.ROOT);
@Autowired
private PrcZoneMapper zoneMapper;
@Autowired
private PrcZoneDetailMapper detailMapper;
@Autowired
private BmsRegionMapper regionMapper;
@Override
public PrcZoneRegion selectZoneRegionById(Long regionId)
{
return zoneMapper.selectZoneRegionById(regionId);
}
@Override
public List<PrcZoneRegion> selectZoneRegionList(PrcZoneRegion query)
{
return zoneMapper.selectZoneRegionList(query);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteZonesByRegionIds(Long[] regionIds)
{
if (regionIds == null || regionIds.length == 0)
{
return 0;
}
for (Long regionId : regionIds)
{
detailMapper.deletePrcZoneDetailByRegionId(regionId);
}
return zoneMapper.deletePrcZoneByRegionIds(regionIds);
}
@Override
public PrcZone selectPrcZoneById(Long zoneId)
{
PrcZone zone = zoneMapper.selectPrcZoneById(zoneId);
if (zone != null)
{
zone.setDetailCount(detailMapper.countByZoneId(zoneId));
}
return zone;
}
@Override
public List<PrcZone> selectPrcZoneList(PrcZone zone)
{
return zoneMapper.selectPrcZoneList(zone);
}
@Override
public int insertPrcZone(PrcZone zone)
{
if (zone.getRegionId() == null)
{
throw new ServiceException(MessageUtils.message("price.zone.region.required"));
}
BmsRegion region = regionMapper.selectRegionById(zone.getRegionId());
if (region == null)
{
throw new ServiceException(MessageUtils.message("price.zone.region.notFound"));
}
if (StringUtils.isEmpty(zone.getCountryCode()))
{
zone.setCountryCode(region.getCountryCode2());
}
else
{
zone.setCountryCode(zone.getCountryCode().trim().toUpperCase(Locale.ROOT));
}
if (StringUtils.isEmpty(zone.getCountryCode()))
{
throw new ServiceException(MessageUtils.message("price.zone.countryCode.empty"));
}
if (zone.getZoneSort() == null)
{
zone.setZoneSort(extractZoneSort(zone.getZoneCode()));
}
if (StringUtils.isNotEmpty(zone.getZoneCode()))
{
zone.setZoneCode(ZoneCodeAbbrevs.toAbbrev(zone.getZoneCode()));
}
if (StringUtils.isEmpty(zone.getZoneName()))
{
zone.setZoneName(buildZoneName(region.getCountryNameCn(), zone.getZoneCode()));
}
return zoneMapper.insertPrcZone(zone);
}
@Override
public int updatePrcZone(PrcZone zone)
{
if (zone.getRegionId() != null && StringUtils.isEmpty(zone.getCountryCode()))
{
BmsRegion region = regionMapper.selectRegionById(zone.getRegionId());
if (region != null)
{
zone.setCountryCode(region.getCountryCode2());
}
}
else if (StringUtils.isNotEmpty(zone.getCountryCode()))
{
zone.setCountryCode(zone.getCountryCode().trim().toUpperCase(Locale.ROOT));
}
if (StringUtils.isNotEmpty(zone.getZoneCode()))
{
zone.setZoneCode(ZoneCodeAbbrevs.toAbbrev(zone.getZoneCode()));
}
if (zone.getZoneSort() == null && StringUtils.isNotEmpty(zone.getZoneCode()))
{
zone.setZoneSort(extractZoneSort(zone.getZoneCode()));
}
return zoneMapper.updatePrcZone(zone);
}
@Override
@Transactional(rollbackFor = Exception.class)
public int deletePrcZoneByIds(Long[] zoneIds)
{
return zoneMapper.deletePrcZoneByIds(zoneIds);
}
@Override
public PrcZoneDetail selectPrcZoneDetailById(Long detailId)
{
return detailMapper.selectPrcZoneDetailById(detailId);
}
@Override
public List<PrcZoneDetail> selectPrcZoneDetailList(PrcZoneDetail detail)
{
return detailMapper.selectPrcZoneDetailList(detail);
}
@Override
public int insertPrcZoneDetail(PrcZoneDetail detail)
{
return detailMapper.insertPrcZoneDetail(detail);
}
@Override
public int updatePrcZoneDetail(PrcZoneDetail detail)
{
return detailMapper.updatePrcZoneDetail(detail);
}
@Override
public int deletePrcZoneDetailByIds(Long[] detailIds)
{
return detailMapper.deletePrcZoneDetailByIds(detailIds);
}
@Override
@Transactional(rollbackFor = Exception.class)
public String importZoneExcel(MultipartFile file, String operName) throws Exception
{
if (file == null || file.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.zone.import.fileEmpty"));
}
String filename = file.getOriginalFilename();
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xls")
|| filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")))
{
throw new ServiceException(MessageUtils.message("price.zone.import.fileType"));
}
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
if (regionByNameCn.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.zone.import.regionEmpty"));
}
int countryCount = 0;
int zoneCount = 0;
int detailCount = 0;
List<String> importedCountries = new ArrayList<>();
List<String> unmatched = new ArrayList<>();
try (InputStream is = file.getInputStream(); Workbook workbook = WorkbookFactory.create(is))
{
List<SheetImport> imports = findZoneImports(workbook);
if (imports.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.zone.import.noSheet"));
}
for (SheetImport item : imports)
{
Map<String, List<String>> zonePostcodes = item.zonePostcodes;
if (zonePostcodes == null || zonePostcodes.isEmpty())
{
continue;
}
BmsRegion region = resolveRegion(item.countryHint, regionByNameCn);
if (region == null)
{
unmatched.add(item.countryHint + "(" + item.sheetName + ")");
continue;
}
int[] counts = replaceRegionZones(region, zonePostcodes);
countryCount += 1;
zoneCount += counts[0];
detailCount += counts[1];
importedCountries.add(region.getCountryNameCn());
}
}
if (countryCount == 0)
{
if (!unmatched.isEmpty())
{
throw new ServiceException(MessageUtils.message("price.zone.import.regionNotFound",
String.join(",", unmatched)));
}
throw new ServiceException(MessageUtils.message("price.zone.import.noData"));
}
String msg = MessageUtils.message("price.zone.import.success", countryCount, zoneCount, detailCount,
String.join(",", importedCountries));
if (!unmatched.isEmpty())
{
msg = msg + " " + MessageUtils.message("price.zone.import.unmatched", String.join(",", unmatched));
}
return msg;
}
private int[] replaceRegionZones(BmsRegion region, Map<String, List<String>> zonePostcodes)
{
if (StringUtils.isEmpty(region.getCountryCode2()))
{
throw new ServiceException(MessageUtils.message("price.zone.countryCode.empty"));
}
Long regionId = region.getRegionId();
detailMapper.deletePrcZoneDetailByRegionId(regionId);
zoneMapper.deletePrcZoneByRegionId(regionId);
int zoneInserted = 0;
int detailInserted = 0;
List<Map.Entry<String, List<String>>> entries = new ArrayList<>(zonePostcodes.entrySet());
entries.sort(Comparator.comparingInt(e -> extractZoneSort(e.getKey())));
String countryName = region.getCountryNameCn();
for (Map.Entry<String, List<String>> entry : entries)
{
String zoneCode = ZoneCodeAbbrevs.toAbbrev(entry.getKey());
PrcZone zone = new PrcZone();
zone.setRegionId(regionId);
zone.setCountryCode(region.getCountryCode2());
zone.setZoneCode(zoneCode);
zone.setZoneName(buildZoneName(countryName, zoneCode));
zone.setZoneSort(extractZoneSort(entry.getKey()));
zoneMapper.insertPrcZone(zone);
zoneInserted++;
Set<String> uniquePostcodes = new LinkedHashSet<>(entry.getValue());
List<PrcZoneDetail> batch = new ArrayList<>(BATCH_SIZE);
for (String postcode : uniquePostcodes)
{
PrcZoneDetail detail = new PrcZoneDetail();
detail.setZoneId(zone.getZoneId());
detail.setPostcode(postcode);
batch.add(detail);
if (batch.size() >= BATCH_SIZE)
{
detailMapper.insertPrcZoneDetailBatch(batch);
detailInserted += batch.size();
batch = new ArrayList<>(BATCH_SIZE);
}
}
if (!batch.isEmpty())
{
detailMapper.insertPrcZoneDetailBatch(batch);
detailInserted += batch.size();
}
}
return new int[] { zoneInserted, detailInserted };
}
private static final class SheetImport
{
private final String sheetName;
private final String countryHint;
private final Map<String, List<String>> zonePostcodes;
private SheetImport(String sheetName, String countryHint, Map<String, List<String>> zonePostcodes)
{
this.sheetName = sheetName;
this.countryHint = countryHint;
this.zonePostcodes = zonePostcodes;
}
}
private List<SheetImport> findZoneImports(Workbook workbook)
{
List<SheetImport> list = new ArrayList<>();
for (int i = 0; i < workbook.getNumberOfSheets(); i++)
{
Sheet sheet = workbook.getSheetAt(i);
if (sheet == null)
{
continue;
}
String sheetName = StringUtils.trim(sheet.getSheetName());
if (StringUtils.isEmpty(sheetName))
{
continue;
}
String countryHint = guessCountryFromSheetName(sheetName);
Map<String, List<String>> parsed = parseSheetByName(sheet, sheetName);
if (parsed.isEmpty())
{
parsed = parseZonePostcodes(sheet);
}
if (parsed.isEmpty())
{
parsed = parseRemoteZonePostcodes(sheet);
}
if (parsed.isEmpty())
{
parsed = parseProvincePostcodes(sheet);
}
if (parsed.isEmpty())
{
parsed = parseCoveragePostcodes(sheet);
}
if (parsed.isEmpty())
{
parsed = parseUkSurchargePostcodes(sheet);
}
if (parsed.isEmpty())
{
continue;
}
if (StringUtils.isEmpty(countryHint))
{
countryHint = sheetName;
}
list.add(new SheetImport(sheetName, countryHint, parsed));
}
return list;
}
private String guessCountryFromSheetName(String sheetName)
{
String name = StringUtils.trim(sheetName);
if (name == null)
{
return null;
}
if (name.endsWith(SHEET_SUFFIX) && name.length() > SHEET_SUFFIX.length())
{
return StringUtils.trim(name.substring(0, name.length() - SHEET_SUFFIX.length()));
}
// strip common suffixes: remote surcharge / surcharge / postcode
String[] suffixes = {
"\u504f\u8fdc\u9644\u52a0\u8d39",
"\u9644\u52a0\u8d39",
"MR\u90ae\u7f16",
"\u90ae\u7f16"
};
for (String suffix : suffixes)
{
if (name.endsWith(suffix) && name.length() > suffix.length())
{
return StringUtils.trim(name.substring(0, name.length() - suffix.length()));
}
}
if (name.contains("\u52a0\u62ff\u5927"))
{
return "\u52a0\u62ff\u5927";
}
if (name.contains("\u6fb3\u5927\u5229\u4e9a"))
{
return "\u6fb3\u5927\u5229\u4e9a";
}
if (name.contains("\u82f1\u56fd"))
{
return "\u82f1\u56fd";
}
if (name.contains("\u65e5\u672c"))
{
return "\u65e5\u672c";
}
if (name.contains("\u6cd5\u56fd"))
{
return "\u6cd5\u56fd";
}
return null;
}
private Map<String, List<String>> parseSheetByName(Sheet sheet, String sheetName)
{
if (sheetName.endsWith(SHEET_SUFFIX))
{
return parseZonePostcodes(sheet);
}
if (sheetName.contains("\u52a0\u62ff\u5927"))
{
return parseRemoteZonePostcodes(sheet);
}
if (sheetName.contains("\u6cd5\u56fd") && sheetName.contains("\u90ae\u7f16"))
{
return parseCoveragePostcodes(sheet);
}
if (sheetName.contains("\u65e5\u672c"))
{
return parseProvincePostcodes(sheet);
}
if (sheetName.contains("\u82f1\u56fd"))
{
return parseUkSurchargePostcodes(sheet);
}
return new LinkedHashMap<>();
}
private List<Sheet> findZoneSheets(Workbook workbook)
{
return new ArrayList<>();
}
private String parseCountryNameFromSheet(String sheetName)
{
return guessCountryFromSheetName(sheetName);
}
/** Australia style: postcode + zone */
private Map<String, List<String>> parseZonePostcodes(Sheet sheet)
{
return parseTwoColZoneMap(sheet, true);
}
/** Canada style: remote postcode + remote zone */
private Map<String, List<String>> parseRemoteZonePostcodes(Sheet sheet)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int postcodeCol = -1;
int zoneCol = -1;
int headerRow = -1;
int lastRow = sheet.getLastRowNum();
for (int r = 0; r <= Math.min(lastRow, 20); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
String val = cellText(row.getCell(c));
if (val.contains("\u90ae\u7f16") && postcodeCol < 0)
{
postcodeCol = c;
headerRow = r;
}
if ((val.equals("\u504f\u8fdc\u5206\u533a") || val.equals(COL_ZONE) || val.contains("\u5206\u533a"))
&& !val.contains("\u9644\u52a0") && zoneCol < 0)
{
zoneCol = c;
headerRow = r;
}
}
if (postcodeCol >= 0 && zoneCol >= 0)
{
break;
}
}
if (postcodeCol < 0 || zoneCol < 0 || headerRow < 0)
{
return map;
}
fillTwoCol(map, sheet, headerRow, postcodeCol, zoneCol);
return map;
}
/** Japan style: province + postcode */
private Map<String, List<String>> parseProvincePostcodes(Sheet sheet)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int provinceCol = -1;
int postcodeCol = -1;
int headerRow = -1;
int lastRow = sheet.getLastRowNum();
for (int r = 0; r <= Math.min(lastRow, 10); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
String val = cellText(row.getCell(c));
if ((val.contains("\u5dde") || val.contains("\u7701") || "\u5dde\u7701".equals(val)) && provinceCol < 0)
{
provinceCol = c;
headerRow = r;
}
if (COL_POSTCODE.equals(val) || val.equals("\u90ae\u7f16"))
{
postcodeCol = c;
headerRow = r;
}
}
if (provinceCol >= 0 && postcodeCol >= 0)
{
break;
}
}
if (provinceCol < 0 || postcodeCol < 0 || headerRow < 0)
{
return map;
}
// zone = province, detail = postcode (swap vs AU)
for (int r = headerRow + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String zoneCode = normalizeCell(cellText(row.getCell(provinceCol)));
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)));
if (StringUtils.isEmpty(zoneCode) || StringUtils.isEmpty(postcode))
{
continue;
}
map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode);
}
return map;
}
/** France MR style: carrier/zone + coverage postcode */
private Map<String, List<String>> parseCoveragePostcodes(Sheet sheet)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int zoneCol = -1;
int postcodeCol = -1;
int headerRow = -1;
int lastRow = sheet.getLastRowNum();
for (int r = 0; r <= Math.min(lastRow, 10); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
String val = cellText(row.getCell(c));
if (val.contains("\u8986\u76d6\u90ae\u7f16") || val.equals(COL_POSTCODE))
{
postcodeCol = c;
headerRow = r;
}
if (val.contains("\u5c3e\u7a0b") || val.contains("\u81ea\u63d0"))
{
zoneCol = c;
headerRow = r;
}
}
if (zoneCol >= 0 && postcodeCol >= 0)
{
break;
}
}
if (postcodeCol < 0)
{
return map;
}
if (zoneCol < 0)
{
zoneCol = 0;
}
if (headerRow < 0)
{
headerRow = 0;
}
for (int r = headerRow + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String zoneCode = normalizeCell(cellText(row.getCell(zoneCol)));
if (StringUtils.isEmpty(zoneCode))
{
zoneCode = "Mondial Relay";
}
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)));
if (StringUtils.isEmpty(postcode))
{
continue;
}
map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode);
}
return map;
}
/** UK surcharge sheet: multi blocks of region + postcode prefix */
private Map<String, List<String>> parseUkSurchargePostcodes(Sheet sheet)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int lastRow = sheet.getLastRowNum();
// detect header row containing ????
int headerRow = -1;
for (int r = 0; r <= Math.min(lastRow, 10); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
if (cellText(row.getCell(c)).contains("\u90ae\u7f16\u5f00\u5934")
|| cellText(row.getCell(c)).contains("\u7a7a\u6c14\u51cf\u6392"))
{
headerRow = r;
break;
}
}
if (headerRow >= 0)
{
break;
}
}
if (headerRow < 0)
{
return map;
}
String lastRm = "";
for (int r = headerRow + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String rmName = normalizeCell(cellText(row.getCell(0)));
if (StringUtils.isNotEmpty(rmName))
{
lastRm = rmName;
}
String rmPc = normalizePostcode(cellText(row.getCell(1)));
if (StringUtils.isNotEmpty(lastRm) && StringUtils.isNotEmpty(rmPc))
{
map.computeIfAbsent(lastRm, k -> new ArrayList<>()).add(rmPc);
}
String evName = normalizeCell(cellText(row.getCell(5)));
if (StringUtils.isEmpty(evName))
{
evName = normalizeCell(cellText(row.getCell(4)));
}
String evPc = normalizePostcode(cellText(row.getCell(6)));
if (StringUtils.isNotEmpty(evName) && StringUtils.isNotEmpty(evPc))
{
map.computeIfAbsent(evName, k -> new ArrayList<>()).add(evPc);
}
String airName = normalizeCell(cellText(row.getCell(8)));
String airPc = normalizePostcode(cellText(row.getCell(9)));
if (StringUtils.isNotEmpty(airName) && StringUtils.isNotEmpty(airPc))
{
map.computeIfAbsent(airName, k -> new ArrayList<>()).add(airPc);
}
}
return map;
}
private Map<String, List<String>> parseTwoColZoneMap(Sheet sheet, boolean requireExactPostcode)
{
Map<String, List<String>> map = new LinkedHashMap<>();
int postcodeCol = -1;
int zoneCol = -1;
int headerRow = -1;
int lastRow = sheet.getLastRowNum();
for (int r = 0; r <= Math.min(lastRow, 20); r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
for (int c = 0; c < row.getLastCellNum(); c++)
{
String val = cellText(row.getCell(c));
if (COL_POSTCODE.equals(val) || (!requireExactPostcode && val.contains(COL_POSTCODE)))
{
postcodeCol = c;
headerRow = r;
}
else if (COL_ZONE.equals(val))
{
zoneCol = c;
headerRow = r;
}
}
if (postcodeCol >= 0 && zoneCol >= 0)
{
break;
}
}
if (postcodeCol < 0 || zoneCol < 0 || headerRow < 0)
{
return map;
}
fillTwoCol(map, sheet, headerRow, postcodeCol, zoneCol);
return map;
}
private void fillTwoCol(Map<String, List<String>> map, Sheet sheet, int headerRow, int postcodeCol, int zoneCol)
{
int lastRow = sheet.getLastRowNum();
for (int r = headerRow + 1; r <= lastRow; r++)
{
Row row = sheet.getRow(r);
if (row == null)
{
continue;
}
String postcode = normalizePostcode(cellText(row.getCell(postcodeCol)));
String zoneCode = normalizeCell(cellText(row.getCell(zoneCol)));
if (StringUtils.isEmpty(postcode) || StringUtils.isEmpty(zoneCode))
{
continue;
}
map.computeIfAbsent(zoneCode, k -> new ArrayList<>()).add(postcode);
}
}
private static String normalizePostcode(String postcode)
{
if (StringUtils.isEmpty(postcode))
{
return postcode;
}
if (postcode.endsWith(".0") && postcode.matches("\\d+\\.0"))
{
return postcode.substring(0, postcode.length() - 2);
}
return postcode;
}
private static String normalizeCell(String text)
{
if (text == null)
{
return "";
}
return text.replace('\r', ' ').replace('\n', ' ').replaceAll("\\s+", " ").trim();
}
private String cellText(Cell cell)
{
if (cell == null)
{
return "";
}
return StringUtils.trim(CELL_FMT.formatCellValue(cell));
}
private Map<String, BmsRegion> loadRegionByNameCn()
{
BmsRegion query = new BmsRegion();
List<BmsRegion> regions = regionMapper.selectRegionList(query);
Map<String, BmsRegion> map = new HashMap<>();
if (regions == null)
{
return map;
}
for (BmsRegion region : regions)
{
if (region != null && StringUtils.isNotEmpty(region.getCountryNameCn()))
{
map.put(region.getCountryNameCn().trim(), region);
}
}
return map;
}
private BmsRegion resolveRegion(String excelName, Map<String, BmsRegion> regionByNameCn)
{
if (StringUtils.isEmpty(excelName) || regionByNameCn == null || regionByNameCn.isEmpty())
{
return null;
}
String name = excelName.trim();
BmsRegion exact = regionByNameCn.get(name);
if (exact != null)
{
return exact;
}
BmsRegion best = null;
int bestLen = -1;
for (BmsRegion region : regionByNameCn.values())
{
String cn = region.getCountryNameCn();
if (StringUtils.isEmpty(cn))
{
continue;
}
if (name.contains(cn) || cn.contains(name))
{
if (cn.length() > bestLen)
{
best = region;
bestLen = cn.length();
}
}
}
return best;
}
private static String buildZoneName(String countryName, String zoneCode)
{
return (countryName == null ? "" : countryName) + "(" + zoneCode + ")";
}
private static int extractZoneSort(String zoneCode)
{
if (StringUtils.isEmpty(zoneCode))
{
return 0;
}
Matcher m = ZONE_NUM.matcher(zoneCode);
if (m.find())
{
try
{
return Integer.parseInt(m.group(1));
}
catch (NumberFormatException ignored)
{
return 0;
}
}
return 0;
}
}