优化代码注释为简体中文
This commit is contained in:
+3
-3
@@ -525,7 +525,7 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
// Concurrent first-create: peer may have inserted; re-lock existing row.
|
||||
// 并发首次创建:其他节点可能已插入;重新锁定已存在的行。
|
||||
if (isAccountInitRace(e))
|
||||
{
|
||||
account = customerMoneyMapper.selectAccountForUpdate(customerId, BASE_CURRENCY);
|
||||
@@ -539,7 +539,7 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
}
|
||||
}
|
||||
|
||||
/** Deadlock / duplicate key while inserting ctr_customer_account. */
|
||||
/** 插入 ctr_customer_account 时出现死锁 / 唯一键冲突。 */
|
||||
private static boolean isAccountInitRace(Throwable e)
|
||||
{
|
||||
Throwable t = e;
|
||||
@@ -607,7 +607,7 @@ public class CustomerAccountApiImpl implements ICustomerAccountApi
|
||||
flow.setEffectiveDate(effectiveDate);
|
||||
flow.setExpireDate(expireDate);
|
||||
flow.setReason("账户初始化");
|
||||
// No login (open API / MQ / job): operatorId null, same as occupyCredit
|
||||
// 无登录态(开放 API / MQ / 定时任务):operatorId 为 null,与 occupyCredit 一致
|
||||
flow.setOperatorId(currentUserId());
|
||||
flow.setCreateBy(username);
|
||||
flow.setRemark(remark);
|
||||
|
||||
@@ -61,7 +61,7 @@ public class CustomerKeywordResolver
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pass-through raw keywords (covers codes not yet in ctr_customer)
|
||||
// 原样保留原始关键字(覆盖尚未录入 ctr_customer 的代码)
|
||||
for (String kw : keywords)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(kw))
|
||||
|
||||
@@ -35,8 +35,8 @@ public class OracleOrderBillMapper
|
||||
req.setUpstreamRow(row);
|
||||
req.setCustomerCode(firstStr(row, "CUSTOMER_CODE"));
|
||||
req.setCustomerName(firstStr(row, "CUSTOMER_NAME"));
|
||||
// Upstream product identity: LINE_CODE / LINE_NAME (fallback PRODUCT_*).
|
||||
// (BMS product_code_mapping applied later in quote / persist).
|
||||
// 上游产品标识:LINE_CODE / LINE_NAME(回退取 PRODUCT_*)。
|
||||
// (BMS 的 product_code_mapping 在后续报价 / 落库时应用)。
|
||||
String product = firstStr(row, "LINE_CODE", "PRODUCT_CODE");
|
||||
if (StringUtils.isEmpty(product))
|
||||
{
|
||||
@@ -48,10 +48,10 @@ public class OracleOrderBillMapper
|
||||
String countryName = firstStr(row, "COUNTRY_NAME", "COUNTRY_EINGTH_NAME", "COUNTRY");
|
||||
req.setCountryName(countryName);
|
||||
req.setDestPostcode(StringUtils.normalizePostcode(firstStr(row, "CONSIGNEE_POSTCODE")));
|
||||
// Prefer CONFIRM_WEIGHT (customer confirmed); fallback DIMENSIONAL_WEIGHT_FACTOR (charge weight).
|
||||
// 优先取 CONFIRM_WEIGHT(客户确认重量);回退取 DIMENSIONAL_WEIGHT_FACTOR(计费重量)。
|
||||
BigDecimal weight = firstPositiveDecimal(row, "CONFIRM_WEIGHT", "DIMENSIONAL_WEIGHT_FACTOR");
|
||||
req.setWeight(weight);
|
||||
// TAB_ORDER.WEIGHT = receive weight (收货重)
|
||||
// TAB_ORDER.WEIGHT = 收货重
|
||||
req.setReceiveWeight(firstDecimal(row, "WEIGHT"));
|
||||
req.setActualWeight(firstPositiveDecimal(row, "ACTUAL_WEIGHT", "PUSH_WEIGHT", "WEIGHT"));
|
||||
req.setPieces(firstInt(row, "ORDER_PIECE", "PACKAGE_NUM", "PRODUCT_SUM"));
|
||||
@@ -64,7 +64,7 @@ public class OracleOrderBillMapper
|
||||
req.setConsigneeName(firstStr(row, "CONSIGNEE_NAME"));
|
||||
req.setWarehouseCode(firstStr(row, "REGISTER_SITE_CODE"));
|
||||
req.setOrderRemark(firstStr(row, "TRACK_REMARK", "EXCEPTION_REMARK", "ORDER_CUSTOMNOTE"));
|
||||
// CREATE_DATE is order place time only; bizDate (billing datetime) comes from task.bill_time.
|
||||
// CREATE_DATE 仅为下单时间;业务日期(计费时间)取自 task.bill_time。
|
||||
Date createDate = firstDate(row, "CREATE_DATE");
|
||||
if (createDate != null)
|
||||
{
|
||||
@@ -112,7 +112,7 @@ public class OracleOrderBillMapper
|
||||
}
|
||||
}
|
||||
|
||||
/** First decimal that is strictly > 0 among keys (skip Oracle zero placeholders). */
|
||||
/** 返回各 key 中第一个严格 > 0 的数值(跳过 Oracle 的 0 占位值)。 */
|
||||
private static BigDecimal firstPositiveDecimal(Map<String, Object> row, String... keys)
|
||||
{
|
||||
if (row == null || keys == null)
|
||||
|
||||
+4
-4
@@ -51,10 +51,10 @@ public class OracleUpstreamOrderService
|
||||
String track = emptyToNull(trackingNo);
|
||||
try
|
||||
{
|
||||
// Avoid PageHelper wrapping this Oracle query (can slow / break SQL).
|
||||
// 避免 PageHelper 包装该 Oracle 查询(可能导致变慢或 SQL 出错)。
|
||||
PageHelper.clearPage();
|
||||
|
||||
// 1) Prefer exact match by transfer no (TRANSFER_ORDER_ID)
|
||||
// 1) 优先按转单号(TRANSFER_ORDER_ID)精确匹配
|
||||
if (tf != null)
|
||||
{
|
||||
Map<String, Object> byTransfer = query("TRANSFER_ORDER_ID",
|
||||
@@ -65,7 +65,7 @@ public class OracleUpstreamOrderService
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Fallback: order no
|
||||
// 2) 回退:按订单号
|
||||
Map<String, Object> row = null;
|
||||
if (ono != null)
|
||||
{
|
||||
@@ -76,7 +76,7 @@ public class OracleUpstreamOrderService
|
||||
row = query("ORG_ORDER_ID", () -> oracleUpstreamOrderMapper.selectByOrgOrderId(table, ono));
|
||||
}
|
||||
|
||||
// 3) Fallback: tracking no
|
||||
// 3) 回退:按运单号
|
||||
if (isEmpty(row) && track != null)
|
||||
{
|
||||
row = query("TRACK_ID", () -> oracleUpstreamOrderMapper.selectByTrackId(table, track));
|
||||
|
||||
+11
-11
@@ -72,7 +72,7 @@ public class TabOrderMonitorService
|
||||
/** 单批 Oracle 查询最长等待(从库 socketTimeout 为 120s,留同样余量)。 */
|
||||
private static final long COLLECT_BATCH_TIMEOUT_SECONDS = 120L;
|
||||
|
||||
/** Same default as historical hardcoded ORDER_STATUS IN (...). */
|
||||
/** 与历史硬编码的 ORDER_STATUS IN (...) 默认值相同。 */
|
||||
private static final List<Integer> DEFAULT_ORDER_STATUSES =
|
||||
Collections.unmodifiableList(Arrays.asList(
|
||||
Integer.valueOf(3), Integer.valueOf(4), Integer.valueOf(5), Integer.valueOf(6),
|
||||
@@ -416,7 +416,7 @@ public class TabOrderMonitorService
|
||||
Long.valueOf(System.currentTimeMillis() - t0));
|
||||
return new TabOrderPageResult(keys.size(), rows);
|
||||
}
|
||||
// Customer+time: exact COUNT(1) + current-page keys.
|
||||
// 客户+时间:精确 COUNT(1) 统计总数 + 当前页主键。
|
||||
return doSelectFilterListPage(table, query, pn, ps);
|
||||
}
|
||||
|
||||
@@ -442,7 +442,7 @@ public class TabOrderMonitorService
|
||||
throw new ServiceException("单号数量超过服务端上限 " + IDENTIFIER_HARD_LIMIT
|
||||
+ ",请分批查询,避免内存溢出");
|
||||
}
|
||||
// optional customer filter only; never use pickup time on this path
|
||||
// 仅可选的客户过滤条件;此路径绝不使用提货时间
|
||||
List<String> codes = query.getCustomerCodes();
|
||||
List<String> cleanedCodes = new ArrayList<String>();
|
||||
if (codes != null)
|
||||
@@ -544,8 +544,8 @@ public class TabOrderMonitorService
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer+time list: exact COUNT(1) for total + key-page for current rows.
|
||||
* Total uses countTabOrderMonitor (same filters as Oct-13 style COUNT).
|
||||
* 客户+时间列表:用精确的 COUNT(1) 统计总数 + 主键分页取当前页数据。
|
||||
* 总数使用 countTabOrderMonitor(与 10-13 风格的 COUNT 过滤条件相同)。
|
||||
*/
|
||||
private TabOrderPageResult doSelectFilterListPage(String table, TabOrderMonitorQuery query, int pn, int ps)
|
||||
{
|
||||
@@ -571,7 +571,7 @@ public class TabOrderMonitorService
|
||||
return new TabOrderPageResult(total, list);
|
||||
}
|
||||
|
||||
/** Exact total for customer+time list; short TTL cache to avoid recount on every page flip. */
|
||||
/** 客户+时间列表的精确总数;短期 TTL 缓存,避免每次翻页都重新统计。 */
|
||||
private long countFilterTotal(String table, TabOrderMonitorQuery query)
|
||||
{
|
||||
String fingerprint = "count|" + collectFingerprint(table, query);
|
||||
@@ -604,7 +604,7 @@ public class TabOrderMonitorService
|
||||
}
|
||||
}
|
||||
|
||||
/** Collect (and cache) filter-path primary keys, newest first; hard-capped at FILTER_LIST_KEY_LIMIT. */
|
||||
/** 收集(并缓存)筛选路径的主键,最新在前;硬性上限为 FILTER_LIST_KEY_LIMIT。 */
|
||||
private List<String> doCollectFilterKeyIds(String table, TabOrderMonitorQuery query)
|
||||
{
|
||||
String fingerprint = "filter|" + collectFingerprint(table, query);
|
||||
@@ -614,7 +614,7 @@ public class TabOrderMonitorService
|
||||
{
|
||||
return new ArrayList<String>(cached.ids);
|
||||
}
|
||||
// One Oracle key-page (ROWNUM <= limit+1). Avoid deep multi-page ROWNUM which hangs month windows.
|
||||
// 一次 Oracle 主键分页查询(ROWNUM <= limit+1)。避免深层多页 ROWNUM 导致月窗口查询卡死。
|
||||
List<String> ids = selectKeyIds(table, query, 0L, FILTER_LIST_KEY_LIMIT + 1L);
|
||||
if (ids.size() > FILTER_LIST_KEY_LIMIT)
|
||||
{
|
||||
@@ -1214,7 +1214,7 @@ public class TabOrderMonitorService
|
||||
normalizeBlPickup(query);
|
||||
}
|
||||
|
||||
/** Empty/missing -> default 3,4,5,6,7,8,11 so SQL always has a status IN list. */
|
||||
/** 为空/未传时默认为 3,4,5,6,7,8,11,保证 SQL 始终有状态 IN 列表。 */
|
||||
private static void normalizeOrderStatuses(TabOrderMonitorQuery query)
|
||||
{
|
||||
List<Integer> statuses = query.getOrderStatuses();
|
||||
@@ -1232,7 +1232,7 @@ public class TabOrderMonitorService
|
||||
query.setOrderStatuses(cleaned.isEmpty() ? new ArrayList<Integer>(DEFAULT_ORDER_STATUSES) : cleaned);
|
||||
}
|
||||
|
||||
/** Always force BL_PICKUP=1; UI no longer exposes this filter. */
|
||||
/** 始终强制 BL_PICKUP=1;界面已不再提供该过滤条件。 */
|
||||
private static void normalizeBlPickup(TabOrderMonitorQuery query)
|
||||
{
|
||||
query.setBlPickup(Integer.valueOf(1));
|
||||
@@ -1320,7 +1320,7 @@ public class TabOrderMonitorService
|
||||
vo.setTrackId(str(row, "TRACK_ID"));
|
||||
vo.setCustomerCode(str(row, "CUSTOMER_CODE"));
|
||||
vo.setCustomerName(str(row, "CUSTOMER_NAME"));
|
||||
// Prefer LINE_CODE / LINE_NAME (billing product); list SQL aliases them as PRODUCT_*.
|
||||
// 优先使用 LINE_CODE / LINE_NAME(计费产品);列表 SQL 将其别名输出为 PRODUCT_*。
|
||||
vo.setProductCode(firstStr(row, "LINE_CODE", "PRODUCT_CODE"));
|
||||
vo.setProductName(firstStr(row, "LINE_NAME", "PRODUCT_NAME"));
|
||||
vo.setProductFloorCode(str(row, "PRODUCT_FLOOR_CODE"));
|
||||
|
||||
+91
-91
@@ -81,7 +81,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
private static final Pattern EFFECTIVE_DATE_CN = Pattern.compile(
|
||||
"(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月\\s*(\\d{1,2})");
|
||||
|
||||
/** Parse 0.01≤W≤0.3 / 0.3<W≤1 */
|
||||
/** 解析重量区间 0.01≤W≤0.3 / 0.3<W≤1 */
|
||||
private static final Pattern WEIGHT_RANGE_PATTERN = Pattern.compile(
|
||||
"(\\d+(?:\\.\\d+)?)\\s*[<\\u2264]=?\\s*W\\s*[<\\u2264]=?\\s*(\\d+(?:\\.\\d+)?)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
@@ -129,7 +129,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
@Override
|
||||
public List<BmsProduct> selectBmsProductList(BmsProduct product)
|
||||
{
|
||||
// Status refresh is done on activate / scheduled task; skip UPDATE on every list read.
|
||||
// 状态刷新在启用时/定时任务中完成;不在每次列表查询时执行 UPDATE。
|
||||
if (product != null)
|
||||
{
|
||||
product.setStatus(normalizeStatusCode(product.getStatus()));
|
||||
@@ -194,7 +194,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
int rows = countryMapper.updateBmsProductCountry(country);
|
||||
if (rows > 0)
|
||||
{
|
||||
// Recalc status when effective/expire dates change
|
||||
// 生效/失效日期变化时重新计算状态
|
||||
countryMapper.refreshStatusByDates();
|
||||
List<Map<String, String>> changes = PriceChangeEsLog.diffCatalogPrice(
|
||||
old == null ? null : old.getFvValue(),
|
||||
@@ -401,7 +401,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
products = dedupeCatalogProducts(products);
|
||||
reportImportProgress(taskId, 20, "parsing excel");
|
||||
// Pre-check all products: reject whole import before any write if any is earlier than prev version
|
||||
// 预检所有产品:若任一产品的生效时间早于上一版本,则在写入前整体拒绝导入
|
||||
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
|
||||
|
||||
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||
@@ -411,7 +411,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||
// Pre-check: any price sheet with duplicate country+weight fails the whole import.
|
||||
// 预检:任一价格表中存在重复的国家+重量即整体导入失败。
|
||||
assertAllPriceSheetsNoDupCountryWeight(workbook, regionByNameCn, euRegions, zoneByCountryAndCode);
|
||||
assertImportProductsNoDupCountryWeight(workbook, products, regionByNameCn, euRegions,
|
||||
zoneByCountryAndCode);
|
||||
@@ -456,7 +456,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
reportImportProgress(taskId, 95, "finishing");
|
||||
if (failureMsg.length() > 0)
|
||||
{
|
||||
// Do not throw: each product already committed/rolled back independently
|
||||
// 不抛异常:每个产品已各自独立提交/回滚
|
||||
return MessageUtils.message("price.product.import.partialFail",
|
||||
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
|
||||
}
|
||||
@@ -531,7 +531,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
products = dedupeCatalogProducts(products);
|
||||
Map<String, PriceBlock> priceBlocks = parseCountryPriceBlocks(workbook);
|
||||
reportImportProgress(taskId, 20, "parsing excel");
|
||||
// Pre-check all products: reject whole import before any write if any is earlier than prev version
|
||||
// 预检所有产品:若任一产品的生效时间早于上一版本,则在写入前整体拒绝导入
|
||||
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
|
||||
|
||||
Long importPsvId = forcedEff == null
|
||||
@@ -674,7 +674,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return readQuoteWorkbook(bytes, filename).byCode;
|
||||
}
|
||||
|
||||
/** One workbook open; each price sheet is parsed once. */
|
||||
/** 只打开一次工作簿;每个价格表只解析一次。 */
|
||||
private QuoteWorkbook readQuoteWorkbook(byte[] bytes, String filename) throws Exception
|
||||
{
|
||||
if (bytes == null || bytes.length == 0)
|
||||
@@ -689,8 +689,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
try (InputStream is = new java.io.ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||
{
|
||||
assertNotSingleCountryQuoteForMultiImport(workbook);
|
||||
// Detail sheets with inline product-code / product-name column: identify by those
|
||||
// columns, not by catalog sheet-name matching (variants like RM/IP/EV share one sheet).
|
||||
// 明细表中带内联产品代码/产品名称列:按这些列识别,
|
||||
// 而不是按产品目录的表名匹配(RM/IP/EV 等变体共用一张表)。
|
||||
if (workbookHasInlineProductColumns(workbook))
|
||||
{
|
||||
return readQuoteWorkbookByInlineProductCode(workbook);
|
||||
@@ -802,8 +802,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Formal product import when price sheets carry inline product-code / product-name columns.
|
||||
* Identifies products from detail rows (not catalog sheet-name matching).
|
||||
* 正式产品导入:价格表中带内联产品代码/产品名称列的场景。
|
||||
* 从明细行识别产品(不依赖产品目录的表名匹配)。
|
||||
*/
|
||||
private String importProductExcelByInline(Workbook workbook, byte[] bytes, String filename,
|
||||
String operName, String taskId, Date forcedEff) throws Exception
|
||||
@@ -883,7 +883,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return msg;
|
||||
}
|
||||
|
||||
/** Prefer catalog metadata when code matches; keep detail product name for variants. */
|
||||
/** 编码匹配时优先使用产品目录的元数据;变体保留明细中的产品名称。 */
|
||||
private List<BmsProduct> enrichInlineProductsFromCatalog(Workbook workbook, List<BmsProduct> inlineProducts)
|
||||
{
|
||||
Map<String, BmsProduct> catalogByCode = new HashMap<String, BmsProduct>();
|
||||
@@ -903,7 +903,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
// Catalog optional for inline import.
|
||||
// 内联导入中产品目录为可选。
|
||||
}
|
||||
}
|
||||
List<BmsProduct> out = new ArrayList<BmsProduct>();
|
||||
@@ -941,8 +941,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* True when any multi-country price sheet has an inline product-code or product-name column
|
||||
* (产品代码 / 产品对接代码 / 产品名称).
|
||||
* 当任一多国价格表存在内联产品代码或产品名称列
|
||||
* (产品代码 / 产品对接代码 / 产品名称)时返回 true。
|
||||
*/
|
||||
private boolean workbookHasInlineProductColumns(Workbook workbook)
|
||||
{
|
||||
@@ -982,8 +982,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-country parse keyed by inline product-code column on price sheets.
|
||||
* Does not use 产品目录 sheet matching.
|
||||
* 按价格表中的内联产品代码列解析多国数据。
|
||||
* 不使用产品目录表匹配。
|
||||
*/
|
||||
private QuoteWorkbook readQuoteWorkbookByInlineProductCode(Workbook workbook) throws Exception
|
||||
{
|
||||
@@ -1076,10 +1076,10 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse one multi-country price sheet grouped by inline product code / product name.
|
||||
* Prefer product-code column; if only product-name exists, resolve code via catalog name map.
|
||||
* Country / product-code / product-name / size / last-mile / lead-time carry forward
|
||||
* when subsequent weight-band rows leave those cells blank.
|
||||
* 解析单个多国价格表,按内联产品代码/产品名称分组。
|
||||
* 优先使用产品代码列;若只有产品名称,则通过产品目录名称映射解析编码。
|
||||
* 当后续重量段行中这些单元格为空时,国家/产品代码/产品名称/尺寸/尾程/时效
|
||||
* 沿用上一行的值。
|
||||
*/
|
||||
private Map<String, List<BmsProductCountry>> parseProductSheetByInlineCode(Sheet sheet,
|
||||
Map<String, String> catalogCodeByName, Map<String, BmsRegion> regionByNameCn,
|
||||
@@ -1166,7 +1166,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
productChanged = true;
|
||||
currentProductName = newName;
|
||||
// Name-only sheets: resolve product code from catalog by product name.
|
||||
// 仅有名称的表:根据产品名称从产品目录解析产品代码。
|
||||
if (colProductCode < 0)
|
||||
{
|
||||
String mapped = resolveCodeByProductName(currentProductName, catalogCodeByName);
|
||||
@@ -1265,7 +1265,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Build productName -> productCode from 产品目录 (first row per name wins). */
|
||||
/** 从产品目录构建 productName -> productCode 映射(同名以首行为准)。 */
|
||||
private Map<String, String> buildCatalogCodeByName(Workbook workbook)
|
||||
{
|
||||
Map<String, String> map = new LinkedHashMap<String, String>();
|
||||
@@ -1297,12 +1297,12 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
// Catalog optional for inline identity; ignore parse errors.
|
||||
// 内联识别中产品目录为可选;忽略解析错误。
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Resolve product code by product name; exact compact match, then base name before '(' / '('. */
|
||||
/** 按产品名称解析产品代码;先做紧凑精确匹配,再取 '(' / '(' 之前的基础名称匹配。 */
|
||||
private String resolveCodeByProductName(String productName, Map<String, String> catalogCodeByName)
|
||||
{
|
||||
if (StringUtils.isEmpty(productName) || catalogCodeByName == null || catalogCodeByName.isEmpty())
|
||||
@@ -1339,8 +1339,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-country import expects price sheets with a "country" column.
|
||||
* Single-country dedicated-line sheets use product-name + weight blocks instead.
|
||||
* 多国导入要求价格表带有“国家”列。
|
||||
* 单国专线表则使用产品名称 + 重量段的结构。
|
||||
*/
|
||||
private void assertNotSingleCountryQuoteForMultiImport(Workbook workbook)
|
||||
{
|
||||
@@ -1350,7 +1350,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** Detect single-country cost-sheet layout (no country column on price sheets). */
|
||||
/** 检测单国成本表结构(价格表中没有国家列)。 */
|
||||
private boolean looksLikeSingleCountryQuote(Workbook workbook)
|
||||
{
|
||||
if (workbook == null)
|
||||
@@ -1371,12 +1371,12 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (findHeaderCell(sheet, "\u56fd\u5bb6") != null)
|
||||
if (findHeaderCell(sheet, "国家") != null)
|
||||
{
|
||||
sawCountryCol = true;
|
||||
break;
|
||||
}
|
||||
int[] nameHeader = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0");
|
||||
int[] nameHeader = findHeaderCell(sheet, "产品名称");
|
||||
if (nameHeader == null)
|
||||
{
|
||||
continue;
|
||||
@@ -1386,7 +1386,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (findColByHeader(head, "\u91cd\u91cf", 0) >= 0)
|
||||
if (findColByHeader(head, "重量", 0) >= 0)
|
||||
{
|
||||
sawCountryPriceBlock = true;
|
||||
}
|
||||
@@ -1404,12 +1404,12 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (findHeaderCell(catalog, "\u5bf9\u63a5\u4ee3\u7801") != null)
|
||||
if (findHeaderCell(catalog, "对接代码") != null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return findHeaderCell(catalog, "\u4ea7\u54c1\u540d\u79f0") != null
|
||||
|| findHeaderCell(catalog, "\u4ea7\u54c1ID") != null;
|
||||
return findHeaderCell(catalog, "产品名称") != null
|
||||
|| findHeaderCell(catalog, "产品ID") != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1798,7 +1798,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/** Stable insert order reduces InnoDB secondary-index lock inversion. */
|
||||
/** 固定的插入顺序可减少 InnoDB 二级索引锁的交叉等待。 */
|
||||
private static void sortProductCountriesForInsert(List<BmsProductCountry> rows)
|
||||
{
|
||||
if (rows == null || rows.size() < 2)
|
||||
@@ -1836,7 +1836,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* One-product import in REQUIRES_NEW; retry whole txn on InnoDB deadlock.
|
||||
* 单产品导入运行在 REQUIRES_NEW 事务中;遇到 InnoDB 死锁时重试整个事务。
|
||||
*/
|
||||
private int[] importOneProductWithDeadlockRetry(Workbook workbook, BmsProduct product, String operName,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
@@ -1988,7 +1988,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return out;
|
||||
}
|
||||
|
||||
/** @return int[]{added, skipped, countryRows} */
|
||||
/** @return int[]{新增数, 跳过数, 国家行数} */
|
||||
private int[] importOneProduct(Workbook workbook, BmsProduct product, String operName,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries, Date forcedEffectiveDate)
|
||||
@@ -2014,7 +2014,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return importOneProductFromRows(product, countries, operName, forcedEffectiveDate);
|
||||
}
|
||||
|
||||
/** Import using pre-parsed country rows (inline product-code / product-name path). */
|
||||
/** 使用预先解析好的国家行导入(内联产品代码/产品名称路径)。 */
|
||||
private int[] importOneProductFromRows(BmsProduct product, List<BmsProductCountry> countries,
|
||||
String operName, Date forcedEffectiveDate)
|
||||
{
|
||||
@@ -2033,7 +2033,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
product == null ? "" : product.getProductCode()));
|
||||
}
|
||||
assertNoDuplicateCountryWeightRows(product, countries);
|
||||
// UI-selected effective date wins over Excel; previous open product/country rows expire at this date
|
||||
// UI 选择的生效日期优先于 Excel;此前开放的产品/国家行在该日期失效
|
||||
Date newEff = forcedEffectiveDate == null && product.getEffectiveDate() == null
|
||||
? null
|
||||
: requireImportEffectiveDate(forcedEffectiveDate != null ? forcedEffectiveDate
|
||||
@@ -2085,12 +2085,12 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Save country rows by effective_date.
|
||||
* For each product_code + country in the Excel:
|
||||
* - expire all earlier open weight bands under that country
|
||||
* - delete same-day open weight bands (unique key blocks insert if only expired)
|
||||
* - insert Excel weight bands only (no copy / merge of old bands)
|
||||
* Reject when newEff is earlier than the product's previous version effective time.
|
||||
* 按 effective_date 保存国家行。
|
||||
* 对 Excel 中的每个 product_code + 国家:
|
||||
* - 将该国家下所有更早的开放重量段置为失效
|
||||
* - 删除同一天开放的重量段(仅置为失效时唯一键会阻止插入)
|
||||
* - 只插入 Excel 中的重量段(不复制 / 合并旧重量段)
|
||||
* 当 newEff 早于该产品上一版本的生效时间时拒绝导入。
|
||||
*/
|
||||
private int saveCountries(BmsProduct product, List<BmsProductCountry> countries)
|
||||
{
|
||||
@@ -2161,7 +2161,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return touched;
|
||||
}
|
||||
|
||||
/** Identity for replace scope: prefer country_code, else country_name. */
|
||||
/** 替换范围的标识:优先 country_code,否则使用 country_name。 */
|
||||
private String countryIdentityKey(BmsProductCountry row)
|
||||
{
|
||||
if (row == null)
|
||||
@@ -2230,7 +2230,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** Truncate to hour precision (mm:ss = 00). */
|
||||
/** 截断到小时精度(分秒为 00)。 */
|
||||
private Date normalizeToHour(Date d)
|
||||
{
|
||||
if (d == null)
|
||||
@@ -2248,7 +2248,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** 0 pending / 1 active / 2 expired from datetime vs now */
|
||||
/** 根据日期时间与当前时间比较得出 0 待生效 / 1 生效 / 2 已失效 */
|
||||
private String resolveStoredStatus(Date effectiveDate, Date expireDate)
|
||||
{
|
||||
Date now = new Date();
|
||||
@@ -2274,7 +2274,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
product.setStatus(resolveStoredStatus(product.getEffectiveDate(), product.getExpireDate()));
|
||||
}
|
||||
|
||||
/** Map ACTIVE/PENDING/EXPIRED or 0/1/2 to stored status code. */
|
||||
/** 将 ACTIVE/PENDING/EXPIRED 或 0/1/2 映射为存储的状态码。 */
|
||||
private String normalizeStatusCode(String status)
|
||||
{
|
||||
if (status == null || status.isEmpty())
|
||||
@@ -2324,8 +2324,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
private List<BmsProduct> parseCatalog(Sheet sheet)
|
||||
{
|
||||
List<BmsProduct> list = new ArrayList<>();
|
||||
// Header is "对接代码" or legacy "产品对接代码". Column A is often empty.
|
||||
// 20260928 layout inserts "计泡规则" before country, so map by header name.
|
||||
// 表头为“对接代码”或旧版“产品对接代码”。A 列通常为空。
|
||||
// 20260928 版式在国家列之前插入了“计泡规则”,因此按表头名称映射。
|
||||
int[] header = findHeaderCell(sheet, "对接代码");
|
||||
if (header == null)
|
||||
{
|
||||
@@ -2385,8 +2385,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip vendor suffixes such as "WSP046(中性)" / "WSP046(中性)" -> "WSP046".
|
||||
* Returns null when no leading product-code token remains.
|
||||
* 去除厂商后缀,例如 "WSP046(中性)" / "WSP046(中性)" -> "WSP046"。
|
||||
* 当不再有前导产品代码标识时返回 null。
|
||||
*/
|
||||
private static String normalizeImportProductCode(String raw)
|
||||
{
|
||||
@@ -2403,14 +2403,14 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return m.group(1);
|
||||
}
|
||||
|
||||
/** Header column, or the legacy fixed offset when the title is absent. */
|
||||
/** 返回表头列;标题缺失时使用旧版固定偏移量。 */
|
||||
private static int catalogColumn(Row headerRow, String keyword, int fallback)
|
||||
{
|
||||
int col = findColByHeader(headerRow, keyword, 0);
|
||||
return col >= 0 ? col : fallback;
|
||||
}
|
||||
|
||||
/** Keep first row per product_code (catalog often repeats code across country rows). */
|
||||
/** 每个 product_code 保留首行(产品目录常在国家行中重复同一编码)。 */
|
||||
private List<BmsProduct> dedupeCatalogProducts(List<BmsProduct> products)
|
||||
{
|
||||
Map<String, BmsProduct> map = new HashMap<>();
|
||||
@@ -2477,7 +2477,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
else if (StringUtils.isNotEmpty(nameKey) && sheetNameBase(nameKey).equals(sheetNameBase(key))
|
||||
&& sheetNameBase(nameKey).length() >= 2)
|
||||
{
|
||||
// e.g. catalog name suffix differs from sheet (tax/cargo label)
|
||||
// 例如产品目录名称后缀与表名不同(税/货属性标识)
|
||||
score = 70;
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(codeKey) && key.contains(codeKey))
|
||||
@@ -2500,13 +2500,13 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return "";
|
||||
}
|
||||
String out = s.trim().toLowerCase(Locale.ROOT);
|
||||
// unify dash variants: - – — -
|
||||
// 统一各种连字符写法:- – — -
|
||||
out = out.replace('–', '-').replace('—', '-').replace('-', '-');
|
||||
out = out.replace(" ", "").replace("\u3000", "");
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Base name before last '-' (catalog/sheet suffix may differ). */
|
||||
/** 取最后一个 '-' 之前的基础名称(产品目录/表名的后缀可能不同)。 */
|
||||
private static String sheetNameBase(String normalizedKey)
|
||||
{
|
||||
if (StringUtils.isEmpty(normalizedKey))
|
||||
@@ -2574,7 +2574,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
|
||||
Date sheetEffective = parseSheetEffectiveDate(sheet);
|
||||
List<BmsProductCountry> list = new ArrayList<>();
|
||||
int[] countryHeader = findHeaderCell(sheet, "\u56fd\u5bb6");
|
||||
int[] countryHeader = findHeaderCell(sheet, "国家");
|
||||
if (countryHeader == null)
|
||||
{
|
||||
return list;
|
||||
@@ -2582,12 +2582,12 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
int headerRowIdx = countryHeader[0];
|
||||
Row headerRow = sheet.getRow(headerRowIdx);
|
||||
int colCountry = countryHeader[1];
|
||||
int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", colCountry + 1);
|
||||
int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", colCountry + 1);
|
||||
int colRegister = findColByHeader(headerRow, "\u6302\u53f7", colCountry + 1);
|
||||
int colSize = findColByHeader(headerRow, "\u5c3a\u5bf8", colCountry + 1);
|
||||
int colLastMile = findColByHeader(headerRow, "\u5c3e\u7a0b", colCountry + 1);
|
||||
int colLeadTime = findColByHeader(headerRow, "\u65f6\u6548", colCountry + 1);
|
||||
int colWeight = findColByHeader(headerRow, "重量", colCountry + 1);
|
||||
int colFreight = findColByHeader(headerRow, "运费", colCountry + 1);
|
||||
int colRegister = findColByHeader(headerRow, "挂号", colCountry + 1);
|
||||
int colSize = findColByHeader(headerRow, "尺寸", colCountry + 1);
|
||||
int colLastMile = findColByHeader(headerRow, "尾程", colCountry + 1);
|
||||
int colLeadTime = findColByHeader(headerRow, "时效", colCountry + 1);
|
||||
if (colWeight < 0)
|
||||
{
|
||||
colWeight = colCountry + 1;
|
||||
@@ -2627,8 +2627,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
String countryCell = cellString(row.getCell(colCountry));
|
||||
String weightLabel = cellString(row.getCell(colWeight));
|
||||
if ("\u56fd\u5bb6".equals(countryCell)
|
||||
|| (weightLabel != null && weightLabel.contains("\u91cd\u91cf\u6bb5")))
|
||||
if ("国家".equals(countryCell)
|
||||
|| (weightLabel != null && weightLabel.contains("重量段")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -2771,7 +2771,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Load region map: country_name_cn -> region (for country_code_2).
|
||||
* 加载区域映射:country_name_cn -> 区域(用于 country_code_2)。
|
||||
*/
|
||||
private Map<String, BmsRegion> loadRegionByNameCn()
|
||||
{
|
||||
@@ -2795,7 +2795,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact country_name_cn match first; single contains match as fallback (e.g. Czech / Czech Republic).
|
||||
* 先按 country_name_cn 精确匹配;否则退化为唯一的包含匹配(例如 Czech / Czech Republic)。
|
||||
*/
|
||||
private BmsRegion resolveRegion(String excelName, Map<String, BmsRegion> regionByNameCn)
|
||||
{
|
||||
@@ -2954,7 +2954,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Find column index on header row whose text contains keyword; -1 if not found.
|
||||
* 在表头行中查找文本包含 keyword 的列下标;未找到返回 -1。
|
||||
*/
|
||||
private static int findColByHeader(Row headerRow, String keyword, int fromCol)
|
||||
{
|
||||
@@ -2981,8 +2981,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan top-left area for a header cell; returns {rowIndex, colIndex} or null.
|
||||
* Prefers exact match, then contains.
|
||||
* 在左上区域扫描表头单元格;返回 {rowIndex, colIndex} 或 null。
|
||||
* 优先精确匹配,其次包含匹配。
|
||||
*/
|
||||
private static int[] findHeaderCell(Sheet sheet, String headerText)
|
||||
{
|
||||
@@ -3004,7 +3004,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Also scan a few leading empty columns beyond lastCellNum edge cases
|
||||
// 同时扫描 lastCellNum 边界情况之外的若干前导空列
|
||||
int maxCol = Math.max(lastCell, 12);
|
||||
maxCol = Math.min(maxCol, 20);
|
||||
for (int c = 0; c < maxCol; c++)
|
||||
@@ -3086,13 +3086,13 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return null;
|
||||
}
|
||||
s = s.trim();
|
||||
// Excel NA markers: / 、- 、— 、N/A
|
||||
// Excel 中的空值标记:/ 、- 、— 、N/A
|
||||
if ("/".equals(s) || "-".equals(s) || "—".equals(s) || "-".equals(s)
|
||||
|| "N/A".equalsIgnoreCase(s) || "NA".equalsIgnoreCase(s))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// strip currency / unit suffixes: 56、56元、56 RMB
|
||||
// 去除货币/单位后缀:56、56元、56 RMB
|
||||
s = s.replace(",", "").replace("¥", "").replace("元", "")
|
||||
.replace("RMB", "").replace("rmb", "").trim();
|
||||
if (s.isEmpty() || "/".equals(s))
|
||||
@@ -3122,8 +3122,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Country cost sheet catalog: product id / name / service / cargo / note / update date.
|
||||
* Destination country is not in the sheet.
|
||||
* 国家成本表目录:产品 ID / 名称 / 服务 / 货属性 / 备注 / 更新日期。
|
||||
* 目的国家不在该表中。
|
||||
*/
|
||||
private List<BmsProduct> parseCountryCatalog(Sheet sheet)
|
||||
{
|
||||
@@ -3392,7 +3392,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Require import effective time (hour precision). */
|
||||
/** 要求提供导入生效时间(小时精度)。 */
|
||||
private Date requireImportEffectiveDate(Date effectiveDate)
|
||||
{
|
||||
Date d = normalizeToHour(effectiveDate);
|
||||
@@ -3404,8 +3404,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject import when selected effective time is earlier than the product's latest previous version.
|
||||
* Same effective time (re-import that version) is allowed.
|
||||
* 当所选生效时间早于该产品最近一个上一版本时拒绝导入。
|
||||
* 生效时间相同(重新导入该版本)允许通过。
|
||||
*/
|
||||
private void assertImportEffectiveNotBeforePrev(BmsProduct product, Date newEff)
|
||||
{
|
||||
@@ -3431,7 +3431,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
code, name, formatDateTime(newEff), formatDateTime(prevEff)));
|
||||
}
|
||||
|
||||
/** Batch pre-check: fail whole import before any DB write. */
|
||||
/** 批量预检:在任何数据库写入前使整个导入失败。 */
|
||||
private void assertImportProductsEffectiveNotBeforePrev(List<BmsProduct> products, Date forcedEff)
|
||||
{
|
||||
if (forcedEff == null)
|
||||
@@ -3455,9 +3455,9 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan every price sheet (not only catalog-matched ones).
|
||||
* Duplicate country+weight on any sheet fails the whole import
|
||||
* (e.g. same country twice for different last-mile carriers).
|
||||
* 扫描每个价格表(不仅限于与产品目录匹配的表)。
|
||||
* 任一表中出现重复的国家+重量即整体导入失败
|
||||
* (例如同一国家因不同尾程承运商出现两次)。
|
||||
*/
|
||||
private void assertAllPriceSheetsNoDupCountryWeight(Workbook workbook,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
@@ -3500,8 +3500,8 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Workbook pre-check for full import: reject before any write when a product sheet
|
||||
* has duplicate country + weight band (same zone) rows.
|
||||
* 全量导入的工作簿预检:若某产品表中存在重复的国家 + 重量段(同一区域)行,
|
||||
* 则在任何写入前拒绝导入。
|
||||
*/
|
||||
private void assertImportProductsNoDupCountryWeight(Workbook workbook, List<BmsProduct> products,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
@@ -3539,7 +3539,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** Fail when Excel rows for one product contain duplicate country + weight band. */
|
||||
/** 当同一产品的 Excel 行中存在重复的国家 + 重量段时失败。 */
|
||||
private void assertNoDuplicateCountryWeightRows(BmsProduct product, List<BmsProductCountry> rows)
|
||||
{
|
||||
String tip = buildDuplicateCountryWeightTip(product, rows);
|
||||
@@ -3597,7 +3597,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return dups;
|
||||
}
|
||||
|
||||
/** Align with uk_product_country_zone_weight_eff (country + zone + weight min/max). */
|
||||
/** 与唯一键 uk_product_country_zone_weight_eff 保持一致(国家 + 区域 + 重量上下限)。 */
|
||||
private String countryWeightDupKey(BmsProductCountry c)
|
||||
{
|
||||
String country = StringUtils.isNotEmpty(c.getCountryCode())
|
||||
@@ -3627,7 +3627,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
return v.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
/** Stamp product + all country detail rows with the same effective date. */
|
||||
/** 为产品及其所有国家明细行写入相同的生效日期。 */
|
||||
private void applyForcedEffectiveDate(BmsProduct product, List<BmsProductCountry> countries, Date newEff)
|
||||
{
|
||||
Date eff = requireImportEffectiveDate(newEff);
|
||||
@@ -3648,7 +3648,7 @@ public class BmsProductServiceImpl implements IBmsProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** @return int[]{added, skipped, countryRows} */
|
||||
/** @return int[]{新增数, 跳过数, 国家行数} */
|
||||
private int[] persistImportedProduct(BmsProduct product, List<BmsProductCountry> countries, String operName)
|
||||
{
|
||||
product.setCreateBy(operName);
|
||||
|
||||
+62
-62
@@ -78,7 +78,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
private static final Pattern EFFECTIVE_DATE_CN = Pattern.compile(
|
||||
"(\\d{4})\\s*年\\s*(\\d{1,2})\\s*月\\s*(\\d{1,2})");
|
||||
|
||||
/** Parse 0.01≤W≤0.3 / 0.3<W≤1 */
|
||||
/** 解析重量区间 0.01≤W≤0.3 / 0.3<W≤1 */
|
||||
private static final Pattern WEIGHT_RANGE_PATTERN = Pattern.compile(
|
||||
"(\\d+(?:\\.\\d+)?)\\s*[<\\u2264]=?\\s*W\\s*[<\\u2264]=?\\s*(\\d+(?:\\.\\d+)?)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
@@ -181,7 +181,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
int rows = countryMapper.updateBmsRedlineProductCountry(country);
|
||||
if (rows > 0)
|
||||
{
|
||||
// Recalc status when effective/expire dates change
|
||||
// 生效/失效日期变化时重新计算状态
|
||||
countryMapper.refreshStatusByDates();
|
||||
List<Map<String, String>> changes = PriceChangeEsLog.diffCatalogPrice(
|
||||
old == null ? null : old.getFvValue(),
|
||||
@@ -379,7 +379,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
products = dedupeCatalogProducts(products);
|
||||
reportImportProgress(taskId, 20, "parsing excel");
|
||||
// Pre-check all products: reject whole import before any write if any is earlier than prev version
|
||||
// 预检所有产品:若任一产品的生效时间早于上一版本,则在写入前整体拒绝导入
|
||||
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
|
||||
|
||||
Map<String, BmsRegion> regionByNameCn = loadRegionByNameCn();
|
||||
@@ -389,7 +389,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
List<BmsRegion> euRegions = loadEuRegions(regionByNameCn);
|
||||
Map<String, PrcZone> zoneByCountryAndCode = loadZoneByCountryAndCode();
|
||||
// Pre-check: same product cannot have duplicate country + weight band in Excel
|
||||
// 预检:同一产品在 Excel 中不能存在重复的国家 + 重量段
|
||||
assertImportProductsNoDupCountryWeight(workbook, products, regionByNameCn, euRegions,
|
||||
zoneByCountryAndCode);
|
||||
|
||||
@@ -443,7 +443,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
reportImportProgress(taskId, 95, "finishing");
|
||||
if (failureMsg.length() > 0)
|
||||
{
|
||||
// Do not throw: each product already committed/rolled back independently
|
||||
// 不抛异常:每个产品已各自独立提交/回滚
|
||||
return MessageUtils.message("price.product.import.partialFail",
|
||||
Integer.valueOf(successNum), Integer.valueOf(skipNum), failureMsg.toString());
|
||||
}
|
||||
@@ -518,7 +518,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
products = dedupeCatalogProducts(products);
|
||||
Map<String, PriceBlock> priceBlocks = parseCountryPriceBlocks(workbook);
|
||||
reportImportProgress(taskId, 20, "parsing excel");
|
||||
// Pre-check all products: reject whole import before any write if any is earlier than prev version
|
||||
// 预检所有产品:若任一产品的生效时间早于上一版本,则在写入前整体拒绝导入
|
||||
assertImportProductsEffectiveNotBeforePrev(products, forcedEff);
|
||||
|
||||
Long importPsvId = forcedEff == null
|
||||
@@ -1029,7 +1029,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return taskId;
|
||||
}
|
||||
|
||||
/** Stable insert order reduces InnoDB secondary-index lock inversion. */
|
||||
/** 固定的插入顺序可减少 InnoDB 二级索引锁的交叉等待。 */
|
||||
private static void sortRedlineCountriesForInsert(List<BmsRedlineProductCountry> rows)
|
||||
{
|
||||
if (rows == null || rows.size() < 2)
|
||||
@@ -1196,7 +1196,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return out;
|
||||
}
|
||||
|
||||
/** @return int[]{added, skipped, countryRows} */
|
||||
/** @return int[]{新增数, 跳过数, 国家行数} */
|
||||
private int[] importOneProduct(Workbook workbook, BmsRedlineProduct product, String operName,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
Map<String, PrcZone> zoneByCountryAndCode, Set<String> unmatchedCountries, Date forcedEffectiveDate)
|
||||
@@ -1222,7 +1222,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return importOneProductFromRows(product, countries, operName, forcedEffectiveDate);
|
||||
}
|
||||
|
||||
/** Import using pre-parsed country rows (inline product-code / product-name path). */
|
||||
/** 使用预先解析好的国家行导入(内联产品代码/产品名称路径)。 */
|
||||
private int[] importOneProductFromRows(BmsRedlineProduct product, List<BmsRedlineProductCountry> countries,
|
||||
String operName, Date forcedEffectiveDate)
|
||||
{
|
||||
@@ -1292,12 +1292,12 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Save country rows by effective_date.
|
||||
* For each product_code + country in the Excel:
|
||||
* - expire all earlier open weight bands under that country
|
||||
* - delete same-day open weight bands (unique key blocks insert if only expired)
|
||||
* - insert Excel weight bands only (no copy / merge of old bands)
|
||||
* Reject when newEff is earlier than the product's previous version effective time.
|
||||
* 按 effective_date 保存国家行。
|
||||
* 对 Excel 中的每个 product_code + 国家:
|
||||
* - 将该国家下所有更早的开放重量段置为失效
|
||||
* - 删除同一天开放的重量段(仅置为失效时唯一键会阻止插入)
|
||||
* - 只插入 Excel 中的重量段(不复制 / 合并旧重量段)
|
||||
* 当 newEff 早于该产品上一版本的生效时间时拒绝导入。
|
||||
*/
|
||||
private int saveCountries(BmsRedlineProduct product, List<BmsRedlineProductCountry> countries)
|
||||
{
|
||||
@@ -1368,7 +1368,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return touched;
|
||||
}
|
||||
|
||||
/** Identity for replace scope: prefer country_code, else country_name. */
|
||||
/** 替换范围的标识:优先 country_code,否则使用 country_name。 */
|
||||
private String countryIdentityKey(BmsRedlineProductCountry row)
|
||||
{
|
||||
if (row == null)
|
||||
@@ -1437,7 +1437,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** Truncate to hour precision (mm:ss = 00). */
|
||||
/** 截断到小时精度(分秒为 00)。 */
|
||||
private Date normalizeToHour(Date d)
|
||||
{
|
||||
if (d == null)
|
||||
@@ -1455,7 +1455,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** 0 pending / 1 active / 2 expired from datetime vs now */
|
||||
/** 根据日期时间与当前时间比较得出 0 待生效 / 1 生效 / 2 已失效 */
|
||||
private String resolveStoredStatus(Date effectiveDate, Date expireDate)
|
||||
{
|
||||
Date now = new Date();
|
||||
@@ -1481,7 +1481,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
product.setStatus(resolveStoredStatus(product.getEffectiveDate(), product.getExpireDate()));
|
||||
}
|
||||
|
||||
/** Map ACTIVE/PENDING/EXPIRED or 0/1/2 to stored status code. */
|
||||
/** 将 ACTIVE/PENDING/EXPIRED 或 0/1/2 映射为存储的状态码。 */
|
||||
private String normalizeStatusCode(String status)
|
||||
{
|
||||
if (status == null || status.isEmpty())
|
||||
@@ -1531,8 +1531,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
private List<BmsRedlineProduct> parseCatalog(Sheet sheet)
|
||||
{
|
||||
List<BmsRedlineProduct> list = new ArrayList<>();
|
||||
// Header is "对接代码" or legacy "产品对接代码". Column A is often empty.
|
||||
// 20260928 layout inserts "计泡规则" before country, so map by header name.
|
||||
// 表头为“对接代码”或旧版“产品对接代码”。A 列通常为空。
|
||||
// 20260928 版式在国家列之前插入了“计泡规则”,因此按表头名称映射。
|
||||
int[] header = findHeaderCell(sheet, "对接代码");
|
||||
if (header == null)
|
||||
{
|
||||
@@ -1592,8 +1592,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip vendor suffixes such as "WSP046(中性)" / "WSP046(中性)" -> "WSP046".
|
||||
* Returns null when no leading product-code token remains.
|
||||
* 去除厂商后缀,例如 "WSP046(中性)" / "WSP046(中性)" -> "WSP046"。
|
||||
* 当不再有前导产品代码标识时返回 null。
|
||||
*/
|
||||
private static String normalizeImportProductCode(String raw)
|
||||
{
|
||||
@@ -1610,14 +1610,14 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return m.group(1);
|
||||
}
|
||||
|
||||
/** Header column, or the legacy fixed offset when the title is absent. */
|
||||
/** 返回表头列;标题缺失时使用旧版固定偏移量。 */
|
||||
private static int catalogColumn(Row headerRow, String keyword, int fallback)
|
||||
{
|
||||
int col = findColByHeader(headerRow, keyword, 0);
|
||||
return col >= 0 ? col : fallback;
|
||||
}
|
||||
|
||||
/** Keep first row per product_code (catalog often repeats code across country rows). */
|
||||
/** 每个 product_code 保留首行(产品目录常在国家行中重复同一编码)。 */
|
||||
private List<BmsRedlineProduct> dedupeCatalogProducts(List<BmsRedlineProduct> products)
|
||||
{
|
||||
Map<String, BmsRedlineProduct> map = new HashMap<>();
|
||||
@@ -1701,7 +1701,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return "";
|
||||
}
|
||||
String out = s.trim().toLowerCase(Locale.ROOT);
|
||||
// unify dash variants: - – — -
|
||||
// 统一各种连字符写法:- – — -
|
||||
out = out.replace('–', '-').replace('—', '-').replace('-', '-');
|
||||
out = out.replace(" ", "").replace("\u3000", "");
|
||||
return out;
|
||||
@@ -1956,7 +1956,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Load region map: country_name_cn -> region (for country_code_2).
|
||||
* 加载区域映射:country_name_cn -> 区域(用于 country_code_2)。
|
||||
*/
|
||||
private Map<String, BmsRegion> loadRegionByNameCn()
|
||||
{
|
||||
@@ -1980,7 +1980,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Exact country_name_cn match first; single contains match as fallback (e.g. Czech / Czech Republic).
|
||||
* 先按 country_name_cn 精确匹配;否则退化为唯一的包含匹配(例如 Czech / Czech Republic)。
|
||||
*/
|
||||
private BmsRegion resolveRegion(String excelName, Map<String, BmsRegion> regionByNameCn)
|
||||
{
|
||||
@@ -2138,7 +2138,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Find column index on header row whose text contains keyword; -1 if not found.
|
||||
* 在表头行中查找文本包含 keyword 的列下标;未找到返回 -1。
|
||||
*/
|
||||
private static int findColByHeader(Row headerRow, String keyword, int fromCol)
|
||||
{
|
||||
@@ -2165,8 +2165,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan top-left area for a header cell; returns {rowIndex, colIndex} or null.
|
||||
* Prefers exact match, then contains.
|
||||
* 在左上区域扫描表头单元格;返回 {rowIndex, colIndex} 或 null。
|
||||
* 优先精确匹配,其次包含匹配。
|
||||
*/
|
||||
private static int[] findHeaderCell(Sheet sheet, String headerText)
|
||||
{
|
||||
@@ -2188,7 +2188,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Also scan a few leading empty columns beyond lastCellNum edge cases
|
||||
// 同时扫描 lastCellNum 边界情况之外的若干前导空列
|
||||
int maxCol = Math.max(lastCell, 12);
|
||||
maxCol = Math.min(maxCol, 20);
|
||||
for (int c = 0; c < maxCol; c++)
|
||||
@@ -2270,13 +2270,13 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return null;
|
||||
}
|
||||
s = s.trim();
|
||||
// Excel NA markers: / 、- 、— 、N/A
|
||||
// Excel 中的空值标记:/ 、- 、— 、N/A
|
||||
if ("/".equals(s) || "-".equals(s) || "—".equals(s) || "-".equals(s)
|
||||
|| "N/A".equalsIgnoreCase(s) || "NA".equalsIgnoreCase(s))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// strip currency / unit suffixes: 56、56元、56 RMB
|
||||
// 去除货币/单位后缀:56、56元、56 RMB
|
||||
s = s.replace(",", "").replace("¥", "").replace("元", "")
|
||||
.replace("RMB", "").replace("rmb", "").trim();
|
||||
if (s.isEmpty() || "/".equals(s))
|
||||
@@ -2306,8 +2306,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Country cost sheet catalog: product id / name / service / cargo / note / update date.
|
||||
* Destination country is not in the sheet.
|
||||
* 国家成本表目录:产品 ID / 名称 / 服务 / 货属性 / 备注 / 更新日期。
|
||||
* 目的国家不在该表中。
|
||||
*/
|
||||
private List<BmsRedlineProduct> parseCountryCatalog(Sheet sheet)
|
||||
{
|
||||
@@ -2513,8 +2513,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Formal redline import when price sheets carry inline product-code / product-name columns.
|
||||
* Identifies products from detail rows (not catalog sheet-name matching).
|
||||
* 正式红线产品导入:价格表中带内联产品代码/产品名称列的场景。
|
||||
* 从明细行识别产品(不依赖产品目录的表名匹配)。
|
||||
*/
|
||||
private String importRedlineExcelByInline(Workbook workbook, byte[] bytes, String filename,
|
||||
String operName, String taskId, Date forcedEff) throws Exception
|
||||
@@ -2593,7 +2593,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return msg;
|
||||
}
|
||||
|
||||
/** Prefer catalog metadata when code matches; keep detail product name for variants. */
|
||||
/** 编码匹配时优先使用产品目录的元数据;变体保留明细中的产品名称。 */
|
||||
private List<BmsRedlineProduct> enrichInlineProductsFromCatalog(Workbook workbook, List<BmsRedlineProduct> inlineProducts)
|
||||
{
|
||||
Map<String, BmsRedlineProduct> catalogByCode = new HashMap<String, BmsRedlineProduct>();
|
||||
@@ -2613,7 +2613,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
// Catalog optional for inline import.
|
||||
// 内联导入中产品目录为可选。
|
||||
}
|
||||
}
|
||||
List<BmsRedlineProduct> out = new ArrayList<BmsRedlineProduct>();
|
||||
@@ -2651,8 +2651,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* True when any multi-country price sheet has an inline product-code or product-name column
|
||||
* (产品代码 / 产品对接代码 / 产品名称).
|
||||
* 当任一多国价格表存在内联产品代码或产品名称列
|
||||
* (产品代码 / 产品对接代码 / 产品名称)时返回 true。
|
||||
*/
|
||||
private boolean workbookHasInlineProductColumns(Workbook workbook)
|
||||
{
|
||||
@@ -2692,8 +2692,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-country parse keyed by inline product-code column on price sheets.
|
||||
* Does not use 产品目录 sheet matching.
|
||||
* 按价格表中的内联产品代码列解析多国数据。
|
||||
* 不使用产品目录表匹配。
|
||||
*/
|
||||
private RedlineQuoteWorkbook readQuoteWorkbookByInlineProductCode(Workbook workbook) throws Exception
|
||||
{
|
||||
@@ -2786,10 +2786,10 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse one multi-country price sheet grouped by inline product code / product name.
|
||||
* Prefer product-code column; if only product-name exists, resolve code via catalog name map.
|
||||
* Country / product-code / product-name / size / last-mile / lead-time carry forward
|
||||
* when subsequent weight-band rows leave those cells blank.
|
||||
* 解析单个多国价格表,按内联产品代码/产品名称分组。
|
||||
* 优先使用产品代码列;若只有产品名称,则通过产品目录名称映射解析编码。
|
||||
* 当后续重量段行中这些单元格为空时,国家/产品代码/产品名称/尺寸/尾程/时效
|
||||
* 沿用上一行的值。
|
||||
*/
|
||||
private Map<String, List<BmsRedlineProductCountry>> parseProductSheetByInlineCode(Sheet sheet,
|
||||
Map<String, String> catalogCodeByName, Map<String, BmsRegion> regionByNameCn,
|
||||
@@ -2876,7 +2876,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
{
|
||||
productChanged = true;
|
||||
currentProductName = newName;
|
||||
// Name-only sheets: resolve product code from catalog by product name.
|
||||
// 仅有名称的表:根据产品名称从产品目录解析产品代码。
|
||||
if (colProductCode < 0)
|
||||
{
|
||||
String mapped = resolveCodeByProductName(currentProductName, catalogCodeByName);
|
||||
@@ -2975,7 +2975,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Build productName -> productCode from 产品目录 (first row per name wins). */
|
||||
/** 从产品目录构建 productName -> productCode 映射(同名以首行为准)。 */
|
||||
private Map<String, String> buildCatalogCodeByName(Workbook workbook)
|
||||
{
|
||||
Map<String, String> map = new LinkedHashMap<String, String>();
|
||||
@@ -3007,12 +3007,12 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
// Catalog optional for inline identity; ignore parse errors.
|
||||
// 内联识别中产品目录为可选;忽略解析错误。
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Resolve product code by product name; exact compact match, then base name before '(' / '('. */
|
||||
/** 按产品名称解析产品代码;先做紧凑精确匹配,再取 '(' / '(' 之前的基础名称匹配。 */
|
||||
private String resolveCodeByProductName(String productName, Map<String, String> catalogCodeByName)
|
||||
{
|
||||
if (StringUtils.isEmpty(productName) || catalogCodeByName == null || catalogCodeByName.isEmpty())
|
||||
@@ -3144,7 +3144,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Require import effective time (hour precision). */
|
||||
/** 要求提供导入生效时间(小时精度)。 */
|
||||
private Date requireImportEffectiveDate(Date effectiveDate)
|
||||
{
|
||||
Date d = normalizeToHour(effectiveDate);
|
||||
@@ -3156,8 +3156,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject import when selected effective time is earlier than the product's latest previous version.
|
||||
* Same effective time (re-import that version) is allowed.
|
||||
* 当所选生效时间早于该产品最近一个上一版本时拒绝导入。
|
||||
* 生效时间相同(重新导入该版本)允许通过。
|
||||
*/
|
||||
private void assertImportEffectiveNotBeforePrev(BmsRedlineProduct product, Date newEff)
|
||||
{
|
||||
@@ -3183,7 +3183,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
code, name, formatDateTime(newEff), formatDateTime(prevEff)));
|
||||
}
|
||||
|
||||
/** Batch pre-check: fail whole import before any DB write. */
|
||||
/** 批量预检:在任何数据库写入前使整个导入失败。 */
|
||||
private void assertImportProductsEffectiveNotBeforePrev(List<BmsRedlineProduct> products, Date forcedEff)
|
||||
{
|
||||
if (forcedEff == null)
|
||||
@@ -3207,8 +3207,8 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
|
||||
/**
|
||||
* Workbook pre-check for full import: reject before any write when a product sheet
|
||||
* has duplicate country + weight band (same zone) rows.
|
||||
* 全量导入的工作簿预检:若某产品表中存在重复的国家 + 重量段(同一区域)行,
|
||||
* 则在任何写入前拒绝导入。
|
||||
*/
|
||||
private void assertImportProductsNoDupCountryWeight(Workbook workbook, List<BmsRedlineProduct> products,
|
||||
Map<String, BmsRegion> regionByNameCn, List<BmsRegion> euRegions,
|
||||
@@ -3246,7 +3246,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** Fail when Excel rows for one product contain duplicate country + weight band. */
|
||||
/** 当同一产品的 Excel 行中存在重复的国家 + 重量段时失败。 */
|
||||
private void assertNoDuplicateCountryWeightRows(BmsRedlineProduct product, List<BmsRedlineProductCountry> rows)
|
||||
{
|
||||
String tip = buildDuplicateCountryWeightTip(product, rows);
|
||||
@@ -3304,7 +3304,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return dups;
|
||||
}
|
||||
|
||||
/** Align with uk_product_country_zone_weight_eff (country + zone + weight min/max). */
|
||||
/** 与唯一键 uk_product_country_zone_weight_eff 保持一致(国家 + 区域 + 重量上下限)。 */
|
||||
private String countryWeightDupKey(BmsRedlineProductCountry c)
|
||||
{
|
||||
String country = StringUtils.isNotEmpty(c.getCountryCode())
|
||||
@@ -3334,7 +3334,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
return v.stripTrailingZeros().toPlainString();
|
||||
}
|
||||
|
||||
/** Stamp product + all country detail rows with the same effective date. */
|
||||
/** 为产品及其所有国家明细行写入相同的生效日期。 */
|
||||
private void applyForcedEffectiveDate(BmsRedlineProduct product, List<BmsRedlineProductCountry> countries, Date newEff)
|
||||
{
|
||||
Date eff = requireImportEffectiveDate(newEff);
|
||||
@@ -3355,7 +3355,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService
|
||||
}
|
||||
}
|
||||
|
||||
/** @return int[]{added, skipped, countryRows} */
|
||||
/** @return int[]{新增数, 跳过数, 国家行数} */
|
||||
private int[] persistImportedProduct(BmsRedlineProduct product, List<BmsRedlineProductCountry> countries, String operName)
|
||||
{
|
||||
product.setCreateBy(operName);
|
||||
|
||||
+17
-17
@@ -272,7 +272,7 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
private void billFromUpstreamRow(PrcBillingTask task, Map<String, Object> row)
|
||||
{
|
||||
Long taskId = task.getTaskId();
|
||||
// When caller already pulled the row (monitor sync), still write ES once here.
|
||||
// 调用方已拉回行数据时(监控同步),仍在此写入一次 ES。
|
||||
saveUpstreamPullEs(task, row);
|
||||
TmsPriceBillRequest billReq = oracleOrderBillMapper.toBillRequest(row,
|
||||
billingProperties.getOracle().getDefaultProductCode());
|
||||
@@ -299,8 +299,8 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
task.setCustomerCode(billReq.getCustomerCode());
|
||||
try
|
||||
{
|
||||
// No hard validate on customer/product/weight: always try bill (or soft-fail snapshot)
|
||||
// so upstream fields still overwrite. Quote failures land in persistFailedSnapshot.
|
||||
// 不对客户/产品/重量做硬校验:始终尝试计费(或软失败快照),
|
||||
// 这样上游字段仍能覆盖。报价失败会落到 persistFailedSnapshot。
|
||||
if (StringUtils.isEmpty(billReq.getOrderNo()))
|
||||
{
|
||||
if (StringUtils.isNotEmpty(billReq.getTransferNo()))
|
||||
@@ -316,7 +316,7 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
{
|
||||
throw new ServiceException("Oracle order mapped orderNo empty");
|
||||
}
|
||||
// Upstream ORDER_STATUS 9/10 -> REFUND (even if task was submitted as PENDING/REBILL).
|
||||
// 上游 ORDER_STATUS 为 9/10 时转为 REFUND(即使任务提交时是 PENDING/REBILL)。
|
||||
if (BillOrderStatus.requiresFeeRefund(billReq.getOrderStatus()))
|
||||
{
|
||||
String refundOrderNo = StringUtils.isNotEmpty(billReq.getOrderNo())
|
||||
@@ -354,7 +354,7 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
return req;
|
||||
}
|
||||
|
||||
/** Order bill is the failure record; drop the queue row by transfer_no. */
|
||||
/** 订单账单即失败记录;按 transfer_no 删除队列行。 */
|
||||
private void deleteTaskAfterBillSaved(PrcBillingTask task)
|
||||
{
|
||||
if (task == null || StringUtils.isEmpty(task.getTransferNo()))
|
||||
@@ -364,7 +364,7 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
billingTaskMapper.deleteByTransferNo(task.getTransferNo().trim());
|
||||
}
|
||||
|
||||
/** Resolve orderNo for REFUND from task keys or existing bill (no Oracle quote required). */
|
||||
/** 从任务键或已有账单解析 REFUND 的 orderNo(无需 Oracle 报价)。 */
|
||||
private String resolveRefundOrderNo(PrcBillingTask task)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(task.getOrderNo()))
|
||||
@@ -428,7 +428,7 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry bill() on MySQL deadlock / lock wait (common when many tasks share one ctr_customer_account).
|
||||
* MySQL 死锁/锁等待时重试 bill()(多个任务共用同一 ctr_customer_account 时常见)。
|
||||
*/
|
||||
private TmsPriceQuoteResult billWithDbRetry(Long taskId, TmsPriceBillRequest billReq)
|
||||
{
|
||||
@@ -465,7 +465,7 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
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;
|
||||
@@ -519,8 +519,8 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
{
|
||||
throw new ServiceException("billing task not found");
|
||||
}
|
||||
// Allow SUCCESS retry: billing result may be wrong and needs re-run.
|
||||
// Reset to WAITING so processTask does not short-circuit on SUCCESS; then sync process.
|
||||
// 允许 SUCCESS 重试:计费结果可能有误,需要重跑。
|
||||
// 重置为 WAITING,避免 processTask 因 SUCCESS 直接短路;随后同步处理。
|
||||
PrcBillingTask reset = new PrcBillingTask();
|
||||
reset.setTaskId(taskId);
|
||||
reset.setSuccessStatus(BillTaskSuccessStatus.WAITING.getCode());
|
||||
@@ -534,8 +534,8 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
}
|
||||
|
||||
/**
|
||||
* Bill save deletes the queue row. Callers still need a SUCCESS result when the row is gone.
|
||||
* A remaining row means the bill was not saved and the task was marked FAILED.
|
||||
* 账单保存后会删除队列行。行已消失时调用方仍需要 SUCCESS 结果。
|
||||
* 行仍存在表示账单未保存,任务被标记为 FAILED。
|
||||
*/
|
||||
private PrcBillingTask taskAfterProcess(PrcBillingTask task)
|
||||
{
|
||||
@@ -622,8 +622,8 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
return existed == null ? BillTaskType.PENDING : BillTaskType.REBILL;
|
||||
}
|
||||
|
||||
/** Write back status/billId/failCode/error only; keep inbound transfer/tracking/order/billTime untouched. */
|
||||
/** Prefer existing bill customer when task has none (REFUND path). */
|
||||
/** 仅回写 status/billId/failCode/error;入参的 transfer/tracking/order/billTime 保持不变。 */
|
||||
/** 任务无客户编码时优先使用已有账单的客户(REFUND 路径)。 */
|
||||
private void fillCustomerCodeFromBill(PrcBillingTask task, String orderNo)
|
||||
{
|
||||
if (task == null || StringUtils.isNotEmpty(task.getCustomerCode()))
|
||||
@@ -670,8 +670,8 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
}
|
||||
|
||||
/**
|
||||
* Write pulled Oracle TAB_ORDER row to ES. bizId prefers transferNo.
|
||||
* Always called once before bill/snapshot so pull is visible even when billing soft-fails.
|
||||
* 将拉回的 Oracle TAB_ORDER 行写入 ES。bizId 优先使用 transferNo。
|
||||
* 在计费/快照前始终调用一次,使拉取记录即使计费软失败也可见。
|
||||
*/
|
||||
private void saveUpstreamPullEs(PrcBillingTask task, Map<String, Object> row)
|
||||
{
|
||||
@@ -710,7 +710,7 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService
|
||||
req.put("upstreamRow", row);
|
||||
try
|
||||
{
|
||||
// ES mapping has response.ok as long; use 1/0 not boolean true/false.
|
||||
// ES 映射中 response.ok 为 long 类型,使用 1/0 而非布尔 true/false。
|
||||
billSearchStore.save(EsLogType.BILL, bizId, req.toJSONString(),
|
||||
"{\"ok\":1,\"action\":\"UPSTREAM_PULL\"}");
|
||||
}
|
||||
|
||||
+86
-86
@@ -204,9 +204,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
private static final int BATCH_ADD_FEE_PREVIEW_LIMIT = 20;
|
||||
private static final int CHARGE_WEIGHT_IMPORT_MAX = 5000;
|
||||
private static final int RECALC_PRICE_MAX = 5000;
|
||||
/** Soft max for one local-rebill job (id-only load + continuous workers). */
|
||||
/** 单个本地重算任务的上限(仅加载 ID + 连续 worker)。 */
|
||||
private static final int LOCAL_REBILL_MAX = 200000;
|
||||
/** Soft max for one order/billing list Excel export. */
|
||||
/** 单次订单/账单列表 Excel 导出的上限。 */
|
||||
private static final int ORDER_BILL_EXPORT_MAX = 50000;
|
||||
|
||||
@Override
|
||||
@@ -266,7 +266,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
failReq.setTransferNo(bill.getTransferNo());
|
||||
assertOrderBillEditable(bill);
|
||||
// Duplicate guard is fee_type=REFUND on prc_income_bill_fee (refund_status alone may be stale).
|
||||
// 重复校验以 prc_income_bill_fee 的 fee_type=REFUND 为准(仅看 refund_status 可能已过期)。
|
||||
List<PrcIncomeBillFee> fees = incomeBillFeeMapper.selectByBillId(bill.getBillId());
|
||||
if (fees == null)
|
||||
{
|
||||
@@ -317,7 +317,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
refundFee.setManualFlag("0");
|
||||
incomeBillFeeMapper.insertPrcIncomeBillFee(refundFee);
|
||||
bill.setRefundStatus(BillRefundStatus.REFUNDED.getCode());
|
||||
// Align with upstream 已退件 after fee refund.
|
||||
// 费用退费后与上游“已退件”状态对齐。
|
||||
bill.setOrderStatus(BillOrderStatus.REFUNDED.getCodeBoxed());
|
||||
if (incomeBill != null)
|
||||
{
|
||||
@@ -511,7 +511,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
applyFailReasonVersion(vo, ver);
|
||||
}
|
||||
}
|
||||
// Soft-fail rows often leave package/version empty on the bill; resolve from customer bind.
|
||||
// 软失败的行在账单上常常缺少套餐/版本;从客户绑定中解析。
|
||||
fillFailReasonPackageVersionFromBind(vo, bill);
|
||||
if (bill.getPsdId() != null && priceSetDetailMapper != null)
|
||||
{
|
||||
@@ -533,9 +533,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* When order-bill package / version is empty, do not keep empty snapshot fields.
|
||||
* Resolve package from customer bind; match version by pickupDate then bizDate.
|
||||
* Leave empty when still not found.
|
||||
* 订单账单的套餐/版本为空时,不要保留空的快照字段。
|
||||
* 从客户绑定解析套餐;再按 pickupDate、bizDate 依次匹配版本。
|
||||
* 仍然找不到时保持为空。
|
||||
*/
|
||||
private void fillFailReasonPackageVersionFromBind(OrderBillFailReasonVo vo, PrcOrderBill bill)
|
||||
{
|
||||
@@ -644,7 +644,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
vo.setExpireTime(ver.getExpireTime());
|
||||
}
|
||||
|
||||
/** Prefer pickupDate, then bizDate, for sales package version window match. */
|
||||
/** 销售套餐版本窗口匹配时优先用 pickupDate,其次用 bizDate。 */
|
||||
private PrcPriceSetVersion selectVersionByPickupOrBiz(Long priceSetId, PrcOrderBill bill)
|
||||
{
|
||||
if (priceSetId == null || priceSetVersionMapper == null || bill == null)
|
||||
@@ -716,7 +716,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
amount = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// uk_bill_fee_item (bill_id, fee_type, sc_id): overwrite when exists
|
||||
// uk_bill_fee_item(bill_id, fee_type, sc_id):存在时覆盖
|
||||
PrcIncomeBillFee existing = incomeBillFeeMapper.selectByBillIdAndFeeTypeAndScId(
|
||||
bill.getBillId(), feeType, surcharge.getScId());
|
||||
if (existing != null)
|
||||
@@ -973,11 +973,11 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
private static final long LOCAL_REBILL_DB_RETRY_BASE_MS = 80L;
|
||||
|
||||
/**
|
||||
* Local rebill without outer txn: each {@link #bill}/{@link #refund} starts its own transaction so
|
||||
* MySQL deadlock can be retried cleanly (same idea as billing-task billWithDbRetry).
|
||||
* <p>If local order is refund-like (order_status 9/10 or refund_status=REFUNDED):
|
||||
* skip when prc_income_bill_fee already has fee_type=REFUND; otherwise call {@link #refund}.
|
||||
* Otherwise normal {@link #bill}.
|
||||
* 本地重算不套外层事务:每次 {@link #bill}/{@link #refund} 各自开启事务,
|
||||
* 以便 MySQL 死锁时能干净地重试(与计费任务的 billWithDbRetry 思路一致)。
|
||||
* <p>若本地订单属于退费类(order_status 9/10 或 refund_status=REFUNDED):
|
||||
* 当 prc_income_bill_fee 已存在 fee_type=REFUND 时跳过;否则调用 {@link #refund}。
|
||||
* 其他情况走正常的 {@link #bill}。
|
||||
*/
|
||||
@Override
|
||||
public TmsPriceQuoteResult rebillLocalByBillId(String billId)
|
||||
@@ -1015,7 +1015,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return billLocalWithDbRetry(billId.trim(), req);
|
||||
}
|
||||
|
||||
/** order_status 9/10 or refund_status=REFUNDED. */
|
||||
/** order_status 为 9/10 或 refund_status=REFUNDED。 */
|
||||
private static boolean isLocalRefundCandidate(PrcOrderBill bill)
|
||||
{
|
||||
if (bill == null)
|
||||
@@ -1122,7 +1122,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
throw last != null ? last : new ServiceException("local rebill refund retry exhausted");
|
||||
}
|
||||
|
||||
/** Deadlock / lock wait — safe to retry whole bill txn (parallel local rebill). */
|
||||
/** 死锁/锁等待——可安全重试整个计费事务(并行本地重算)。 */
|
||||
private static boolean isRetryableDbConflict(Throwable e)
|
||||
{
|
||||
Throwable t = e;
|
||||
@@ -1169,7 +1169,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
throw new ServiceException(MessageUtils.message("price.bill.recalc.tooMany",
|
||||
Integer.valueOf(LOCAL_REBILL_MAX)));
|
||||
}
|
||||
// Id-only: avoid loading full prc_order_bill rows for large jobs.
|
||||
// 仅取 ID:大数据量任务避免加载完整的 prc_order_bill 行。
|
||||
List<String> billIds = self.selectBillIdsForLocalRebill(query);
|
||||
if (billIds == null || billIds.isEmpty())
|
||||
{
|
||||
@@ -1178,7 +1178,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
String taskId = IdUtils.fastSimpleUUID();
|
||||
localRebillAsyncRunner.put(taskId, BatchAddIncomeBillFeeTask.STATUS_PENDING, 0, "queued", null,
|
||||
billIds.size(), 0, 0);
|
||||
// One ES op-log per batch on finish (LOCAL_REBILL_DONE); fail orders go into detail.failDetails.
|
||||
// 完成时每批写一条 ES 操作日志(LOCAL_REBILL_DONE);失败订单写入 detail.failDetails。
|
||||
localRebillAsyncRunner.run(taskId, billIds, request);
|
||||
return taskId;
|
||||
}
|
||||
@@ -1243,7 +1243,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
String taskId = IdUtils.fastSimpleUUID();
|
||||
orderBillExportAsyncRunner.put(taskId, PrcOrderBillExportTask.STATUS_PENDING, 0, "queued", null,
|
||||
type, fileName, null, 0);
|
||||
// Pass request Authentication into async thread for @DataScope on selectBillList.
|
||||
// 将请求的 Authentication 传入异步线程,供 selectBillList 上的 @DataScope 使用。
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
orderBillExportAsyncRunner.run(taskId, type, q, fileName, authentication);
|
||||
return taskId;
|
||||
@@ -1355,7 +1355,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
boolean hasOtherFilter = (productCodes != null && !productCodes.isEmpty())
|
||||
|| StringUtils.isNotEmpty(creditArrearsStatus)
|
||||
|| StringUtils.isNotEmpty(countryCode);
|
||||
// Same gate as order-list UI: identifiers, or customer + (bizDate or pickupDate).
|
||||
// 与订单列表 UI 相同的校验:提供标识,或客户 +(bizDate 或 pickupDate)。
|
||||
boolean hasFilter = hasIdentifier
|
||||
|| (hasCustomer && (hasBizDate || hasPickupDate))
|
||||
|| hasOtherFilter;
|
||||
@@ -1413,7 +1413,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return m;
|
||||
}
|
||||
|
||||
/** Update income header + fee snapshots for product or redline only. */
|
||||
/** 仅针对产品或红线更新收入表头 + 费用快照。 */
|
||||
private void persistProductOrRedlineOnly(PrcOrderBill bill, TmsPriceQuoteResult result, boolean productMode)
|
||||
{
|
||||
PrcIncomeBill incomeBill = incomeBillMapper.selectByBillId(bill.getBillId());
|
||||
@@ -1503,9 +1503,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared list-scope resolve for batch fee / recalc price.
|
||||
* scopeMode: SELECTED uses billIds; ALL uses filters (at least one required).
|
||||
* orderStatuses alone does not count as a meaningful filter (same as export).
|
||||
* 批量添加费用/重算价格共用的列表范围解析。
|
||||
* scopeMode:SELECTED 使用 billIds;ALL 使用筛选条件(至少需要一个)。
|
||||
* 仅传 orderStatuses 不算有效筛选(与导出规则一致)。
|
||||
*/
|
||||
private List<PrcOrderBill> resolveListScopedBills(String scopeModeRaw, List<String> billIdsRaw,
|
||||
List<String> orderNosRaw, List<String> trackingNosRaw, List<String> transferNosRaw, List<String> orderIdentifiersRaw,
|
||||
@@ -1652,7 +1652,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return ccy;
|
||||
}
|
||||
|
||||
/** Recalc surcharge/income totals on order + income bill, sync credit, return detail. */
|
||||
/** 重新计算订单 + 收入账单的附加费/收入合计,同步授信,并返回明细。 */
|
||||
private TmsPriceQuoteResult refreshBillIncomeTotals(PrcOrderBill bill)
|
||||
{
|
||||
List<PrcIncomeBillFee> fees = incomeBillFeeMapper.selectByBillId(bill.getBillId());
|
||||
@@ -2013,7 +2013,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
bill.setBillKey(billKey);
|
||||
bill.setOrderNo(orderNo);
|
||||
// Upstream header overwrite (no validate).
|
||||
// 上游表头覆盖(不做校验)。
|
||||
applyUpstreamBillHeaderFields(bill, request);
|
||||
Long customerId = request.getCustomerId();
|
||||
if (customerId == null && result != null)
|
||||
@@ -2035,7 +2035,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
customerActiveStatus = BillCustomerActiveStatus.ACTIVE.getCode();
|
||||
}
|
||||
bill.setCustomerActiveStatus(customerActiveStatus);
|
||||
// Product: quote result already resolved LINE_CODE via product_code_mapping.
|
||||
// 产品:报价结果已通过 product_code_mapping 解析出 LINE_CODE。
|
||||
if (result != null && StringUtils.isNotEmpty(result.getProductCode()))
|
||||
{
|
||||
bill.setProductCode(result.getProductCode());
|
||||
@@ -2056,12 +2056,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
{
|
||||
bill.setCountryName(result.getCountryName());
|
||||
}
|
||||
// weight = system/original upstream weight; charge_weight may differ when VOL_8000
|
||||
// (or manual override) raised/lowered the billed weight.
|
||||
// weight = 系统/上游原始重量;当 VOL_8000(或人工覆盖)提高/降低了计费重量时,
|
||||
// charge_weight 可能与之不同。
|
||||
bill.setWeight(weight);
|
||||
bill.setBizDate(bizDate);
|
||||
bill.setTmsId(request.getTmsId() == null ? null : Long.valueOf(request.getTmsId()));
|
||||
// Do not persist match_source / match_message on order bill.
|
||||
// 不在订单账单上持久化 match_source / match_message。
|
||||
bill.setMatchSource(null);
|
||||
bill.setMatchMessage(null);
|
||||
bill.setPriceSetId(result.getPriceSetId());
|
||||
@@ -2078,7 +2078,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
bill.setEffectiveDate(result.getEffectiveDate());
|
||||
if (isUpstreamSyncRequest(request))
|
||||
{
|
||||
// request.weight already carries ruled/calc weight used for quote
|
||||
// request.weight 已携带用于报价的规则/计算重量
|
||||
BigDecimal cw = request.getWeight() != null ? request.getWeight() : weight;
|
||||
bill.setChargeWeight(cw);
|
||||
}
|
||||
@@ -2106,7 +2106,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
if (billingFailed)
|
||||
{
|
||||
// Soft-fail: persist order snapshot with zero amounts; skip fee detail rows.
|
||||
// 软失败:以零金额持久化订单快照;跳过费用明细行。
|
||||
bill.setFreightRate(BigDecimal.ZERO);
|
||||
bill.setBaseFreightRate(BigDecimal.ZERO);
|
||||
bill.setDiscountPercent(null);
|
||||
@@ -2139,12 +2139,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
bill.setFreightAmount(result.getFreightAmount());
|
||||
bill.setSurchargeAmount(result.getSurchargeAmount());
|
||||
bill.setIncomeTotal(result.getPayableTotal());
|
||||
// Payable total is decoupled from product catalog; leave null when quote has no pay_total.
|
||||
// 应付合计与产品目录解耦;报价没有 pay_total 时保持为 null。
|
||||
bill.setPayTotal(result.getPayTotal());
|
||||
}
|
||||
bill.setCurrency(result.getCurrency());
|
||||
bill.setRoundMode(result.getRoundMode());
|
||||
// Credit soft-fail before persist so insert/update stores arrears flag in one write.
|
||||
// 在持久化前先做授信软失败处理,使 insert/update 一次写入即包含欠费标记。
|
||||
String creditArrearsStatus = syncOrderCredit(bill, previousIncome, customerActiveStatus);
|
||||
bill.setCreditArrearsStatus(creditArrearsStatus);
|
||||
if (existed == null || StringUtils.isEmpty(bill.getRefundStatus()))
|
||||
@@ -2180,10 +2180,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
else
|
||||
{
|
||||
// Keep manually added fees (manual_flag=1) across rebill
|
||||
// 重算时保留人工添加的费用(manual_flag=1)
|
||||
incomeBillFeeMapper.deleteAutoFeesByBillId(bill.getBillId());
|
||||
}
|
||||
// Payable not in scope yet: skip prc_pay_bill_fee cleanup/write.
|
||||
// 应付暂不在范围内:跳过 prc_pay_bill_fee 的清理/写入。
|
||||
// payBillFeeMapper.deleteByBillId(bill.getBillId());
|
||||
}
|
||||
PrcIncomeBill incomeBill = null;
|
||||
@@ -2211,7 +2211,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
if (existed != null && !billingFailed)
|
||||
{
|
||||
// Quote totals were written above; recalc so retained manual fees join income/credit
|
||||
// 报价合计已在上面写入;重新计算以便保留的人工费用计入收入/授信
|
||||
TmsPriceQuoteResult refreshed = refreshBillIncomeTotals(bill);
|
||||
if (refreshed != null)
|
||||
{
|
||||
@@ -2229,7 +2229,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
result.setRefundStatus(bill.getRefundStatus());
|
||||
orderBillMapper.refreshArBillStatusByBillIds(Arrays.asList(bill.getBillId()));
|
||||
}
|
||||
// Payable not in scope yet: do not write prc_pay_bill / prc_pay_bill_fee.
|
||||
// 应付暂不在范围内:不写入 prc_pay_bill / prc_pay_bill_fee。
|
||||
// PrcPayBill payBill = persistPayBill(bill, result);
|
||||
// List<PrcPayBillFee> payFees = toPayFeeRows(bill.getBillId(),
|
||||
// payBill == null ? null : payBill.getPayBillId(), result);
|
||||
@@ -2244,8 +2244,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop auto fee lines that collide with retained manual rows on uk_bill_fee_item
|
||||
* (bill_id, fee_type, sc_id). Manual fees win on rebill.
|
||||
* 丢弃在 uk_bill_fee_item(bill_id, fee_type, sc_id)上与保留的人工行冲突的自动费用行。
|
||||
* 重算时以人工费用优先。
|
||||
*/
|
||||
private List<PrcIncomeBillFee> filterAutoFeesAgainstManual(String billId, List<PrcIncomeBillFee> autoFees)
|
||||
{
|
||||
@@ -2291,10 +2291,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Occupy/release credit by income total delta (bizType=ORDER).
|
||||
* Missing/disabled customer: skip occupy, return NORMAL.
|
||||
* Insufficient credit: still occupy with checkAvailable=false, return ARREARS.
|
||||
* Always write CREDIT log to ES (SUCCESS / FAIL / SKIP).
|
||||
* 按收入合计的差额占用/释放授信(bizType=ORDER)。
|
||||
* 客户不存在/已停用:跳过占用,返回 NORMAL。
|
||||
* 授信不足:仍以 checkAvailable=false 占用,返回 ARREARS。
|
||||
* 始终向 ES 写入 CREDIT 日志(SUCCESS / FAIL / SKIP)。
|
||||
*/
|
||||
private String syncOrderCredit(PrcOrderBill bill, BigDecimal previousIncome, String customerActiveStatus)
|
||||
{
|
||||
@@ -2328,7 +2328,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
cmd.setCurrency(StringUtils.isEmpty(bill.getCurrency()) ? "CNY" : bill.getCurrency());
|
||||
cmd.setBizType(CustomerFundBizType.ORDER);
|
||||
cmd.setBizNo(bill.getOrderNo());
|
||||
// Soft: allow used_credit to exceed available; mark ARREARS on the bill instead of failing.
|
||||
// 软处理:允许已用授信超过可用额度;在账单上标记 ARREARS 而不是让交易失败。
|
||||
cmd.setCheckAvailable(Boolean.FALSE);
|
||||
String action = delta.compareTo(BigDecimal.ZERO) > 0 ? "OCCUPY" : "RELEASE";
|
||||
boolean markArrears = false;
|
||||
@@ -2354,13 +2354,13 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
// Unexpected occupy/release failure (lock conflict etc.): fail the bill txn.
|
||||
// 意外的占用/释放失败(锁冲突等):使账单事务失败。
|
||||
saveCreditEsLog(bill, cmd, action, false, EsBizLogSupport.errorText(e));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/** True when available credit cannot cover this occupy delta (or already overdue). */
|
||||
/** 可用授信不足以覆盖本次占用差额(或已逾期)时返回 true。 */
|
||||
private boolean isCreditInsufficient(Long customerId, String customerCode, BigDecimal delta)
|
||||
{
|
||||
try
|
||||
@@ -2387,7 +2387,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
}
|
||||
|
||||
/** CREDIT ES log; does not set ATTR_FAIL_LOGGED so BILL fail can still be written. */
|
||||
/** CREDIT 的 ES 日志;不设置 ATTR_FAIL_LOGGED,以便仍能写入 BILL 失败日志。 */
|
||||
private void saveCreditEsLog(PrcOrderBill bill, CreditUsageCommand cmd, String action, boolean success,
|
||||
String errorMessage)
|
||||
{
|
||||
@@ -2399,7 +2399,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
{
|
||||
return;
|
||||
}
|
||||
// bizId: prefer transferNo, fallback orderNo (same as BILL)
|
||||
// bizId:优先 transferNo,其次 orderNo(与 BILL 一致)
|
||||
String bizId = EsBizLogSupport.billBizId(bill.getTransferNo(), bill.getOrderNo());
|
||||
JSONObject req = new JSONObject();
|
||||
req.put("transferNo", bill.getTransferNo());
|
||||
@@ -2436,8 +2436,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Block re-bill when BASIC_FREIGHT fee is already in an AR bill (BILLED).
|
||||
* Source of truth is prc_income_bill_fee.ar_bill_status (not prc_order_bill.ar_bill_status).
|
||||
* 当 BASIC_FREIGHT 费用已在 AR 账单中(BILLED)时,阻止重新计费。
|
||||
* 判断依据是 prc_income_bill_fee.ar_bill_status(而非 prc_order_bill.ar_bill_status)。
|
||||
*/
|
||||
private void assertOrderBillEditable(String orderNo)
|
||||
{
|
||||
@@ -2519,7 +2519,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
row.setCreateBy(bill.getCreateBy());
|
||||
}
|
||||
row.setCurrency(StringUtils.isEmpty(bill.getCurrency()) ? "CNY" : bill.getCurrency());
|
||||
// product catalog + redline from quote
|
||||
// 产品目录 + 报价中的红线金额
|
||||
row.setProductAmount(result == null ? null : result.getProductAmount());
|
||||
row.setRedlineAmount(result == null ? null : result.getRedlineAmount());
|
||||
row.setSalesAmount(sales);
|
||||
@@ -2584,7 +2584,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
row.setCreateBy(bill.getCreateBy());
|
||||
}
|
||||
row.setCurrency(StringUtils.isEmpty(bill.getCurrency()) ? "CNY" : bill.getCurrency());
|
||||
// product = catalog; redline from quote; cost/pay 不再等于产品目录价
|
||||
// product 取目录价;redline 取报价;cost/pay 不再等于产品目录价
|
||||
row.setProductAmount(result == null ? null : result.getProductAmount());
|
||||
row.setRedlineAmount(result == null ? null : result.getRedlineAmount());
|
||||
row.setCostAmount(result == null ? null : result.getCostAmount());
|
||||
@@ -2768,7 +2768,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return null;
|
||||
}
|
||||
obj.remove("apiCode");
|
||||
// Keep upstreamRow (Oracle/TAB_ORDER payload) in ES request for ops troubleshooting.
|
||||
// 在 ES 请求中保留 upstreamRow(Oracle/TAB_ORDER 载荷)便于运维排查。
|
||||
return obj.toJSONString();
|
||||
}
|
||||
|
||||
@@ -2809,7 +2809,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.bill.billIdRequired"));
|
||||
}
|
||||
// data-scope check via list query
|
||||
// 通过列表查询做数据范围校验
|
||||
self.getBill(billId.trim());
|
||||
return self.updateChargeWeightByBillId(billId.trim(), chargeWeight);
|
||||
}
|
||||
@@ -2845,7 +2845,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
throw new ServiceException(MessageUtils.message("price.bill.bizDateRequired"));
|
||||
}
|
||||
TmsPriceQuoteResult result = tmsPriceQuoteService.calculate(req);
|
||||
// keep original system weight; charge_weight becomes the manual override
|
||||
// 保留原始系统重量;charge_weight 作为人工覆盖值
|
||||
persistBill(req, bill.getWeight(), bizDate, result);
|
||||
return toResult(bill.getBillId());
|
||||
}
|
||||
@@ -2890,7 +2890,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
+ CHARGE_WEIGHT_IMPORT_MAX + " 条");
|
||||
}
|
||||
|
||||
// Batch load bills with data-scope (chunked IN), avoid N+1 select + getBill
|
||||
// 批量加载带数据范围的账单(分块 IN),避免 N+1 查询和逐单 getBill
|
||||
java.util.Map<String, PrcOrderBill> billByOrderNo = new java.util.HashMap<String, PrcOrderBill>();
|
||||
List<String> allOrderNos = new ArrayList<String>(dedup.keySet());
|
||||
final int chunkSize = 500;
|
||||
@@ -3080,13 +3080,13 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
payBillFeeMapper.selectByBillId(bill.getBillId()));
|
||||
}
|
||||
|
||||
/** Upstream Oracle row present => sync/billing path that overwrites weight & charge_weight together. */
|
||||
/** 存在上游 Oracle 行 => 同步/计费路径会同时覆盖 weight 与 charge_weight。 */
|
||||
private static boolean isUpstreamSyncRequest(TmsPriceBillRequest request)
|
||||
{
|
||||
return request != null && request.getUpstreamRow() != null && !request.getUpstreamRow().isEmpty();
|
||||
}
|
||||
|
||||
/** charge_weight differs from weight => treated as manual override. */
|
||||
/** charge_weight 与 weight 不同 => 视为人工覆盖。 */
|
||||
private static boolean isManualChargeWeight(PrcOrderBill bill)
|
||||
{
|
||||
if (bill == null || bill.getChargeWeight() == null || bill.getWeight() == null)
|
||||
@@ -3096,7 +3096,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
return bill.getChargeWeight().compareTo(bill.getWeight()) != 0;
|
||||
}
|
||||
|
||||
/** Prefer manual charge_weight when present; otherwise system weight. */
|
||||
/** 存在人工 charge_weight 时优先使用,否则使用系统重量。 */
|
||||
private static BigDecimal resolveCalcWeight(PrcOrderBill existed, BigDecimal systemWeight)
|
||||
{
|
||||
if (isManualChargeWeight(existed))
|
||||
@@ -3107,8 +3107,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve weight used for quote/persist.
|
||||
* Upstream: TAB_ORDER weight then biz-rule. Local: manual charge_weight wins, else biz-rule.
|
||||
* 解析用于报价/持久化的重量。
|
||||
* 上游:先取 TAB_ORDER 重量再按业务规则。本地:人工 charge_weight 优先,否则按业务规则。
|
||||
*/
|
||||
private BigDecimal resolveBillCalcWeight(TmsPriceBillRequest request, PrcOrderBill existed,
|
||||
BigDecimal systemWeight)
|
||||
@@ -3124,7 +3124,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Upstream sync / soft-fail: keep original weight, charge_weight may follow biz-rule.
|
||||
* 上游同步/软失败:保留原始重量,charge_weight 可按业务规则计算。
|
||||
*/
|
||||
private void applyUpstreamWeightFields(PrcOrderBill bill, TmsPriceBillRequest request)
|
||||
{
|
||||
@@ -3138,8 +3138,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Biz-rule charge weight (VOL_8000 etc).
|
||||
* To disable: comment out the resolveWeightByRule call and always return baseWeight.
|
||||
* 业务规则计费重量(VOL_8000 等)。
|
||||
* 如需禁用:注释掉 resolveWeightByRule 调用,始终返回 baseWeight。
|
||||
*/
|
||||
private BigDecimal applyBizRuleWeight(TmsPriceBillRequest request, BigDecimal baseWeight)
|
||||
{
|
||||
@@ -3152,12 +3152,12 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist charge_weight: keep manual override when calc weight != system weight,
|
||||
* or when existing bill already had a manual override.
|
||||
* 持久化 charge_weight:当计算重量 != 系统重量,
|
||||
* 或已有账单本就是人工覆盖时,保留人工覆盖值。
|
||||
*/
|
||||
private static BigDecimal resolvePersistChargeWeight(BigDecimal systemWeight, BigDecimal resultChargeWeight)
|
||||
{
|
||||
// calc weight != system weight => keep result as manual charge_weight
|
||||
// 计算重量 != 系统重量 => 将结果作为人工 charge_weight 保留
|
||||
if (systemWeight != null && resultChargeWeight != null
|
||||
&& systemWeight.compareTo(resultChargeWeight) != 0)
|
||||
{
|
||||
@@ -3216,7 +3216,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer bill.productName; when missing or equal to productCode, recover from ext LINE_NAME / PRODUCT_NAME.
|
||||
* 优先使用 bill.productName;缺失或与 productCode 相同时,从 ext 的 LINE_NAME / PRODUCT_NAME 恢复。
|
||||
*/
|
||||
private String resolveBillProductName(PrcOrderBill bill)
|
||||
{
|
||||
@@ -3269,8 +3269,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Order-source sync: full upstream field overwrite onto local bill + ext.
|
||||
* Does not quote, occupy credit, touch fees, or validate AR / refund.
|
||||
* 订单来源同步:用上游字段完整覆盖本地账单 + ext。
|
||||
* 不做报价、不占用授信、不涉及费用,也不校验 AR/退费。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -3325,7 +3325,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
bill.setBillId(UUID.randomUUID().toString());
|
||||
bill.setArBillStatus(ArOrderBillStatus.UNBILLED.getCode());
|
||||
bill.setRefundStatus(BillRefundStatus.NONE.getCode());
|
||||
// Order-source sync only: mark not billed until quote/bill runs.
|
||||
// 仅订单来源同步:在报价/计费执行前标记为未计费。
|
||||
bill.setBillingFailStatus(Integer.valueOf(BillBillingFailStatus.NOT_BILLED.getCode()));
|
||||
bill.setFreightRate(BigDecimal.ZERO);
|
||||
bill.setBaseFreightRate(BigDecimal.ZERO);
|
||||
@@ -3370,16 +3370,16 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
req.put("billId", bill.getBillId());
|
||||
req.put("orderStatus", bill.getOrderStatus());
|
||||
req.put("creating", Boolean.valueOf(creating));
|
||||
// Persist pulled Oracle TAB_ORDER row for ops; bizId = transferNo (fallback orderNo).
|
||||
// 持久化拉取的 Oracle TAB_ORDER 行以便运维排查;bizId = transferNo(回退 orderNo)。
|
||||
req.put("upstreamRow", upstreamRow);
|
||||
String bizId = EsBizLogSupport.billBizId(bill.getTransferNo(), orderNo);
|
||||
// ES mapping has response.ok as long; use 1/0 not boolean true/false.
|
||||
// ES 映射中 response.ok 为 long 类型,使用 1/0 而非布尔 true/false。
|
||||
billSearchStore.save(EsLogType.BILL, bizId, req.toJSONString(),
|
||||
"{\"ok\":1,\"action\":\"UPSTREAM_SYNC\"}");
|
||||
return bill.getBillId();
|
||||
}
|
||||
|
||||
/** Best-effort customer id by code; keep previous id when lookup misses. */
|
||||
/** 按编码尽力查询客户 ID;查询不到时保留原 ID。 */
|
||||
private Long resolveCustomerIdByCode(String customerCode, Long previousId)
|
||||
{
|
||||
if (StringUtils.isEmpty(customerCode))
|
||||
@@ -3430,7 +3430,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
orderNo = orderNo.trim();
|
||||
request.setOrderNo(orderNo);
|
||||
PrcOrderBill existed = orderBillMapper.selectPrcOrderBillByOrderNo(orderNo);
|
||||
// Already in AR: still record fail + ORDER_STATUS; do not touch amounts/fees.
|
||||
// 已在 AR 中:仍记录失败和 ORDER_STATUS;不改动金额/费用。
|
||||
if (isAlreadyInArBill(existed))
|
||||
{
|
||||
persistFailedSnapshotWhenLocked(existed, request, billingFailStatus, errorMessage);
|
||||
@@ -3501,7 +3501,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
incomeBillMapper.deleteByBillId(bill.getBillId());
|
||||
}
|
||||
persistBillExt(bill, request.getUpstreamRow());
|
||||
// Soft-fail path also writes ES (bizId = transferNo) so upstream pull is searchable.
|
||||
// 软失败路径也写 ES(bizId = transferNo),以便上游拉取记录可检索。
|
||||
JSONObject failReq = new JSONObject();
|
||||
failReq.put("action", "BILL_SOFT_FAIL");
|
||||
failReq.put("orderNo", orderNo);
|
||||
@@ -3517,7 +3517,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Locked by AR bill: overwrite upstream header + ext + weight/charge_weight; keep amounts / fees.
|
||||
* 已被 AR 账单锁定:覆盖上游表头 + ext + weight/charge_weight;保留金额/费用。
|
||||
*/
|
||||
private void persistFailedSnapshotWhenLocked(PrcOrderBill bill, TmsPriceBillRequest request,
|
||||
int billingFailStatus, String errorMessage)
|
||||
@@ -3549,8 +3549,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite local bill header fields from upstream request (no validation).
|
||||
* Does not set weight / charge_weight / bizDate / amounts / customerId.
|
||||
* 用上游请求覆盖本地账单表头字段(不做校验)。
|
||||
* 不设置 weight / charge_weight / bizDate / 金额 / customerId。
|
||||
*/
|
||||
private void applyUpstreamBillHeaderFields(PrcOrderBill bill, TmsPriceBillRequest request)
|
||||
{
|
||||
@@ -3584,7 +3584,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
bill.setOrderTime(request.getOrderTime());
|
||||
applyUpstreamOrderStatus(bill, request);
|
||||
bill.setOrderRemark(request.getOrderRemark());
|
||||
// Source = TAB_ORDER.LINE_CODE / LINE_NAME (mapping applied by caller after quote / sync).
|
||||
// 来源 = TAB_ORDER.LINE_CODE / LINE_NAME(调用方在报价/同步后再套用映射)。
|
||||
bill.setProductCode(request.getProductCode());
|
||||
bill.setProductName(request.getProductName());
|
||||
bill.setCountryCode(request.getCountryCode());
|
||||
@@ -3592,7 +3592,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* LINE_CODE (on request/bill) -> BMS product via product_code_mapping when configured.
|
||||
* LINE_CODE(请求/账单上的)-> 已配置 product_code_mapping 时映射为 BMS 产品。
|
||||
*/
|
||||
private void applyProductCodeMapping(PrcOrderBill bill, TmsPriceBillRequest request)
|
||||
{
|
||||
@@ -3623,9 +3623,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer request status fields; never clear existing order_status with null.
|
||||
* Do not pin order_status to 9/10 when refundStatus=REFUNDED — rebill / upstream
|
||||
* bill must follow Oracle ORDER_STATUS. Actual fee refund still sets 9 in {@link #refund}.
|
||||
* 优先使用请求中的状态字段;绝不用 null 清空已有的 order_status。
|
||||
* refundStatus=REFUNDED 时不要将 order_status 固定为 9/10——重算/上游
|
||||
* 计费必须遵循 Oracle ORDER_STATUS。实际费用退费仍会在 {@link #refund} 中置为 9。
|
||||
*/
|
||||
private static void applyUpstreamOrderStatus(PrcOrderBill bill, TmsPriceBillRequest request)
|
||||
{
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ import com.bms.price.service.IPrcPriceExcelFileService;
|
||||
@Service
|
||||
public class PrcPriceExcelFileServiceImpl implements IPrcPriceExcelFileService
|
||||
{
|
||||
/** Object key prefix under configured OSS pathPrefix. */
|
||||
/** 配置的 OSS pathPrefix 下的对象 key 前缀。 */
|
||||
private static final String OBJECT_DIR = "price/excel";
|
||||
|
||||
@Autowired
|
||||
@@ -95,7 +95,7 @@ public class PrcPriceExcelFileServiceImpl implements IPrcPriceExcelFileService
|
||||
row.setPsvId(psvId);
|
||||
row.setEffectiveTime(effectiveTime);
|
||||
row.setFileName(originalName);
|
||||
// Persist oss://bucket/key like AR bill storageKey (download via /common/download/resource).
|
||||
// 按 AR 账单 storageKey 的方式持久化 oss://bucket/key(通过 /common/download/resource 下载)。
|
||||
row.setFilePath(storageKey);
|
||||
row.setFileSize(Long.valueOf(bytes.length));
|
||||
row.setContentType(contentType);
|
||||
|
||||
+103
-103
@@ -68,7 +68,7 @@ import com.bms.customer.customerinfo.service.IBmsCustomerService;
|
||||
@Service
|
||||
public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
/** Package status: D pending-enable / A audit / E effective / R reject / S stopped */
|
||||
/** 套餐状态:D 待启用 / A 审核中 / E 已生效 / R 已驳回 / S 已停用 */
|
||||
private static final String STATUS_DRAFT = "D";
|
||||
private static final String STATUS_AUDIT = "A";
|
||||
private static final String STATUS_EFFECT = "E";
|
||||
@@ -81,15 +81,15 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
private static final String TYPE_DISCOUNT = "DISCOUNT";
|
||||
private static final String VER_DRAFT = "0";
|
||||
private static final String VER_EFFECT = "1";
|
||||
/** Pending: effective_time set but not yet reached */
|
||||
/** 待生效:已设置 effective_time 但尚未到达 */
|
||||
private static final String VER_PENDING = "3";
|
||||
private static final String VER_EXPIRED = "2";
|
||||
/** Bulk detail insert chunk size for create/finish async save. */
|
||||
/** 创建/完成异步保存时明细批量插入的分片大小。 */
|
||||
private static final int DETAIL_INSERT_BATCH = 200;
|
||||
/** Request-scoped cache: productCode -> open product_country rows (bulk upsert). */
|
||||
/** 请求级缓存:productCode -> 未过期的 product_country 行(批量 upsert)。 */
|
||||
private final ThreadLocal<Map<String, List<BmsProductCountry>>> countryCacheTl =
|
||||
new ThreadLocal<Map<String, List<BmsProductCountry>>>();
|
||||
/** Request-scoped cache: productCode -> psp_id (bulk upsert). */
|
||||
/** 请求级缓存:productCode -> psp_id(批量 upsert)。 */
|
||||
private final ThreadLocal<Map<String, Long>> setProductCacheTl =
|
||||
new ThreadLocal<Map<String, Long>>();
|
||||
|
||||
@@ -152,7 +152,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
PrcPriceSet set = setMapper.selectPrcPriceSetById(priceSetId);
|
||||
if (set != null)
|
||||
{
|
||||
// Reconcile version status/expire windows so UI always shows at most one EFFECT
|
||||
// 校准版本状态/失效时间窗口,确保 UI 最多只显示一个 EFFECT 版本
|
||||
refreshVersionStatus(priceSetId);
|
||||
fillExpired(set);
|
||||
fillVersions(set);
|
||||
@@ -161,8 +161,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Price-set list data scope: bound customer's sales user (same as customer list)
|
||||
* or package creator. Super admin skips filter in DataScopeAspect.
|
||||
* 价格套餐列表数据范围:绑定客户的销售用户(与客户列表一致)
|
||||
* 或套餐创建人。超级管理员在 DataScopeAspect 中跳过过滤。
|
||||
*/
|
||||
@Override
|
||||
@DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id",
|
||||
@@ -209,7 +209,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Always replace SALES rels for this batch API (null would skip in updateDetail)
|
||||
// 该批量接口始终替换 SALES 关联(传 null 会在 updateDetail 中被跳过)
|
||||
if (detail.getSurcharges() == null)
|
||||
{
|
||||
detail.setSurcharges(new ArrayList<PrcSurchargeRel>());
|
||||
@@ -263,13 +263,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
PrcPriceSet db = requireSet(set.getPriceSetId());
|
||||
assertBasicEditable(db);
|
||||
set.setStatus(db.getStatus());
|
||||
// Category is immutable after create; keep DB value for name uniqueness scope.
|
||||
// 分类创建后不可变;保留 DB 中的值以确定名称唯一性范围。
|
||||
set.setSetCategory(normalizeSetCategory(db.getSetCategory()));
|
||||
setMapper.updatePrcPriceSet(set);
|
||||
}
|
||||
reportSaveProgress(set, 20, "saving version");
|
||||
replaceChildren(set);
|
||||
// submitFlag / audit flow removed: versions activate via activateVersion
|
||||
// submitFlag / 审核流程已移除:版本通过 activateVersion 生效
|
||||
return set.getPriceSetId();
|
||||
}
|
||||
|
||||
@@ -440,8 +440,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
boolean importFresh = "IMPORT_FRESH".equalsIgnoreCase(StringUtils.trim(version.getPriceBuildMode()));
|
||||
if (importFresh)
|
||||
{
|
||||
// May start inside a prior version window; forbid equal to any other version's eff/exp.
|
||||
// refreshVersionStatus then sets B.expire=A.eff, A.expire=B.oldExpire and syncs children.
|
||||
// 可能从上一个版本窗口内部开始;禁止与任何其他版本的生效/失效时间相同。
|
||||
// 随后 refreshVersionStatus 会设置 B.expire=A.eff、A.expire=B.oldExpire,并同步子记录。
|
||||
validateImportFreshActivateTime(version, effectiveTime);
|
||||
}
|
||||
else
|
||||
@@ -452,7 +452,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.effectiveTime.beforePrev"));
|
||||
}
|
||||
}
|
||||
// Set schedule; refreshVersionStatus decides PENDING(3) vs EFFECT(1) and chains expire_time
|
||||
// 设置定时;由 refreshVersionStatus 判定 PENDING(3) 还是 EFFECT(1),并串联 expire_time
|
||||
version.setEffectiveTime(effectiveTime);
|
||||
version.setExpireTime(null);
|
||||
version.setUpdateBy(updateBy);
|
||||
@@ -462,8 +462,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* IMPORT_FRESH activate: allow effective time earlier than prev-by-seq (incl. inside prior window B).
|
||||
* Must not equal any other timed version's effectiveTime or expireTime.
|
||||
* IMPORT_FRESH 生效:允许生效时间早于按 seq 排序的前一版本(包括落在前一个窗口 B 内)。
|
||||
* 但不得与任何其他定时版本的 effectiveTime 或 expireTime 相同。
|
||||
*/
|
||||
private void validateImportFreshActivateTime(PrcPriceSetVersion version, Date effectiveTime)
|
||||
{
|
||||
@@ -496,7 +496,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
}
|
||||
|
||||
/** Previous version (lower version_seq) effective_time; null if none. */
|
||||
/** 前一版本(version_seq 更小)的 effective_time;不存在时返回 null。 */
|
||||
private Date findPrevVersionEffectiveTime(PrcPriceSetVersion version)
|
||||
{
|
||||
if (version == null || version.getPriceSetId() == null)
|
||||
@@ -631,7 +631,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.notExists"));
|
||||
}
|
||||
// Draft / pending / effect may be deleted; expired stays view-only.
|
||||
// 草稿/待生效/已生效版本可删除;已过期版本仅可查看。
|
||||
if (!VER_DRAFT.equals(version.getStatus())
|
||||
&& !VER_PENDING.equals(version.getStatus())
|
||||
&& !VER_EFFECT.equals(version.getStatus()))
|
||||
@@ -639,11 +639,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
throw new ServiceException(MessageUtils.message("price.set.version.delete.statusNotAllowed"));
|
||||
}
|
||||
requireSet(version.getPriceSetId());
|
||||
// Products / details / surcharge always physical delete.
|
||||
// 产品/明细/附加费始终物理删除。
|
||||
surchargeRelMapper.physicalDeleteByPsvId(psvId);
|
||||
detailMapper.physicalDeleteByPsvId(psvId);
|
||||
setProductMapper.physicalDeleteByPsvId(psvId);
|
||||
// Version: soft-delete when any order bill references psv_id; otherwise hard-delete.
|
||||
// 版本:有订单账单引用 psv_id 时软删除;否则物理删除。
|
||||
int rows;
|
||||
if (orderBillMapper.countByPsvId(psvId) > 0)
|
||||
{
|
||||
@@ -653,7 +653,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
rows = versionMapper.deletePrcPriceSetVersionByIds(new Long[] { psvId });
|
||||
}
|
||||
// Rechain expire_time / status for remaining versions (reopen previous effect window)
|
||||
// 重新串联剩余版本的 expire_time/status(重新打开前一个生效窗口)
|
||||
refreshVersionStatus(version.getPriceSetId());
|
||||
return rows;
|
||||
}
|
||||
@@ -999,7 +999,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Attach SALES surcharge rels (detail_id=psd_id) to a page of details. */
|
||||
/** 为一页明细挂载 SALES 附加费关联(detail_id=psd_id)。 */
|
||||
private void fillDetailSurcharges(List<PrcPriceSetDetail> details)
|
||||
{
|
||||
if (details == null || details.isEmpty())
|
||||
@@ -1180,7 +1180,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
set.setUpdateBy(operName);
|
||||
set.setCreateBy(operName);
|
||||
set.setSaveTaskId(taskId);
|
||||
// Dedicated pull API — does not go through savePrcPriceSet.
|
||||
// 专用拉取接口——不经过 savePrcPriceSet。
|
||||
set.setPullFromLibrary(null);
|
||||
set.setForcePullLibrary(force ? "Y" : "N");
|
||||
if (psvId == null)
|
||||
@@ -1255,7 +1255,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
List<PrcPriceSetDetail> details = new ArrayList<PrcPriceSetDetail>();
|
||||
// Keep prior draft rows only for updateOnly or Excel build-price merge.
|
||||
// 仅在 updateOnly 或 Excel 建价合并时保留原有草稿行。
|
||||
if (existingDetails != null && (updateOnly || replaceScope))
|
||||
{
|
||||
for (PrcPriceSetDetail d : existingDetails)
|
||||
@@ -1274,7 +1274,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
else if (replaceScope && !details.isEmpty())
|
||||
{
|
||||
// Package Excel build-price: replace only product+country scopes in this file.
|
||||
// 套餐 Excel 建价:仅替换本文件涉及的 product+country 范围。
|
||||
removeDetailsByReplaceScopes(details, detailIndex, collectImportReplaceScopes(byProduct));
|
||||
}
|
||||
String operName = null;
|
||||
@@ -1391,7 +1391,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.import.noDetailWritten"));
|
||||
}
|
||||
// Keep product list from existing details codes
|
||||
// 依据已有明细的产品编码保留产品列表
|
||||
for (PrcPriceSetDetail d : details)
|
||||
{
|
||||
if (d != null && StringUtils.isNotEmpty(d.getProductCode()))
|
||||
@@ -1493,7 +1493,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
if (StringUtils.isNotEmpty(countryCode))
|
||||
{
|
||||
throw new ServiceException(
|
||||
"\u4e13\u5c5e\u62a5\u4ef7\u4e0d\u652f\u6301\u5355\u56fd\u5bfc\u5165\uff0c\u8bf7\u4e0a\u4f20\u5b8c\u6574\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868");
|
||||
"专属报价不支持单国导入,请上传完整大客折扣报价表");
|
||||
}
|
||||
return exclusiveHalaraQuoteExcelParser.parse(bytes, filename);
|
||||
}
|
||||
@@ -1603,7 +1603,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
public String importDetailPrices(Long priceSetId, Long psvId, byte[] bytes, String filename, String operName,
|
||||
String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode) throws Exception
|
||||
{
|
||||
// Async import has no SecurityContext; bind operName for PriceChangeEsLog.operator().
|
||||
// 异步导入没有 SecurityContext;为 PriceChangeEsLog.operator() 绑定 operName。
|
||||
PriceChangeEsLog.beginOperator(operName);
|
||||
try
|
||||
{
|
||||
@@ -1660,7 +1660,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
else if (replaceScope && !details.isEmpty())
|
||||
{
|
||||
// Package Excel build-price only.
|
||||
// 仅用于套餐 Excel 建价。
|
||||
Set<String> replaceScopes = collectImportReplaceScopes(byProduct);
|
||||
List<Long> deleteIds = new ArrayList<Long>();
|
||||
for (PrcPriceSetDetail d : details)
|
||||
@@ -1685,7 +1685,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
beforeReplace.put(d.getPsdId(), d);
|
||||
}
|
||||
}
|
||||
// Draft/pending: hard delete covered rows; effect/expired: soft delete.
|
||||
// 草稿/待生效:物理删除被覆盖的行;已生效/已过期:软删除。
|
||||
deleteDetailRowsByIds(deleteIds, isDraftOrPendingVersionStatus(resolveVersionStatus(psvId)));
|
||||
for (Long id : deleteIds)
|
||||
{
|
||||
@@ -1907,7 +1907,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
saveAsyncRunner.put(taskId, PrcPriceSetSaveTask.STATUS_RUNNING, percent, message, priceSetId, null);
|
||||
}
|
||||
|
||||
/** Resolve editable version id: draft > pending > effect > first version. */
|
||||
/** 解析可编辑的版本 ID:草稿 > 待生效 > 已生效 > 第一个版本。 */
|
||||
private Long resolveCurrentPsvId(PrcPriceSet set)
|
||||
{
|
||||
if (set == null)
|
||||
@@ -1981,7 +1981,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Group Excel rows by country (code preferred, else name)
|
||||
// 按国家分组 Excel 行(优先用编码,否则用名称)
|
||||
Map<String, List<BmsProductCountry>> excelGroups = new LinkedHashMap<String, List<BmsProductCountry>>();
|
||||
for (BmsProductCountry row : e.getValue())
|
||||
{
|
||||
@@ -2109,8 +2109,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace scope for upsert import: productCode + country (code preferred, else name).
|
||||
* Rows in Excel for a scope replace all existing details of that product+country.
|
||||
* upsert 导入的替换范围:productCode + 国家(优先用编码,否则用名称)。
|
||||
* Excel 中某范围的行会替换该 product+country 下的所有已有明细。
|
||||
*/
|
||||
private String importCountryScopeKey(String productCode, String countryCode, String countryName)
|
||||
{
|
||||
@@ -2238,7 +2238,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(detailZone))
|
||||
{
|
||||
// Excel has no zone: only match non-zone details
|
||||
// Excel 没有分区:仅匹配无分区的明细
|
||||
continue;
|
||||
}
|
||||
hits.add(d);
|
||||
@@ -2315,7 +2315,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
detail.setPspId(pspId);
|
||||
int rows = detailMapper.updatePrcPriceSetDetail(detail);
|
||||
// non-null surcharges = replace SALES rels for this detail (empty list clears)
|
||||
// surcharges 非 null 表示替换该明细的 SALES 关联(空列表表示清空)
|
||||
if (detail.getSurcharges() != null)
|
||||
{
|
||||
replaceSalesSurchargeRels(java.util.Collections.singletonList(detail));
|
||||
@@ -2399,8 +2399,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete detail rows (+ SALES surcharge rels).
|
||||
* hard=true: physical delete (draft/pending); false: soft delete (effect/expired).
|
||||
* 删除明细行(含 SALES 附加费关联)。
|
||||
* hard=true:物理删除(草稿/待生效);false:软删除(已生效/已过期)。
|
||||
*/
|
||||
private int deleteDetailRowsByIds(List<Long> ids, boolean hard)
|
||||
{
|
||||
@@ -2418,7 +2418,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return detailMapper.deletePrcPriceSetDetailByIds(arr);
|
||||
}
|
||||
|
||||
/** Snapshot freight/registration for import before-after log. */
|
||||
/** 为导入前后对比日志生成运费/挂号费快照。 */
|
||||
private PrcPriceSetDetail buildPriceSnapshot(BigDecimal fvValue, BigDecimal registrationFee)
|
||||
{
|
||||
PrcPriceSetDetail snap = new PrcPriceSetDetail();
|
||||
@@ -2427,7 +2427,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return snap;
|
||||
}
|
||||
|
||||
/** Version status draft(0) / pending(3): hard delete children. */
|
||||
/** 版本状态为草稿(0)/待生效(3):硬删除子数据。 */
|
||||
private boolean isDraftOrPendingVersionStatus(String status)
|
||||
{
|
||||
return VER_DRAFT.equals(status) || VER_PENDING.equals(status);
|
||||
@@ -2444,8 +2444,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* After detail delete: if no detail left for product, remove set-product + its SALES rels.
|
||||
* hard=true uses physical delete (draft/pending); otherwise soft delete (effect/expired).
|
||||
* 明细删除后:若产品下已无明细,则移除套餐产品及其 SALES 关联。
|
||||
* hard=true 使用物理删除(草稿/待生效);否则软删除(已生效/已过期)。
|
||||
*/
|
||||
private void cleanupOrphanSetProducts(Set<String> touchKeys, boolean hard)
|
||||
{
|
||||
@@ -2494,7 +2494,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
}
|
||||
|
||||
/** Wipe version products/details/SALES rels: physical for draft/pending, soft otherwise. */
|
||||
/** 清空版本的产品/明细/SALES 关联:草稿/待生效为物理删除,其他为软删除。 */
|
||||
private void wipeVersionChildren(Long psvId, String versionStatus)
|
||||
{
|
||||
if (psvId == null)
|
||||
@@ -2744,7 +2744,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
reportSaveProgress(set, 96, "finishing");
|
||||
}
|
||||
|
||||
/** Upsert customer binds by customerPricesetId / customerCode; delete removed ones. */
|
||||
/** 按 customerPricesetId / customerCode upsert 客户绑定,并删除被移除的绑定。 */
|
||||
private void upsertCustomers(PrcPriceSet set)
|
||||
{
|
||||
PrcCustomerPriceSet cquery = new PrcCustomerPriceSet();
|
||||
@@ -2846,7 +2846,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
}
|
||||
|
||||
/** Write customer biz log for package bind / rebind (visible in customer info Logs). */
|
||||
/** 记录套餐绑定/重新绑定的客户业务日志(可在客户信息日志中查看)。 */
|
||||
private void logCustomerPriceSetBind(PrcCustomerPriceSet existing, PrcCustomerPriceSet bind, PrcPriceSet set)
|
||||
{
|
||||
if (customerLogService == null || bind == null || set == null)
|
||||
@@ -2977,7 +2977,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
}
|
||||
|
||||
/** PACKAGE (default) or EXCLUSIVE. */
|
||||
/** PACKAGE(默认)或 EXCLUSIVE。 */
|
||||
public static String normalizeSetCategory(String setCategory)
|
||||
{
|
||||
String raw = StringUtils.trim(setCategory);
|
||||
@@ -3000,7 +3000,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return String.format("%s%04d", prefix, priceSetId);
|
||||
}
|
||||
|
||||
/** Ensure the package belongs to the expected module category. */
|
||||
/** 确保套餐属于预期的模块分类。 */
|
||||
public void assertSetCategory(Long priceSetId, String expectedCategory)
|
||||
{
|
||||
PrcPriceSet db = requireSet(priceSetId);
|
||||
@@ -3056,10 +3056,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
}
|
||||
|
||||
/** Package freight floor vs imported base — temporarily disabled. */
|
||||
/** 套餐运费与导入底价的比较校验——暂时禁用。 */
|
||||
private void assertNotBelowImportedPrice(PrcPriceSetDetail detail)
|
||||
{
|
||||
// Temporarily allow package freight below imported base price.
|
||||
// 暂时允许套餐运费低于导入底价。
|
||||
// BmsProductCountry base = findImportedPrice(detail);
|
||||
// if (base == null)
|
||||
// {
|
||||
@@ -3135,7 +3135,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* When product country rows for this country have zones, zone is required.
|
||||
* 当该国家的 product country 行带有分区时,必须填写分区。
|
||||
*/
|
||||
private void requireZoneIfNeeded(PrcPriceSetDetail detail)
|
||||
{
|
||||
@@ -3158,13 +3158,13 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve / verify product_country_id from product_country when missing.
|
||||
* product_country_id 缺失时,从 product_country 解析/校验。
|
||||
*/
|
||||
private void fillProductCountryId(PrcPriceSetDetail detail)
|
||||
{
|
||||
if (detail.getProductCountryId() != null)
|
||||
{
|
||||
// Preview/pull already stamped weight bounds — skip per-row product_country reload.
|
||||
// 预览/拉取时已写入重量边界——跳过逐行重新加载 product_country。
|
||||
if (detail.getWeightMin() == null || detail.getWeightMax() == null)
|
||||
{
|
||||
syncZoneFromProductCountry(detail);
|
||||
@@ -3205,7 +3205,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
List<BmsProductCountry> rows = listOpenProductCountries(detail.getProductCode());
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
// Fallback: include expired rows when open-only cache misses
|
||||
// 兜底:仅查未过期数据的缓存未命中时,包含已过期的行
|
||||
BmsProductCountry query = new BmsProductCountry();
|
||||
query.setProductCode(detail.getProductCode());
|
||||
rows = productCountryMapper.selectBmsProductCountryList(query);
|
||||
@@ -3364,8 +3364,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.set.customer.customerId.empty"));
|
||||
}
|
||||
// UI no longer collects bind dates: fall back to set / version / today.
|
||||
// Do not reject empty begin date — always fill a value.
|
||||
// UI 不再收集绑定日期:依次回退到套餐/版本/当天。
|
||||
// 不拒绝为空的开始日期——始终填入一个值。
|
||||
if (customer.getPrBegindate() == null)
|
||||
{
|
||||
customer.setPrBegindate(set.getEffectiveDate());
|
||||
@@ -3382,7 +3382,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
customer.setPrEnddate(set.getExpireDate());
|
||||
}
|
||||
// Auto-filled dates may conflict (e.g. set expire already past); keep bind, clear end.
|
||||
// 自动填充的日期可能冲突(例如套餐失效日期已过);保留绑定,清空结束日期。
|
||||
if (customer.getPrEnddate() != null && customer.getPrBegindate() != null
|
||||
&& customer.getPrBegindate().after(customer.getPrEnddate()))
|
||||
{
|
||||
@@ -3444,7 +3444,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
|
||||
/**
|
||||
* Detect duplicate details; FIXED key = product + country + zone + weight (or productCountryId).
|
||||
* 检测重复明细;FIXED 的键 = 产品 + 国家 + 分区 + 重量(或 productCountryId)。
|
||||
*/
|
||||
private List<String> findDetailDuplicateTips(String setType, List<PrcPriceSetDetail> details)
|
||||
{
|
||||
@@ -3546,7 +3546,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
|
||||
private PrcPriceSet requireSet(Long priceSetId)
|
||||
{
|
||||
// Header only: avoid count(detail) waiting on long pull/import locks.
|
||||
// 仅查表头:避免 count(detail) 被长时间的拉取/导入锁阻塞。
|
||||
PrcPriceSet db = setMapper.selectPrcPriceSetHeaderById(priceSetId);
|
||||
if (db == null)
|
||||
{
|
||||
@@ -3634,7 +3634,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
fillVersionChildren(version);
|
||||
}
|
||||
// Soft-deleted EFFECT versions for the "deleted" tab (status may still be EFFECT)
|
||||
// “已删除”页签中的软删除 EFFECT 版本(状态可能仍是 EFFECT)
|
||||
PrcPriceSetVersion dq = new PrcPriceSetVersion();
|
||||
dq.setPriceSetId(priceSetId);
|
||||
dq.setPriceType(com.bms.price.domain.PriceType.SALES);
|
||||
@@ -3653,7 +3653,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
set.setVersions(versions);
|
||||
PrcPriceSetVersion current = pickCurrentVersion(versions);
|
||||
set.setVersion(current);
|
||||
// Do not load full details here (large sets). Front loads by product via /detail/byProduct.
|
||||
// 此处不加载完整明细(数据量大)。前端通过 /detail/byProduct 按产品加载。
|
||||
set.setDetails(new ArrayList<PrcPriceSetDetail>());
|
||||
PrcCustomerPriceSet cquery = new PrcCustomerPriceSet();
|
||||
cquery.setPriceSetId(priceSetId);
|
||||
@@ -3666,7 +3666,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
pq.setPsvId(version.getPsvId());
|
||||
List<PrcPriceSetProduct> products = setProductMapper.selectPrcPriceSetProductList(pq);
|
||||
version.setProducts(products == null ? new ArrayList<PrcPriceSetProduct>() : products);
|
||||
// details + SALES surcharge rels are loaded on demand (edit price / detail drawer)
|
||||
// 明细 + SALES 附加费关联按需加载(编辑价格/明细抽屉)
|
||||
version.setDetails(new ArrayList<PrcPriceSetDetail>());
|
||||
}
|
||||
|
||||
@@ -3698,11 +3698,11 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
private void saveOrUpdateVersion(PrcPriceSet set)
|
||||
{
|
||||
PrcPriceSetVersion version = resolveSaveVersion(set);
|
||||
// null details = leave existing rows alone; empty list = clear / replace with nothing.
|
||||
// Library pull must NOT run here — only via pullFromLibrary() / Excel import on Step 2.
|
||||
// details 为 null = 保持现有行不变;空列表 = 清空/不保留任何行。
|
||||
// 此处绝不能执行库拉取——只能通过 pullFromLibrary() 或 Step 2 的 Excel 导入。
|
||||
boolean detailsProvided = set.getDetails() != null;
|
||||
List<PrcPriceSetDetail> details = filterDetails(set.getDetails());
|
||||
// Allow creating an empty draft version when client sends version (Step1 -> Step2).
|
||||
// 客户端传入版本时允许创建空草稿版本(Step1 -> Step2)。
|
||||
if (version == null && !detailsProvided)
|
||||
{
|
||||
return;
|
||||
@@ -3712,12 +3712,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
version = new PrcPriceSetVersion();
|
||||
version.setEffectiveTime(set.getEffectiveDate());
|
||||
}
|
||||
// Draft versions may omit effective_time; set on activate
|
||||
// 草稿版本可以不填 effective_time;在生效时设置
|
||||
version.setPriceSetId(set.getPriceSetId());
|
||||
version.setPriceType(com.bms.price.domain.PriceType.SALES);
|
||||
version.setCreateBy(set.getCreateBy());
|
||||
version.setUpdateBy(set.getUpdateBy());
|
||||
// Reuse existing version when client omitted psvId (avoids V2/V3 + leftover details)
|
||||
// 客户端未传 psvId 时复用已有版本(避免产生 V2/V3 及残留明细)
|
||||
if (version.getPsvId() == null && set.getPriceSetId() != null)
|
||||
{
|
||||
PrcPriceSetVersion reusable = findReusableVersion(set.getPriceSetId(), version.getEffectiveTime());
|
||||
@@ -3752,7 +3752,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
version.setExpireTime(db.getExpireTime());
|
||||
}
|
||||
// Draft + build mode changed: wipe old product/detail/surcharge before mode switch
|
||||
// 草稿且建价模式变更:切换模式前清空旧的产品/明细/附加费
|
||||
clearDraftVersionChildrenIfBuildModeChanged(db, version);
|
||||
versionMapper.updatePrcPriceSetVersion(version);
|
||||
}
|
||||
@@ -3793,10 +3793,10 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Draft only: when priceBuildMode changes (PULL/IMPORT/IMPORT_FRESH), delete this version's
|
||||
* product / detail / SALES surcharge rel rows. Same mode or non-draft: no-op.
|
||||
* IMPORT: after wipe, copy products+details from latest prior version (if any).
|
||||
* IMPORT_FRESH: wipe only, do not copy prior version (blank Excel build).
|
||||
* 仅草稿:当 priceBuildMode 变更时(PULL/IMPORT/IMPORT_FRESH),删除该版本的
|
||||
* 产品/明细/SALES 附加费关联行。模式相同或非草稿:不做处理。
|
||||
* IMPORT:清空后从最近的前一版本复制产品+明细(如有)。
|
||||
* IMPORT_FRESH:只清空,不复制前一版本(空白 Excel 建价)。
|
||||
*/
|
||||
private void clearDraftVersionChildrenIfBuildModeChanged(PrcPriceSetVersion db, PrcPriceSetVersion incoming)
|
||||
{
|
||||
@@ -3820,7 +3820,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
Long psvId = db.getPsvId();
|
||||
wipeVersionChildren(psvId, db.getStatus());
|
||||
// IMPORT inherits prior version; IMPORT_FRESH starts empty for Excel import
|
||||
// IMPORT 继承前一版本;IMPORT_FRESH 为空,用于 Excel 导入
|
||||
if ("IMPORT".equalsIgnoreCase(newMode))
|
||||
{
|
||||
copyLatestPriorVersionChildren(db.getPriceSetId(), psvId);
|
||||
@@ -3828,8 +3828,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy products + details from latest prior version into the draft (Excel build-price base).
|
||||
* Prefer EFFECT, then PENDING, then highest versionSeq.
|
||||
* 将最近的前一版本的产品+明细复制到草稿中(Excel 建价的基准)。
|
||||
* 优先 EFFECT,其次 PENDING,最后取 versionSeq 最大者。
|
||||
*/
|
||||
private void copyLatestPriorVersionChildren(Long priceSetId, Long targetPsvId)
|
||||
{
|
||||
@@ -3936,8 +3936,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer same effective_time, else an existing draft.
|
||||
* Never fall back to EFFECT/EXPIRED — that would overwrite a live version when creating a new one.
|
||||
* 优先使用相同 effective_time 的版本,否则使用已有的草稿。
|
||||
* 绝不回退到 EFFECT/EXPIRED——新建版本时会覆盖正在使用的线上版本。
|
||||
*/
|
||||
private PrcPriceSetVersion findReusableVersion(Long priceSetId, Date effectiveTime)
|
||||
{
|
||||
@@ -3970,9 +3970,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk create details via SQL joins (no per-product HTTP roundtrips).
|
||||
* Idempotent: if this version already has details and forcePullLibrary!=Y, skip re-insert.
|
||||
* SALES surcharge rels are not auto-created; configure via package surcharge picker.
|
||||
* 通过 SQL 关联批量创建明细(不做逐产品的 HTTP 往返)。
|
||||
* 幂等:若该版本已有明细且 forcePullLibrary!=Y,则跳过重新插入。
|
||||
* SALES 附加费关联不会自动创建;需通过套餐附加费选择器配置。
|
||||
*/
|
||||
private void replaceVersionDetailsFromLibrary(PrcPriceSet set, PrcPriceSetVersion version)
|
||||
{
|
||||
@@ -3987,7 +3987,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return;
|
||||
}
|
||||
wipeVersionChildren(version.getPsvId(), version.getStatus());
|
||||
// clean legacy rows without psv_id under this set (would look like duplicates in UI)
|
||||
// 清理该套餐下没有 psv_id 的历史遗留行(否则在 UI 中会显示为重复)
|
||||
detailMapper.deleteOrphanDetailsByPriceSetId(set.getPriceSetId());
|
||||
int productRows = setProductMapper.insertFromLibrary(version.getPsvId());
|
||||
if (productRows <= 0)
|
||||
@@ -4002,7 +4002,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prefer currently effective open country rows; fallback asOf window if needed
|
||||
// 优先使用当前生效且未过期的国家行;必要时回退到 asOf 时间窗口
|
||||
inserted = detailMapper.insertFixedDetailsFromLibrary(version.getPsvId(), null, true);
|
||||
if (inserted <= 0)
|
||||
{
|
||||
@@ -4021,9 +4021,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert details by psdId. Keeps existing rows when id matches; inserts new; deletes orphans
|
||||
* not present in the payload. Avoids full delete+reinsert.
|
||||
* Create/finish path: caches set-product + product_country lookups, batch-inserts new rows.
|
||||
* 按 psdId upsert 明细。id 匹配时保留已有行;插入新行;删除载荷中不存在的孤儿行。
|
||||
* 避免整体删除再重新插入。
|
||||
* 创建/完成路径:缓存 set-product + product_country 查询结果,批量插入新行。
|
||||
*/
|
||||
private void upsertVersionDetails(PrcPriceSet set, PrcPriceSetVersion version, List<PrcPriceSetDetail> details)
|
||||
{
|
||||
@@ -4190,8 +4190,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup existing set-product id only (no insert).
|
||||
* Insert happens only on pull-from-library / Excel import.
|
||||
* 仅查询已存在的套餐产品 ID(不插入)。
|
||||
* 仅在从库拉取/Excel 导入时才会插入。
|
||||
*/
|
||||
private Long lookupSetProductId(Long psvId, String productCode)
|
||||
{
|
||||
@@ -4204,8 +4204,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure package-side product master row exists for this version+code; return psp_id.
|
||||
* Uses request-scoped cache during bulk upsert to avoid N selects on same code.
|
||||
* 确保该版本+编码对应的套餐侧产品主数据行存在;返回 psp_id。
|
||||
* 批量 upsert 期间使用请求级缓存,避免同一编码多次查询。
|
||||
*/
|
||||
private Long ensureSetProduct(Long priceSetId, Long psvId, String productCode, String productNameHint)
|
||||
{
|
||||
@@ -4277,7 +4277,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return row.getPspId();
|
||||
}
|
||||
|
||||
/** Only rewrite SALES surcharge rels for rows that explicitly sent surcharges. */
|
||||
/** 仅为显式传了附加费的明细行重写 SALES 附加费关联。 */
|
||||
private void replaceSalesSurchargesIfNeeded(List<PrcPriceSetDetail> details)
|
||||
{
|
||||
if (details == null || details.isEmpty())
|
||||
@@ -4299,9 +4299,9 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace SALES prc_surcharge_rel rows for the given package details.
|
||||
* product_id = psp_id, detail_id = psd_id, price_type = SALES.
|
||||
* Empty surcharges list clears associations for that detail.
|
||||
* 为给定套餐明细替换 SALES 的 prc_surcharge_rel 行。
|
||||
* product_id = psp_id,detail_id = psd_id,price_type = SALES。
|
||||
* surcharges 为空列表表示清空该明细的关联。
|
||||
*/
|
||||
private void replaceSalesSurchargeRels(List<PrcPriceSetDetail> details)
|
||||
{
|
||||
@@ -4434,7 +4434,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return code.trim().toUpperCase();
|
||||
}
|
||||
|
||||
/** Delete all SALES surcharge rels under a package version (by detail + set-product ids). */
|
||||
/** 删除套餐版本下的所有 SALES 附加费关联(按明细 + 套餐产品 ID)。 */
|
||||
private void deleteSalesRelsByPsvId(Long psvId)
|
||||
{
|
||||
if (psvId == null)
|
||||
@@ -4480,8 +4480,8 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy SALES surcharge rels from source version details onto newly copied details
|
||||
* (match by product_code + country + zone + weight + product_country_id).
|
||||
* 将 SALES 附加费关联从源版本明细复制到新复制的明细上
|
||||
* (按 product_code + country + zone + weight + product_country_id 匹配)。
|
||||
*/
|
||||
private void copySalesSurchargeRelsFromVersion(Long srcPsvId, Long newPsvId, String createBy)
|
||||
{
|
||||
@@ -4581,7 +4581,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
+ "|" + (d.getDiscountValue() == null ? "-1" : d.getDiscountValue().toPlainString());
|
||||
}
|
||||
|
||||
/** True when package already has a true draft (status=0). Pending(3) does not block copy. */
|
||||
/** 套餐已存在真正的草稿(status=0)时返回 true。Pending(3) 不阻止复制。 */
|
||||
private boolean hasDraftVersion(Long priceSetId)
|
||||
{
|
||||
if (priceSetId == null)
|
||||
@@ -4620,7 +4620,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Timed versions first (by effective_time), drafts without time last (by seq)
|
||||
// 有生效时间的版本在前(按 effective_time 排序),无时间的草稿在后(按 seq 排序)
|
||||
versions.sort((a, b) -> {
|
||||
Date ta = a.getEffectiveTime();
|
||||
Date tb = b.getEffectiveTime();
|
||||
@@ -4644,7 +4644,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
return Integer.compare(nullToZero(a.getVersionSeq()), nullToZero(b.getVersionSeq()));
|
||||
});
|
||||
Date now = new Date();
|
||||
// First pass: chain expire_time = next sibling's effective_time; compute window status
|
||||
// 第一轮:串联 expire_time = 下一个版本的 effective_time;计算窗口状态
|
||||
Long effectPsvId = null;
|
||||
for (int i = 0; i < versions.size(); i++)
|
||||
{
|
||||
@@ -4665,23 +4665,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
v.setStatus(VER_DRAFT);
|
||||
continue;
|
||||
}
|
||||
// Not started yet -> pending
|
||||
// 尚未开始 -> 待生效
|
||||
if (v.getEffectiveTime().getTime() > now.getTime())
|
||||
{
|
||||
v.setStatus(VER_PENDING);
|
||||
continue;
|
||||
}
|
||||
// Window ended: expire_time <= now (same rule as bill: expire_time > asOf to stay active)
|
||||
// 窗口已结束:expire_time <= now(与账单规则一致:expire_time > asOf 才保持生效)
|
||||
if (nextStart != null && nextStart.getTime() <= now.getTime())
|
||||
{
|
||||
v.setStatus(VER_EXPIRED);
|
||||
continue;
|
||||
}
|
||||
// In window: candidate for the single EFFECT version (latest wins)
|
||||
// 处于窗口内:单个 EFFECT 版本的候选(较晚者胜出)
|
||||
v.setStatus(VER_EFFECT);
|
||||
effectPsvId = v.getPsvId();
|
||||
}
|
||||
// Second pass: only the latest in-window version stays EFFECT
|
||||
// 第二轮:仅窗口内最新的版本保持 EFFECT
|
||||
if (effectPsvId != null)
|
||||
{
|
||||
for (PrcPriceSetVersion v : versions)
|
||||
@@ -4708,7 +4708,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService
|
||||
versionMapper.updatePrcPriceSetVersion(v);
|
||||
}
|
||||
}
|
||||
// Keep package product/detail effect fields aligned with version
|
||||
// 保持套餐产品/明细的生效字段与版本一致
|
||||
if (priceSetId != null)
|
||||
{
|
||||
setProductMapper.syncEffectByPriceSetId(priceSetId);
|
||||
|
||||
+8
-8
@@ -403,7 +403,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService
|
||||
{
|
||||
return StringUtils.trim(name.substring(0, name.length() - SHEET_SUFFIX.length()));
|
||||
}
|
||||
// strip common suffixes: remote surcharge / surcharge / postcode
|
||||
// 去掉常见后缀:偏远附加费 / 附加费 / 邮编
|
||||
String[] suffixes = {
|
||||
"偏远附加费",
|
||||
"附加费",
|
||||
@@ -475,13 +475,13 @@ public class PrcZoneServiceImpl implements IPrcZoneService
|
||||
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<>();
|
||||
@@ -524,7 +524,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Japan style: province + postcode */
|
||||
/** 日本样式:州省 + 邮编 */
|
||||
private Map<String, List<String>> parseProvincePostcodes(Sheet sheet)
|
||||
{
|
||||
Map<String, List<String>> map = new LinkedHashMap<>();
|
||||
@@ -562,7 +562,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService
|
||||
{
|
||||
return map;
|
||||
}
|
||||
// zone = province, detail = postcode (swap vs AU)
|
||||
// 分区 = 州省,明细 = 邮编(与澳大利亚的对应关系相反)
|
||||
for (int r = headerRow + 1; r <= lastRow; r++)
|
||||
{
|
||||
Row row = sheet.getRow(r);
|
||||
@@ -581,7 +581,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService
|
||||
return map;
|
||||
}
|
||||
|
||||
/** France MR style: carrier/zone + coverage postcode */
|
||||
/** 法国 MR 样式:承运商/分区 + 覆盖邮编 */
|
||||
private Map<String, List<String>> parseCoveragePostcodes(Sheet sheet)
|
||||
{
|
||||
Map<String, List<String>> map = new LinkedHashMap<>();
|
||||
@@ -649,12 +649,12 @@ public class PrcZoneServiceImpl implements IPrcZoneService
|
||||
return map;
|
||||
}
|
||||
|
||||
/** UK surcharge sheet: multi blocks of region + postcode prefix */
|
||||
/** 英国附加费 Sheet:多块 region + 邮编前缀 */
|
||||
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++)
|
||||
{
|
||||
|
||||
+57
-57
@@ -170,7 +170,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
PrcQuote cached = quoteMapper.selectPrcQuoteByKey(quoteKey);
|
||||
if (cached != null)
|
||||
{
|
||||
// Legacy PRODUCT_DEFAULT (or no package stamp) must be recomputed after package-snapshot pricing.
|
||||
// 历史遗留的 PRODUCT_DEFAULT(或未打套餐标记)在套餐快照计价后必须重新计算。
|
||||
if (isStaleCachedQuote(cached))
|
||||
{
|
||||
quoteFeeMapper.deleteByQuoteId(cached.getQuoteId());
|
||||
@@ -210,8 +210,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached quotes from pre-package-snapshot logic (PRODUCT_DEFAULT / missing package id)
|
||||
* must not be reused after billing rules changed.
|
||||
* 套餐快照之前逻辑产生的缓存报价(PRODUCT_DEFAULT / 缺少套餐 ID)
|
||||
* 在计费规则变更后不得复用。
|
||||
*/
|
||||
private boolean isStaleCachedQuote(PrcQuote cached)
|
||||
{
|
||||
@@ -230,7 +230,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
public TmsPriceQuoteResult calculate(TmsPriceQuoteRequest request)
|
||||
{
|
||||
validateRequest(request);
|
||||
// Bill path: missing/disabled customer only marks status, pricing still continues.
|
||||
// 计费链路:客户缺失/停用仅标记状态,计价仍然继续。
|
||||
String customerActiveStatus = softResolveCustomer(request);
|
||||
Date bizDate = resolveBizDate(request.getBizDate());
|
||||
TmsPriceQuoteResult result = doQuote(request, bizDate);
|
||||
@@ -343,7 +343,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return new ArrayList<PrcPriceSetProduct>(byCode.values());
|
||||
}
|
||||
|
||||
/** Prefer version active at asOf; else latest version that has set-product rows. */
|
||||
/** 优先取 asOf 时点生效的版本;否则取包含套餐产品行的最新版本。 */
|
||||
private List<PrcPriceSetProduct> loadProductsForPackage(Long priceSetId, Date asOf)
|
||||
{
|
||||
PrcPriceSetVersion active = versionMapper.selectActiveByPriceSetAt(priceSetId, asOf);
|
||||
@@ -359,7 +359,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
// newest seq first
|
||||
// 版本序号从新到旧排序
|
||||
versions.sort((a, b) -> {
|
||||
int sa = a.getVersionSeq() == null ? 0 : a.getVersionSeq().intValue();
|
||||
int sb = b.getVersionSeq() == null ? 0 : b.getVersionSeq().intValue();
|
||||
@@ -411,7 +411,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
countryCode = countryCode.toUpperCase();
|
||||
}
|
||||
// Charge weight keeps 3 decimals; freight unit price keeps 2 (see buildFromRates).
|
||||
// 计费重量保留 3 位小数;运费单价保留 2 位小数(见 buildFromRates)。
|
||||
BigDecimal weight = request.getWeight().setScale(3, RoundingMode.HALF_UP);
|
||||
String destPostcode = emptyToNull(normalizePostcode(request.getDestPostcode()));
|
||||
if (destPostcode != null)
|
||||
@@ -441,7 +441,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
// Soft-fail: persist billing_fail_status=4 on the order bill.
|
||||
// 软失败:在订单账单上持久化 billing_fail_status=4。
|
||||
return buildBillingMissResult(request, productCode, productName, countryCode, countryName,
|
||||
weight, bizDate, BillBillingFailStatus.NO_CUSTOMER_PACKAGE.getCode());
|
||||
}
|
||||
@@ -453,15 +453,15 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
boolean productExcludedBySetProducts = false;
|
||||
boolean attemptedPriceMatch = false;
|
||||
boolean anyZoneRequired = false;
|
||||
// Sales miss: WEIGHT(3) > COUNTRY(2) > PRODUCT(1). Catalog miss: 25 > 24 > 23.
|
||||
// 销售未命中优先级:WEIGHT(3) > COUNTRY(2) > PRODUCT(1)。目录未命中:25 > 24 > 23。
|
||||
int salesMissCode = 0;
|
||||
int catalogMissCode = 0;
|
||||
// Match version by exact bill datetime (effective_time / expire_time), not end-of-day.
|
||||
// 按账单实际时间精确匹配版本(effective_time / expire_time),而不是按当天结束时间。
|
||||
Date asOf = bizDate != null ? bizDate : new Date();
|
||||
for (PrcCustomerPriceSet bind : binds)
|
||||
{
|
||||
PrcPriceSet set = priceSetMapper.selectPrcPriceSetById(bind.getPriceSetId());
|
||||
// Only enabled (E) sets participate; draft/stop are skipped and recorded as fail below.
|
||||
// 仅启用(E)状态的套餐参与;草稿/停用状态会跳过,并在后续记录为失败。
|
||||
if (set == null || !"E".equals(set.getStatus()))
|
||||
{
|
||||
continue;
|
||||
@@ -474,7 +474,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return doLabelQuote(request, bind, set, productCode, productName, countryCode, countryName,
|
||||
weight, bizDate);
|
||||
}
|
||||
// Temporal gate: prc_price_set_version.effective_time / expire_time (not bind pr_begindate)
|
||||
// 时间门槛:prc_price_set_version.effective_time / expire_time(不使用绑定表的 pr_begindate)
|
||||
PrcPriceSetVersion version = versionMapper.selectActiveByPriceSetAt(set.getPriceSetId(), asOf);
|
||||
if (version == null || version.getPsvId() == null)
|
||||
{
|
||||
@@ -485,7 +485,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
PrcPriceSetProduct setProduct = resolveSetProduct(version.getPsvId(), productCode);
|
||||
if (setProduct == null)
|
||||
{
|
||||
// Package has no this product: try product_code_mapping (upstream -> BMS).
|
||||
// 套餐中没有该产品:尝试通过 product_code_mapping 映射(上游 -> BMS)。
|
||||
String mappedCode = resolveMappedProductCode(productCode);
|
||||
if (mappedCode != null)
|
||||
{
|
||||
@@ -515,8 +515,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
}
|
||||
attemptedPriceMatch = true;
|
||||
// Prefer package product name only when it is a real name (not equal to product code).
|
||||
// Otherwise keep upstream LINE_NAME / catalog name (e.g. "标快鞋盒-包税(中性)").
|
||||
// 仅当套餐产品名是真实名称(不等于产品编码)时才优先使用。
|
||||
// 否则保留上游 LINE_NAME / 目录名称(例如 "标快鞋盒-包税(中性)")。
|
||||
if (setProduct != null && StringUtils.isNotEmpty(setProduct.getProductName()))
|
||||
{
|
||||
String spn = setProduct.getProductName().trim();
|
||||
@@ -546,7 +546,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
productCode, productName, cc, cn, zone.zoneCode, weight, bizDate);
|
||||
if (hit != null)
|
||||
{
|
||||
// Temporarily skip product-catalog (产品价) soft-fail; sales package hit is enough.
|
||||
// 暂时跳过产品目录(产品价)的软失败校验;销售套餐命中即可。
|
||||
// if (request instanceof TmsPriceBillRequest && !hasProductCatalogPrice(catalog))
|
||||
// {
|
||||
// catalogMissCode = Math.max(catalogMissCode,
|
||||
@@ -561,12 +561,12 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
else if (TYPE_DISCOUNT.equals(setType))
|
||||
{
|
||||
// DISCOUNT still needs catalog as base rate; do not soft-fail as 产品价未匹配 for now.
|
||||
// DISCOUNT 仍需要产品目录作为基础价;目前不按“产品价未匹配”做软失败处理。
|
||||
BmsProductCountry discCatalog = catalog;
|
||||
BigDecimal discWeight = weight;
|
||||
if (!hasProductCatalogPrice(discCatalog))
|
||||
{
|
||||
// Below min catalog band: snap charge weight to lowest band min.
|
||||
// 低于目录最低重量段:将计费重量向上取整到最低段的起始重量。
|
||||
BigDecimal snapped = snapWeightToCatalogMinBand(
|
||||
productCode, cc, cn, zone.zoneCode, weight, bizDate);
|
||||
if (snapped != null)
|
||||
@@ -586,7 +586,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
productCode, productName, discWeight, bizDate);
|
||||
if (hit != null)
|
||||
{
|
||||
// Calc-only snap: do not persist raised min-band weight as charge_weight.
|
||||
// 仅计算时的取整:不将抬升后的最低段重量持久化为 charge_weight。
|
||||
if (discWeight != null && weight != null && discWeight.compareTo(weight) != 0)
|
||||
{
|
||||
hit.setWeight(weight);
|
||||
@@ -595,7 +595,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
stampRequestMeta(request, hit);
|
||||
return hit;
|
||||
}
|
||||
// Discount detail missing for product (or invalid discount) after catalog hit.
|
||||
// 产品目录命中后,该产品的折扣明细缺失(或折扣无效)。
|
||||
salesMissCode = Math.max(salesMissCode, BillBillingFailStatus.PRODUCT.getCode());
|
||||
}
|
||||
}
|
||||
@@ -604,8 +604,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
if (request instanceof TmsPriceBillRequest)
|
||||
{
|
||||
// Soft-fail: package bound but not enabled, or enabled with no version at bizDate.
|
||||
// Persists billing_fail_status=5 so local rebill / MQ bill both record the miss.
|
||||
// 软失败:套餐已绑定但未启用,或已启用但在 bizDate 没有生效版本。
|
||||
// 持久化 billing_fail_status=5,使本地重算 / MQ 计费都能记录该未命中。
|
||||
TmsPriceQuoteResult miss = buildBillingMissResult(request, productCode, productName,
|
||||
countryCode, countryName, weight, bizDate,
|
||||
BillBillingFailStatus.NO_ACTIVE_PACKAGE_VERSION.getCode());
|
||||
@@ -624,7 +624,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
salesMissCode = Math.max(salesMissCode, BillBillingFailStatus.PRODUCT.getCode());
|
||||
}
|
||||
// Prefer product-catalog miss when present (sales may already have hit).
|
||||
// 若存在产品目录未命中则优先采用(销售价可能已命中)。
|
||||
int missCode = catalogMissCode > 0 ? catalogMissCode : salesMissCode;
|
||||
if (request instanceof TmsPriceBillRequest && BillBillingFailStatus.isFail(Integer.valueOf(missCode)))
|
||||
{
|
||||
@@ -820,7 +820,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnose FIXED miss: product rows absent -> 1; country/zone absent -> 2; else weight -> 3.
|
||||
* 诊断 FIXED 未命中:缺少产品行 -> 1;缺少国家/区域 -> 2;否则为重量 -> 3。
|
||||
*/
|
||||
private int diagnoseFixedMiss(Long psvId, String productCode, String countryCode,
|
||||
String countryName, String zoneCode)
|
||||
@@ -844,7 +844,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Diagnose product-catalog (产品价) miss: product absent -> 23; country/zone -> 24; else weight -> 25.
|
||||
* 诊断产品目录(产品价)未命中:缺少产品 -> 23;缺少国家/区域 -> 24;否则为重量 -> 25。
|
||||
*/
|
||||
private int diagnoseCatalogMiss(String productCode, String countryCode, String countryName,
|
||||
String zoneCode, Date bizDate)
|
||||
@@ -868,7 +868,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-fail bill result: keep order snapshot fields, all amounts zero, set billingFailStatus.
|
||||
* 软失败账单结果:保留订单快照字段,所有金额为 0,并设置 billingFailStatus。
|
||||
*/
|
||||
private TmsPriceQuoteResult buildBillingMissResult(TmsPriceQuoteRequest request, String productCode,
|
||||
String productName, String countryCode, String countryName, BigDecimal weight, Date bizDate,
|
||||
@@ -952,7 +952,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft lookup of package product row; null when version has no such product.
|
||||
* 软查询套餐产品行;版本中没有该产品时返回 null。
|
||||
*/
|
||||
private PrcPriceSetProduct resolveSetProduct(Long psvId, String productCode)
|
||||
{
|
||||
@@ -964,7 +964,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Return mapped BMS product code only when mapping exists and differs from upstream code.
|
||||
* 仅当映射存在且与上游编码不同时,返回映射后的 BMS 产品编码。
|
||||
*/
|
||||
private String resolveMappedProductCode(String upstreamCode)
|
||||
{
|
||||
@@ -1027,13 +1027,13 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return out;
|
||||
}
|
||||
|
||||
/** True when product catalog row has a usable freight price (产品价). */
|
||||
/** 当产品目录行存在可用的运费价格(产品价)时返回 true。 */
|
||||
private static boolean hasProductCatalogPrice(BmsProductCountry catalog)
|
||||
{
|
||||
return catalog != null && catalog.getFreightPrice() != null;
|
||||
}
|
||||
|
||||
/** Catalog match for product_amount / DISCOUNT base; quote FIXED may still hit without catalog. */
|
||||
/** 为 product_amount / DISCOUNT 基础价匹配产品目录;即使没有产品目录,FIXED 报价仍可能命中。 */
|
||||
private BmsProductCountry softMatchCatalog(String productCode, String countryCode, String countryName,
|
||||
String zoneCode, BigDecimal weight, Date bizDate, PrcZone resolvedZone)
|
||||
{
|
||||
@@ -1071,7 +1071,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
{
|
||||
result.setDestPostcode(emptyToNull(request.getDestPostcode()));
|
||||
}
|
||||
// Always prefer customer resolved from customerCode (ignore TMS/bind ids)
|
||||
// 始终优先使用根据 customerCode 解析出的客户(忽略 TMS/绑定 ID)
|
||||
if (request.getCustomerId() != null)
|
||||
{
|
||||
result.setCustomerId(request.getCustomerId());
|
||||
@@ -1083,7 +1083,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Require customerCode; resolve customer_id from ctr_customer.
|
||||
* 要求提供 customerCode;从 ctr_customer 解析 customer_id。
|
||||
*/
|
||||
private void resolveCustomer(TmsPriceQuoteRequest request)
|
||||
{
|
||||
@@ -1105,7 +1105,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerCodeNotFound"));
|
||||
}
|
||||
BmsCustomer hit = list.get(0);
|
||||
// is_enable: 0 normal, 1 disabled — stopped customers cannot quote/bill
|
||||
// is_enable:0 正常,1 停用 —— 已停用客户不能试算/计费
|
||||
if ("1".equals(hit.getIsEnable()))
|
||||
{
|
||||
throw new ServiceException(MessageUtils.message("price.quote.customerDisabled"));
|
||||
@@ -1114,8 +1114,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft resolve for billing: never throws on missing/disabled customer.
|
||||
* Returns ACTIVE / MISSING / DISABLED. Quote path still uses hard {@link #resolveCustomer}.
|
||||
* 计费场景的软解析:客户缺失/停用时不抛异常。
|
||||
* 返回 ACTIVE / MISSING / DISABLED。试算路径仍使用强校验的 {@link #resolveCustomer}。
|
||||
*/
|
||||
private String softResolveCustomer(TmsPriceQuoteRequest request)
|
||||
{
|
||||
@@ -1204,7 +1204,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
int detailCount = detailMapper.countByPsvId(psvId);
|
||||
if (detailCount == 0)
|
||||
{
|
||||
// FIXED sales price is version-detail only; no fallback to product_country.
|
||||
// FIXED 销售价仅来自版本明细;不回退到 product_country。
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1214,7 +1214,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
BigDecimal billWeight = weight;
|
||||
boolean snappedToMinBand = false;
|
||||
boolean snappedUpToNextBand = false;
|
||||
// Miss: below lowest band -> bill at that band's weight_min.
|
||||
// 未命中:低于最低重量段 -> 按该段的 weight_min 计费。
|
||||
if (detail == null)
|
||||
{
|
||||
PrcPriceSetDetail minBand = detailMapper.selectFixedMinBand(
|
||||
@@ -1227,7 +1227,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
snappedToMinBand = true;
|
||||
}
|
||||
}
|
||||
// Miss in a gap (e.g. 1.001 between 0-1 and 1.01-2): ceil to next band's weight_min.
|
||||
// 落在重量段空隙中(例如 1.001 位于 0-1 与 1.01-2 之间):向上取整到下一段的 weight_min。
|
||||
if (detail == null)
|
||||
{
|
||||
PrcPriceSetDetail nextBand = detailMapper.selectFixedNextBandUp(
|
||||
@@ -1239,7 +1239,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
snappedUpToNextBand = true;
|
||||
}
|
||||
}
|
||||
// Still no band (above last max, or no details): caller records WEIGHT miss.
|
||||
// 仍无匹配重量段(超过最后一段上限,或没有明细):由调用方记录 WEIGHT 未命中。
|
||||
if (detail == null)
|
||||
{
|
||||
return null;
|
||||
@@ -1266,15 +1266,15 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
String msg = MessageUtils.message("price.quote.hitCustomerFixedPackageDetail") + " [" + verLabel + "]";
|
||||
if (snappedToMinBand)
|
||||
{
|
||||
// Calc-only snap: keep original weight/chargeWeight for persist so upstream re-sync
|
||||
// can replace them when the new weight is already in-band.
|
||||
// 仅计算时的取整:持久化时保留原始 weight/chargeWeight,以便上游重新同步时
|
||||
// 在新重量已落在段内的情况下替换它们。
|
||||
r.setWeight(weight);
|
||||
r.setChargeWeight(weight);
|
||||
msg = msg + "; billed at min band " + billWeight.toPlainString() + " (persist original weight)";
|
||||
}
|
||||
else if (snappedUpToNextBand)
|
||||
{
|
||||
// Gap ceil: bill at next band min (1.001 -> 1.01); persist snapped charge_weight.
|
||||
// 空隙向上取整:按下一段起始重量计费(1.001 -> 1.01);持久化取整后的 charge_weight。
|
||||
r.setWeight(weight);
|
||||
r.setChargeWeight(billWeight);
|
||||
msg = msg + "; snapped up " + weight.toPlainString() + " -> " + billWeight.toPlainString();
|
||||
@@ -1285,8 +1285,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* When weight misses catalog bands but is below the lowest band, return that band's weight_min.
|
||||
* Otherwise null (caller keeps original miss).
|
||||
* 当重量未命中目录中的重量段但低于最低段时,返回该段的 weight_min。
|
||||
* 否则返回 null(由调用方保留原始未命中结果)。
|
||||
*/
|
||||
private BigDecimal snapWeightToCatalogMinBand(String productCode, String countryCode, String countryName,
|
||||
String zoneCode, BigDecimal weight, Date bizDate)
|
||||
@@ -1368,7 +1368,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXED sales from package detail snapshot; catalog only fills product_amount / cost.
|
||||
* 基于套餐明细快照生成 FIXED 销售价;产品目录仅用于填充 product_amount / cost。
|
||||
*/
|
||||
private TmsPriceQuoteResult buildFromPackageDetail(String source, PrcPriceSetDetail detail,
|
||||
BmsProductCountry catalog, PrcZone resolvedZone, String productCode, String productName,
|
||||
@@ -1399,7 +1399,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
TmsPriceQuoteResult r = new TmsPriceQuoteResult();
|
||||
r.setMatchSource(source);
|
||||
r.setProductCode(productCode);
|
||||
// Prefer detail name only when it is not a code placeholder; keep upstream/catalog otherwise.
|
||||
// 仅当明细名称不是编码占位符时才优先使用;否则保留上游/产品目录名称。
|
||||
if (detail != null && StringUtils.isNotEmpty(detail.getProductName()))
|
||||
{
|
||||
String dpn = detail.getProductName().trim();
|
||||
@@ -1538,8 +1538,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Product catalog amount (product_amount) and its fee lines.
|
||||
* Formula: product_country.freight_price * weight + registration_fee.
|
||||
* 产品目录金额(product_amount)及其费用明细行。
|
||||
* 公式:product_country.freight_price * weight + registration_fee。
|
||||
* 应付(cost_amount / pay_total)与产品目录价解耦,此处不再赋值,留空待接入真实成本数据源。
|
||||
*/
|
||||
private void fillProductAmountAndPay(TmsPriceQuoteResult r, BmsProductCountry base,
|
||||
@@ -1568,8 +1568,8 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Redline catalog amount from redline_product_country
|
||||
* (same match keys as product catalog, including bill datetime).
|
||||
* 从 redline_product_country 获取红线目录金额
|
||||
* (与产品目录相同的匹配键,包括账单时间)。
|
||||
*/
|
||||
private void fillRedlineAmount(TmsPriceQuoteResult r, BigDecimal weight, RoundingMode rm, Date bizDate)
|
||||
{
|
||||
@@ -1591,7 +1591,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
BigDecimal freight = base.getFreightPrice().multiply(weight).setScale(2, rm);
|
||||
BigDecimal regScaled = baseReg.setScale(2, rm);
|
||||
r.setRedlineAmount(freight.add(regScaled).setScale(2, rm));
|
||||
// Stash freight/reg on existing payFeeDetails for income fee-line apportionment
|
||||
// 将运费/挂号费暂存到已有的 payFeeDetails 上,用于收入费用行分摊
|
||||
if (r.getPayFeeDetails() != null)
|
||||
{
|
||||
for (TmsPriceQuoteFeeLine line : r.getPayFeeDetails())
|
||||
@@ -1708,9 +1708,9 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* Load matched surcharge rels by price_type.
|
||||
* SALES: package detail_id = psd_id.
|
||||
* PRODUCT / REDLINE: product catalog associations by product_code + country/detail.
|
||||
* 按 price_type 加载匹配的附加费关联记录。
|
||||
* SALES:套餐 detail_id = psd_id。
|
||||
* PRODUCT / REDLINE:按 product_code + 国家/明细匹配产品目录关联。
|
||||
*/
|
||||
private List<PrcSurchargeRel> loadMatchedSurcharges(TmsPriceQuoteResult result, String priceType)
|
||||
{
|
||||
@@ -1763,7 +1763,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param salesRel true = SALES (detail_id already = psd_id); false = PRODUCT/REDLINE detail match
|
||||
* @param salesRel true = SALES(detail_id 已是 psd_id);false = PRODUCT/REDLINE 明细匹配
|
||||
*/
|
||||
private boolean surchargeApplies(PrcSurchargeRel rel, TmsPriceQuoteResult result, boolean salesRel)
|
||||
{
|
||||
@@ -2082,7 +2082,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return Md5Utils.hash(raw);
|
||||
}
|
||||
|
||||
/** Read catalog product amount for a fee type from payFeeDetails. */
|
||||
/** 从 payFeeDetails 中读取指定费用类型的目录产品金额。 */
|
||||
private static BigDecimal productAmountOf(List<TmsPriceQuoteFeeLine> payLines, String feeType)
|
||||
{
|
||||
if (payLines == null || feeType == null)
|
||||
@@ -2099,7 +2099,7 @@ public class TmsPriceQuoteServiceImpl implements ITmsPriceQuoteService
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Read redline catalog amount for a fee type from payFeeDetails. */
|
||||
/** 从 payFeeDetails 中读取指定费用类型的红线目录金额。 */
|
||||
private static BigDecimal redlineAmountOf(List<TmsPriceQuoteFeeLine> payLines, String feeType)
|
||||
{
|
||||
if (payLines == null || feeType == null)
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import com.bms.common.utils.StringUtils;
|
||||
import com.bms.price.domain.vo.OrderBillFailReasonVo;
|
||||
|
||||
/**
|
||||
* Build templated billing-fail reason text for order-list popup.
|
||||
* 为订单列表弹窗构建模板化的计费失败原因文案。
|
||||
*/
|
||||
public final class BillingFailReasonSupport
|
||||
{
|
||||
|
||||
+28
-28
@@ -48,7 +48,7 @@ public class CatalogVersionSupport
|
||||
}
|
||||
}
|
||||
|
||||
/** Latest version for list: prefer EFFECT, else max version_seq. */
|
||||
/** 列表使用的最新版本:优先 EFFECT,否则取最大 version_seq。 */
|
||||
public PrcPriceSetVersion resolveLatestForList(String priceType)
|
||||
{
|
||||
assertCatalogType(priceType);
|
||||
@@ -72,8 +72,8 @@ public class CatalogVersionSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure a draft version for import/manual edit at effectiveTime.
|
||||
* If an open draft exists, reuse it; else create next seq and optionally copy from latest.
|
||||
* 确保在 effectiveTime 存在一个可导入/手工编辑的草稿版本。
|
||||
* 若已有未关闭的草稿则复用;否则创建下一个序号,并可选地从最新版本复制。
|
||||
*/
|
||||
public PrcPriceSetVersion ensureDraftVersion(String priceType, Date effectiveTime, boolean copyFromLatest)
|
||||
{
|
||||
@@ -102,7 +102,7 @@ public class CatalogVersionSupport
|
||||
nv.setVersionNo("V" + next);
|
||||
nv.setEffectiveTime(effectiveTime);
|
||||
nv.setStatus(STATUS_DRAFT);
|
||||
// IMPORT = inherit effect version; IMPORT_FRESH = empty rebuild (Excel)
|
||||
// IMPORT = 沿用已生效版本;IMPORT_FRESH = 空库重建(Excel)
|
||||
nv.setPriceBuildMode(copyFromLatest ? "IMPORT" : "IMPORT_FRESH");
|
||||
nv.setCreateBy("system");
|
||||
versionMapper.insertPrcPriceSetVersion(nv);
|
||||
@@ -115,7 +115,7 @@ public class CatalogVersionSupport
|
||||
productMapper.copyFromVersion(src.getPsvId(), nv.getPsvId());
|
||||
detailMapper.copyDetailsFromVersion(src.getPsvId(), nv.getPsvId());
|
||||
productMapper.bindDetailPspByPsvId(nv.getPsvId());
|
||||
// stamp price_type on copied rows
|
||||
// 给复制出的行打上 price_type
|
||||
productMapper.updatePriceTypeByPsvId(nv.getPsvId(), priceType);
|
||||
detailMapper.updatePriceTypeByPsvId(nv.getPsvId(), priceType);
|
||||
}
|
||||
@@ -124,9 +124,9 @@ public class CatalogVersionSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate draft/pending catalog version.
|
||||
* IMPORT_FRESH (align sales): may start inside a prior window; forbid equal to any other
|
||||
* version's effective/expire of the same price_type; then chain expire times and sync children.
|
||||
* 启动草稿/待生效的目录版本。
|
||||
* IMPORT_FRESH(对齐销售价):可以落在上一版本的时间窗内;但禁止与同一 price_type 下
|
||||
* 其他版本的生效/失效时间相同;随后串联失效时间并同步子表。
|
||||
*/
|
||||
public void activateVersion(Long psvId, Date effectiveTime)
|
||||
{
|
||||
@@ -162,13 +162,13 @@ public class CatalogVersionSupport
|
||||
ver.setEffectiveTime(eff);
|
||||
ver.setExpireTime(null);
|
||||
versionMapper.updatePrcPriceSetVersion(ver);
|
||||
// Chain B.expire=A.eff, A.expire=next/B.oldExpire; refresh status + product/detail effect
|
||||
// 串联失效时间:B.expire=A.eff,A.expire=next/B.oldExpire;刷新状态与产品/明细的生效标记
|
||||
chainAndRefreshCatalogStatuses(ver.getPriceType());
|
||||
}
|
||||
|
||||
/**
|
||||
* IMPORT_FRESH: may be earlier than prev-by-seq; must not equal any other version's eff/exp
|
||||
* within the same PRODUCT/REDLINE type.
|
||||
* IMPORT_FRESH:生效时间可以早于按序号的前一版本;但不得与同一 PRODUCT/REDLINE 类型下
|
||||
* 其他版本的生效/失效时间相同。
|
||||
*/
|
||||
private void validateImportFreshActivateTime(PrcPriceSetVersion version, Date effectiveTime)
|
||||
{
|
||||
@@ -201,8 +201,8 @@ public class CatalogVersionSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort timed versions by effective_time, chain expire_time = next start, then set status
|
||||
* and sync product/detail effect fields (same idea as sales refreshVersionStatus).
|
||||
* 按 effective_time 排序有时间的版本,串联 expire_time = 下一版本的开始时间,再设置状态
|
||||
* 并同步产品/明细的生效字段(与销售价 refreshVersionStatus 思路一致)。
|
||||
*/
|
||||
private void chainAndRefreshCatalogStatuses(String priceType)
|
||||
{
|
||||
@@ -324,9 +324,9 @@ public class CatalogVersionSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete catalog version:
|
||||
* - draft / pending: hard-delete version + children + excel
|
||||
* - effect: soft-delete version + products + details (+ surcharge rel)
|
||||
* 删除目录版本:
|
||||
* - 草稿 / 待生效:物理删除版本 + 子表 + Excel
|
||||
* - 已生效:逻辑删除版本 + 产品 + 明细(+ 附加费关联)
|
||||
*/
|
||||
public int deleteDraftVersion(Long psvId)
|
||||
{
|
||||
@@ -356,9 +356,9 @@ public class CatalogVersionSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Soft-delete EFFECT version with products and details,
|
||||
* then reopen the previous version as EFFECT (clear expire_time;
|
||||
* product/detail expire cleared via syncEffect).
|
||||
* 逻辑删除已生效版本及其产品和明细,
|
||||
* 再把上一版本重新置为 EFFECT(清空 expire_time;
|
||||
* 产品/明细的失效时间通过 syncEffect 清除)。
|
||||
*/
|
||||
private int softDeleteEffectVersion(PrcPriceSetVersion ver)
|
||||
{
|
||||
@@ -381,8 +381,8 @@ public class CatalogVersionSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Previous catalog version (highest version_seq below deleted) that can become EFFECT again.
|
||||
* Prefers expired/effect rows that already had an effective_time.
|
||||
* 可以重新置为 EFFECT 的上一目录版本(被删版本之下 version_seq 最大者)。
|
||||
* 优先选择已有 effective_time 的已失效/已生效行。
|
||||
*/
|
||||
private PrcPriceSetVersion findPreviousVersionToRestore(PrcPriceSetVersion deleted)
|
||||
{
|
||||
@@ -415,7 +415,7 @@ public class CatalogVersionSupport
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Skip future pending drafts: they are not "previous" effective windows
|
||||
// 跳过未来待生效的草稿:它们不属于「上一段」生效窗口
|
||||
if (STATUS_PENDING.equals(v.getStatus()) && v.getEffectiveTime().after(now))
|
||||
{
|
||||
continue;
|
||||
@@ -438,7 +438,7 @@ public class CatalogVersionSupport
|
||||
return best;
|
||||
}
|
||||
|
||||
/** Current draft version (status=0), or null. */
|
||||
/** 当前草稿版本(status=0),没有则为 null。 */
|
||||
public PrcPriceSetVersion currentDraft(String priceType)
|
||||
{
|
||||
List<PrcPriceSetVersion> drafts = listVersions(priceType, STATUS_DRAFT);
|
||||
@@ -446,7 +446,7 @@ public class CatalogVersionSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog force-edit: draft, pending, effect, and expired are writable.
|
||||
* 目录强制编辑:草稿、待生效、已生效和已失效版本都可写。
|
||||
*/
|
||||
public PrcPriceSetVersion requireEditableCatalogVersion(Long psvId, String priceType)
|
||||
{
|
||||
@@ -475,10 +475,10 @@ public class CatalogVersionSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare draft for catalog price create page.
|
||||
* copyFromLatest=true → priceBuildMode=IMPORT: copy from current EFFECT version when draft is empty.
|
||||
* copyFromLatest=false → priceBuildMode=IMPORT_FRESH: empty draft (Excel import / rebuild).
|
||||
* Legacy values COPY/IMPORT are migrated by SQL to IMPORT/IMPORT_FRESH.
|
||||
* 为目录价新增页准备草稿。
|
||||
* copyFromLatest=true → priceBuildMode=IMPORT:草稿为空时从当前 EFFECT 版本复制。
|
||||
* copyFromLatest=false → priceBuildMode=IMPORT_FRESH:空草稿(Excel 导入 / 重建)。
|
||||
* 旧值 COPY/IMPORT 由 SQL 迁移为 IMPORT/IMPORT_FRESH。
|
||||
*/
|
||||
public PrcPriceSetVersion prepareDraftVersion(String priceType, boolean copyFromLatest)
|
||||
{
|
||||
|
||||
+54
-54
@@ -35,25 +35,25 @@ import com.bms.system.domain.BmsRegion;
|
||||
import com.bms.system.mapper.BmsRegionMapper;
|
||||
|
||||
/**
|
||||
* Exclusive / large-customer (HALARA) quote Excel parser.
|
||||
* Completely separate from ordinary package / product quote Excel parsing.
|
||||
* 专属 / 大客户(HALARA)报价 Excel 解析器。
|
||||
* 与普通套餐 / 产品报价 Excel 解析完全独立。
|
||||
*
|
||||
* Catalog columns: category / country / level / product name / price sheet / product code / feature / lead time.
|
||||
* Price sheets: destination / origin / weight / freight(CNY/KG) / handling(CNY/PCS).
|
||||
* 目录列:分类 / 国家 / 等级 / 产品名称 / 价格表 / 产品代码 / 产品特性 / 时效。
|
||||
* 价格表:目的地 / 起运地 / 重量 / 运费(CNY/KG)/ 操作费(CNY/PCS)。
|
||||
*/
|
||||
@Component
|
||||
public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
private static final String CATALOG_SHEET = "\u4ea7\u54c1\u76ee\u5f55";
|
||||
private static final String CATALOG_SHEET = "产品目录";
|
||||
|
||||
/** Parse 0.01<=W<=0.5 / 0.3<W<=1 */
|
||||
/** 解析 0.01<=W<=0.5 / 0.3<W<=1 这类写法 */
|
||||
private static final Pattern WEIGHT_W_PATTERN = Pattern.compile(
|
||||
"(\\d+(?:\\.\\d+)?)\\s*[<\u2264]=?\\s*W\\s*[<\u2264]=?\\s*(\\d+(?:\\.\\d+)?)",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/** Parse HALARA bands: 0-0.3kg / 0kg-1kg / 1.01kg-2kg */
|
||||
/** 解析 HALARA 重量段写法:0-0.3kg / 0kg-1kg / 1.01kg-2kg */
|
||||
private static final Pattern WEIGHT_DASH_PATTERN = Pattern.compile(
|
||||
"(?i)(\\d+(?:\\.\\d+)?)\\s*(?:kg)?\\s*[-~\uFF5E\u2014\u2013]\\s*(\\d+(?:\\.\\d+)?)\\s*(?:kg)?");
|
||||
"(?i)(\\d+(?:\\.\\d+)?)\\s*(?:kg)?\\s*[-~~\u2014\u2013]\\s*(\\d+(?:\\.\\d+)?)\\s*(?:kg)?");
|
||||
|
||||
@Autowired
|
||||
private BmsRegionMapper regionMapper;
|
||||
@@ -65,12 +65,12 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
if (bytes == null || bytes.length == 0)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u5bfc\u5165\u6587\u4ef6\u4e3a\u7a7a");
|
||||
throw new ServiceException("专属报价导入文件为空");
|
||||
}
|
||||
if (filename == null || !(filename.toLowerCase(Locale.ROOT).endsWith(".xlsx")
|
||||
|| filename.toLowerCase(Locale.ROOT).endsWith(".xls")))
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301 .xls / .xlsx");
|
||||
throw new ServiceException("专属报价仅支持 .xls / .xlsx");
|
||||
}
|
||||
try (InputStream is = new ByteArrayInputStream(bytes); Workbook workbook = WorkbookFactory.create(is))
|
||||
{
|
||||
@@ -78,12 +78,12 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
Sheet catalog = workbook.getSheet(CATALOG_SHEET);
|
||||
if (catalog == null)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u7f3a\u5c11\u300c\u4ea7\u54c1\u76ee\u5f55\u300d");
|
||||
throw new ServiceException("专属报价缺少「产品目录」");
|
||||
}
|
||||
List<BmsProduct> products = dedupeByCode(parseCatalog(catalog));
|
||||
if (products.isEmpty())
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ea7\u54c1\u76ee\u5f55\u65e0\u6709\u6548\u4ea7\u54c1");
|
||||
throw new ServiceException("专属报价产品目录无有效产品");
|
||||
}
|
||||
|
||||
Map<String, List<BmsProduct>> ownersBySheet = new LinkedHashMap<String, List<BmsProduct>>();
|
||||
@@ -121,7 +121,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
continue;
|
||||
}
|
||||
Sheet sheet = workbook.getSheetAt(i);
|
||||
if (sheet == null || findHeaderCell(sheet, "\u76ee\u7684\u5730") == null)
|
||||
if (sheet == null || findHeaderCell(sheet, "目的地") == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -157,11 +157,11 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
}
|
||||
if (!dupTips.isEmpty())
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u5b58\u5728\u91cd\u590d\u56fd\u5bb6+\u91cd\u91cf\u6bb5\uff1a<br/>" + String.join("<br/>", dupTips));
|
||||
throw new ServiceException("专属报价存在重复国家+重量段:<br/>" + String.join("<br/>", dupTips));
|
||||
}
|
||||
if (out.isEmpty())
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u672a\u89e3\u6790\u5230\u6709\u6548\u4ef7\u683c\u884c");
|
||||
throw new ServiceException("专属报价未解析到有效价格行");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -178,7 +178,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
continue;
|
||||
}
|
||||
Sheet sheet = workbook.getSheetAt(i);
|
||||
if (sheet != null && findHeaderCell(sheet, "\u76ee\u7684\u5730") != null)
|
||||
if (sheet != null && findHeaderCell(sheet, "目的地") != null)
|
||||
{
|
||||
hasDest = true;
|
||||
break;
|
||||
@@ -186,7 +186,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
}
|
||||
if (!hasDest)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868\uff08\u542b\u300c\u76ee\u7684\u5730\u300d\u300c\u8fd0\u8d39\u300d\u300c\u64cd\u4f5c\u8d39\u300d\u5217\uff09\uff0c\u8bf7\u52ff\u4f7f\u7528\u666e\u901a\u5957\u9910 Excel");
|
||||
throw new ServiceException("专属报价仅支持大客折扣报价表(含「目的地」「运费」「操作费」列),请勿使用普通套餐 Excel");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,49 +205,49 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (n.contains("\u90ae\u7f16") || n.contains("\u8d54\u507f") || n.contains("\u9000\u4ef6")
|
||||
|| n.contains("\u7533\u62a5"))
|
||||
if (n.contains("邮编") || n.contains("赔偿") || n.contains("退件")
|
||||
|| n.contains("申报"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return n.contains("\u7a0e\u7387") || n.toUpperCase(Locale.ROOT).contains("VAT");
|
||||
return n.contains("税率") || n.toUpperCase(Locale.ROOT).contains("VAT");
|
||||
}
|
||||
|
||||
private List<BmsProduct> parseCatalog(Sheet sheet)
|
||||
{
|
||||
List<BmsProduct> list = new ArrayList<BmsProduct>();
|
||||
int[] header = findHeaderCell(sheet, "\u4ea7\u54c1\u4ee3\u7801");
|
||||
int[] header = findHeaderCell(sheet, "产品代码");
|
||||
if (header == null)
|
||||
{
|
||||
header = findHeaderCell(sheet, "\u4ea7\u54c1\u540d\u79f0");
|
||||
header = findHeaderCell(sheet, "产品名称");
|
||||
}
|
||||
if (header == null)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u672a\u627e\u5230\u8868\u5934\u300c\u4ea7\u54c1\u4ee3\u7801\u300d/\u300c\u4ea7\u54c1\u540d\u79f0\u300d");
|
||||
throw new ServiceException("专属报价「产品目录」未找到表头「产品代码」/「产品名称」");
|
||||
}
|
||||
int headerRowIdx = header[0];
|
||||
Row head = sheet.getRow(headerRowIdx);
|
||||
int colCode = findColByHeader(head, "\u4ea7\u54c1\u4ee3\u7801", 0);
|
||||
int colName = findColByHeader(head, "\u4ea7\u54c1\u540d\u79f0", 0);
|
||||
int colLevel = findColByHeader(head, "\u7b49\u7ea7", 0);
|
||||
int colCode = findColByHeader(head, "产品代码", 0);
|
||||
int colName = findColByHeader(head, "产品名称", 0);
|
||||
int colLevel = findColByHeader(head, "等级", 0);
|
||||
if (colLevel < 0)
|
||||
{
|
||||
colLevel = findColByHeader(head, "\u65f6\u6548", 0);
|
||||
colLevel = findColByHeader(head, "时效", 0);
|
||||
}
|
||||
int colCargo = findColByHeader(head, "\u4ea7\u54c1\u7279\u6027", 0);
|
||||
int colCargo = findColByHeader(head, "产品特性", 0);
|
||||
if (colCargo < 0)
|
||||
{
|
||||
colCargo = findColByHeader(head, "\u8d70\u8d27\u5c5e\u6027", 0);
|
||||
colCargo = findColByHeader(head, "走货属性", 0);
|
||||
}
|
||||
int colDest = findColByHeader(head, "\u56fd\u5bb6", 0);
|
||||
int colDest = findColByHeader(head, "国家", 0);
|
||||
if (colDest < 0)
|
||||
{
|
||||
colDest = findColByHeader(head, "\u76ee\u7684", 0);
|
||||
colDest = findColByHeader(head, "目的", 0);
|
||||
}
|
||||
int colLead = findColByHeader(head, "\u53c2\u8003\u65f6\u6548", 0);
|
||||
int colLead = findColByHeader(head, "参考时效", 0);
|
||||
if (colCode < 0 && colName < 0)
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u7f3a\u5c11\u300c\u4ea7\u54c1\u4ee3\u7801\u300d/\u300c\u4ea7\u54c1\u540d\u79f0\u300d\u5217");
|
||||
throw new ServiceException("专属报价「产品目录」缺少「产品代码」/「产品名称」列");
|
||||
}
|
||||
int lastRow = sheet.getLastRowNum();
|
||||
for (int i = headerRowIdx + 1; i <= lastRow; i++)
|
||||
@@ -263,7 +263,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if ("\u4ea7\u54c1\u4ee3\u7801".equals(rawCode) || "\u4ea7\u54c1\u540d\u79f0".equals(name))
|
||||
if ("产品代码".equals(rawCode) || "产品名称".equals(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -296,14 +296,14 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
return list;
|
||||
}
|
||||
|
||||
/** Keep dots in codes such as GB.PEXP / DE.PEXP. */
|
||||
/** 保留 GB.PEXP / DE.PEXP 这类代码中的点号。 */
|
||||
private static String normalizeCode(String raw)
|
||||
{
|
||||
if (StringUtils.isEmpty(raw))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String s = raw.trim().replaceAll("[\\(\\uFF08][^\\)\\uFF09]*[\\)\\uFF09]", "").trim();
|
||||
String s = raw.trim().replaceAll("[\\((][^\\))]*[\\))]", "").trim();
|
||||
if (StringUtils.isEmpty(s))
|
||||
{
|
||||
return null;
|
||||
@@ -383,7 +383,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
else if (StringUtils.isNotEmpty(nameBase) && sheetNameBase(key).equals(nameBase)
|
||||
&& nameBase.length() >= 2)
|
||||
{
|
||||
// e.g. catalog "DE express-general" -> sheet "DE express"
|
||||
// 例如目录中的 "DE express-general" 对应工作表 "DE express"
|
||||
score = 70;
|
||||
}
|
||||
else if (StringUtils.isNotEmpty(codeKey) && key.contains(codeKey))
|
||||
@@ -406,7 +406,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
return "";
|
||||
}
|
||||
String out = s.trim().toLowerCase(Locale.ROOT);
|
||||
out = out.replace('\u2013', '-').replace('\u2014', '-').replace('\uFF0D', '-');
|
||||
out = out.replace('\u2013', '-').replace('\u2014', '-').replace('-', '-');
|
||||
out = out.replace(" ", "").replace("\u3000", "");
|
||||
return out;
|
||||
}
|
||||
@@ -434,7 +434,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
return list;
|
||||
}
|
||||
int[] destHeader = findHeaderCell(sheet, "\u76ee\u7684\u5730");
|
||||
int[] destHeader = findHeaderCell(sheet, "目的地");
|
||||
if (destHeader == null)
|
||||
{
|
||||
return list;
|
||||
@@ -442,16 +442,16 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
int headerRowIdx = destHeader[0];
|
||||
Row headerRow = sheet.getRow(headerRowIdx);
|
||||
int colDest = destHeader[1];
|
||||
int colWeight = findColByHeader(headerRow, "\u91cd\u91cf", colDest + 1);
|
||||
int colFreight = findColByHeader(headerRow, "\u8fd0\u8d39", colDest + 1);
|
||||
int colHandle = findColByHeader(headerRow, "\u64cd\u4f5c\u8d39", colDest + 1);
|
||||
int colWeight = findColByHeader(headerRow, "重量", colDest + 1);
|
||||
int colFreight = findColByHeader(headerRow, "运费", colDest + 1);
|
||||
int colHandle = findColByHeader(headerRow, "操作费", colDest + 1);
|
||||
if (colHandle < 0)
|
||||
{
|
||||
colHandle = findColByHeader(headerRow, "\u6302\u53f7", colDest + 1);
|
||||
colHandle = findColByHeader(headerRow, "挂号", colDest + 1);
|
||||
}
|
||||
int colLead = findColByHeader(headerRow, "\u65f6\u6548", colDest + 1);
|
||||
int colFeature = findColByHeader(headerRow, "\u4ea7\u54c1\u7279\u6027", colDest + 1);
|
||||
int colRemark = findColByHeader(headerRow, "\u5907\u6ce8", colDest + 1);
|
||||
int colLead = findColByHeader(headerRow, "时效", colDest + 1);
|
||||
int colFeature = findColByHeader(headerRow, "产品特性", colDest + 1);
|
||||
int colRemark = findColByHeader(headerRow, "备注", colDest + 1);
|
||||
if (colWeight < 0)
|
||||
{
|
||||
colWeight = colDest + 2;
|
||||
@@ -478,7 +478,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
}
|
||||
String destCell = cellString(row.getCell(colDest));
|
||||
String weightLabel = cellString(row.getCell(colWeight));
|
||||
if ("\u76ee\u7684\u5730".equals(destCell) || (weightLabel != null && weightLabel.contains("\u91cd\u91cf\u6bb5")))
|
||||
if ("目的地".equals(destCell) || (weightLabel != null && weightLabel.contains("重量段")))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -492,8 +492,8 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (currentDest.startsWith("\u4ea7\u54c1\u5b9a\u4e49") || currentDest.startsWith("\u4e00\u3001")
|
||||
|| currentDest.startsWith("\u4e8c\u3001") || currentDest.contains("\u670d\u52a1\u6761\u4ef6"))
|
||||
if (currentDest.startsWith("产品定义") || currentDest.startsWith("一、")
|
||||
|| currentDest.startsWith("二、") || currentDest.contains("服务条件"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -525,7 +525,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
if (euRegions == null || euRegions.isEmpty())
|
||||
{
|
||||
throw new ServiceException("\u4e13\u5c5e\u62a5\u4ef7\u89e3\u6790\u5230\u300c\u6b27\u76df\u300d\u4f46\u533a\u57df\u5e93\u65e0\u6b27\u76df\u56fd\u5bb6");
|
||||
throw new ServiceException("专属报价解析到「欧盟」但区域库无欧盟国家");
|
||||
}
|
||||
for (BmsRegion eu : euRegions)
|
||||
{
|
||||
@@ -651,7 +651,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
String name = product == null ? "" : product.getProductName();
|
||||
String code = product == null ? "" : product.getProductCode();
|
||||
return (StringUtils.isNotEmpty(name) ? name : code) + " \u91cd\u590d\u56fd\u5bb6+\u91cd\u91cf\u6bb5";
|
||||
return (StringUtils.isNotEmpty(name) ? name : code) + " 重复国家+重量段";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -777,8 +777,8 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
return false;
|
||||
}
|
||||
String n = name.trim();
|
||||
return "\u6b27\u76df".equals(n) || "EU".equalsIgnoreCase(n) || "\u6b27\u6d32\u8054\u76df".equals(n)
|
||||
|| "\u6b27\u76df\u56fd\u5bb6".equals(n) || "\u4e1c\u6b27".equals(n);
|
||||
return "欧盟".equals(n) || "EU".equalsIgnoreCase(n) || "欧洲联盟".equals(n)
|
||||
|| "欧盟国家".equals(n) || "东欧".equals(n);
|
||||
}
|
||||
|
||||
private static int[] findHeaderCell(Sheet sheet, String headerText)
|
||||
@@ -893,7 +893,7 @@ public class ExclusiveHalaraQuoteExcelParser
|
||||
{
|
||||
return null;
|
||||
}
|
||||
s = s.replace(",", "").replace("\uFFE5", "").replace("CNY", "").trim();
|
||||
s = s.replace(",", "").replace("¥", "").replace("CNY", "").trim();
|
||||
if (StringUtils.isEmpty(s))
|
||||
{
|
||||
return null;
|
||||
|
||||
+5
-5
@@ -18,8 +18,8 @@ public final class OrderBillExportSupport
|
||||
}
|
||||
|
||||
/**
|
||||
* True when query has at least one meaningful filter.
|
||||
* Default orderStatuses alone does not count.
|
||||
* 查询条件中至少有一个有效筛选时返回 true。
|
||||
* 仅默认的 orderStatuses 不算。
|
||||
*/
|
||||
public static boolean hasExportFilter(PrcOrderBill query)
|
||||
{
|
||||
@@ -65,7 +65,7 @@ public final class OrderBillExportSupport
|
||||
}
|
||||
|
||||
|
||||
/** Null/empty => export all columns; otherwise only named Java props. */
|
||||
/** 为 null/空 => 导出全部列;否则只导出指定的 Java 属性。 */
|
||||
public static String[] toExportFieldArray(List<String> exportFields)
|
||||
{
|
||||
if (exportFields == null || exportFields.isEmpty())
|
||||
@@ -110,13 +110,13 @@ public final class OrderBillExportSupport
|
||||
return v != null && StringUtils.isNotEmpty(String.valueOf(v).trim());
|
||||
}
|
||||
|
||||
/** Export order-list Excel by current query result. */
|
||||
/** 按当前查询结果导出订单列表 Excel。 */
|
||||
public static void exportOrderList(HttpServletResponse response, List<PrcOrderBill> list)
|
||||
{
|
||||
ExcelUtil.export(response, toOrderListRows(list), PrcOrderBillListExportRow.class, "订单列表");
|
||||
}
|
||||
|
||||
/** Export billing-list Excel by current query result. */
|
||||
/** 按当前查询结果导出计费列表 Excel。 */
|
||||
public static void exportBillingList(HttpServletResponse response, List<PrcOrderBill> list)
|
||||
{
|
||||
ExcelUtil.export(response, toBillingListRows(list), PrcBillingBillListExportRow.class, "计费列表");
|
||||
|
||||
+9
-9
@@ -40,7 +40,7 @@ public class BmsProductImportAsyncRunner
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
/** Background all-country (Weishi catalog) import. */
|
||||
/** 后台全国家(威狮目录)导入。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runImportAll(String taskId, byte[] bytes, String filename, boolean updateSupport, String operName,
|
||||
Date effectiveDate)
|
||||
@@ -60,7 +60,7 @@ public class BmsProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background single-country cost-sheet import. */
|
||||
/** 后台单国家成本表导入。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runImportCountry(String taskId, byte[] bytes, String filename, String countryCode, String operName,
|
||||
Date effectiveDate)
|
||||
@@ -80,7 +80,7 @@ public class BmsProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background multi-country parse-only preview. One task merges up to 5 workbooks. */
|
||||
/** 后台多国家仅解析预览。一个任务最多合并 5 个工作簿。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runPreviewAll(String taskId, List<byte[]> bytesList, List<String> names)
|
||||
{
|
||||
@@ -98,7 +98,7 @@ public class BmsProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background single-country parse-only preview. One task merges up to 5 workbooks. */
|
||||
/** 后台单国家仅解析预览。一个任务最多合并 5 个工作簿。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runPreviewCountry(String taskId, List<byte[]> bytesList, List<String> names, String countryCode)
|
||||
{
|
||||
@@ -117,7 +117,7 @@ public class BmsProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse workbooks together. Merge stays in upload order so a later file wins. */
|
||||
/** 一起解析多个工作簿。合并保持上传顺序,后上传的文件优先。 */
|
||||
private Map<String, Object> parseCatalogFiles(String taskId, List<byte[]> bytesList, List<String> names,
|
||||
boolean country, String countryCode) throws Exception
|
||||
{
|
||||
@@ -142,8 +142,8 @@ public class BmsProductImportAsyncRunner
|
||||
}
|
||||
|
||||
/**
|
||||
* Save preview products sequentially (same psv_id).
|
||||
* Parallel inserts into prc_price_set_detail deadlock on secondary indexes.
|
||||
* 顺序保存预览产品(使用同一个 psv_id)。
|
||||
* 并行插入 prc_price_set_detail 会因二级索引而死锁。
|
||||
*/
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runApplyPreview(String taskId, CatalogPreviewApplyRequest request)
|
||||
@@ -174,7 +174,7 @@ public class BmsProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
}
|
||||
// Stable order reduces lock-order inversion if another session writes the same version.
|
||||
// 稳定排序可减少另一会话写入同一版本时的锁顺序倒置。
|
||||
jobs.sort((a, b) -> String.valueOf(a.getKey()).compareToIgnoreCase(String.valueOf(b.getKey())));
|
||||
int n = jobs.size();
|
||||
if (n == 0)
|
||||
@@ -217,7 +217,7 @@ public class BmsProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** InnoDB rolls the product transaction back on deadlock; start a new one. */
|
||||
/** InnoDB 遇到死锁会回滚产品事务;需要重新开启一个。 */
|
||||
private void applyWithDeadlockRetry(Runnable action)
|
||||
{
|
||||
int max = 8;
|
||||
|
||||
+8
-8
@@ -40,7 +40,7 @@ public class BmsRedlineProductImportAsyncRunner
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
/** Background all-country (Weishi catalog) import. */
|
||||
/** 后台全国家(威狮目录)导入。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runImportAll(String taskId, byte[] bytes, String filename, boolean updateSupport, String operName,
|
||||
Date effectiveDate)
|
||||
@@ -60,7 +60,7 @@ public class BmsRedlineProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background single-country cost-sheet import. */
|
||||
/** 后台单国家成本表导入。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runImportCountry(String taskId, byte[] bytes, String filename, String countryCode, String operName,
|
||||
Date effectiveDate)
|
||||
@@ -80,7 +80,7 @@ public class BmsRedlineProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background multi-country parse-only preview. One task merges up to 5 workbooks. */
|
||||
/** 后台多国家仅解析预览。一个任务最多合并 5 个工作簿。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runPreviewAll(String taskId, List<byte[]> bytesList, List<String> names)
|
||||
{
|
||||
@@ -98,7 +98,7 @@ public class BmsRedlineProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background single-country parse-only preview. One task merges up to 5 workbooks. */
|
||||
/** 后台单国家仅解析预览。一个任务最多合并 5 个工作簿。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runPreviewCountry(String taskId, List<byte[]> bytesList, List<String> names, String countryCode)
|
||||
{
|
||||
@@ -117,7 +117,7 @@ public class BmsRedlineProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse workbooks together. Merge stays in upload order so a later file wins. */
|
||||
/** 一起解析多个工作簿。合并保持上传顺序,后上传的文件优先。 */
|
||||
private Map<String, Object> parseCatalogFiles(String taskId, List<byte[]> bytesList, List<String> names,
|
||||
boolean country, String countryCode) throws Exception
|
||||
{
|
||||
@@ -142,8 +142,8 @@ public class BmsRedlineProductImportAsyncRunner
|
||||
}
|
||||
|
||||
/**
|
||||
* Save preview products sequentially (same psv_id).
|
||||
* Parallel inserts into prc_price_set_detail deadlock on secondary indexes.
|
||||
* 顺序保存预览产品(使用同一个 psv_id)。
|
||||
* 并行插入 prc_price_set_detail 会因二级索引而死锁。
|
||||
*/
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runApplyPreview(String taskId, RedlineCatalogPreviewApplyRequest request)
|
||||
@@ -216,7 +216,7 @@ public class BmsRedlineProductImportAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** InnoDB rolls the product transaction back on deadlock; start a new one. */
|
||||
/** InnoDB 遇到死锁会回滚产品事务;需要重新开启一个。 */
|
||||
private void applyWithDeadlockRetry(Runnable action)
|
||||
{
|
||||
int max = 8;
|
||||
|
||||
+11
-11
@@ -16,27 +16,27 @@ import com.bms.price.mapper.PrcBillingTaskMapper;
|
||||
import com.bms.price.service.IPrcBillingTaskService;
|
||||
|
||||
/**
|
||||
* Auto retry prc_billing_task on a schedule.
|
||||
* 定时自动重试 prc_billing_task。
|
||||
*
|
||||
* <p>Within [startHour, endHour) (default all day 00:00-24:00): pull up to {@value #DEFAULT_BATCH}
|
||||
* task ids (any success_status) -> bill all -> pull next page.
|
||||
* Empty page skips billing and ends this tick (next cron handles new rows).</p>
|
||||
* <p>在 [startHour, endHour) 内(默认全天 00:00-24:00):拉取最多 {@value #DEFAULT_BATCH}
|
||||
* 个任务 id(不限 success_status)-> 全部计费 -> 再拉下一页。
|
||||
* 空页跳过计费并结束本次触发(新数据由下次 cron 处理)。</p>
|
||||
*
|
||||
* <p>Same path as monitor manual / filter retry (reset to WAITING then process).</p>
|
||||
* <p>与监控页手工 / 筛选重试走同一路径(先重置为 WAITING 再处理)。</p>
|
||||
*/
|
||||
@Component
|
||||
public class PrcBillingTaskNightRetryTask
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(PrcBillingTaskNightRetryTask.class);
|
||||
|
||||
/** Default page size: bill this many, then pull next page */
|
||||
/** 默认每页条数:计费这么多条后再拉下一页 */
|
||||
public static final int DEFAULT_BATCH = 500;
|
||||
public static final int MIN_BATCH = 1;
|
||||
public static final int MAX_BATCH = 1000;
|
||||
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
/** Keyset cursor within the current local-day window; reset when the window day changes. */
|
||||
/** 当前本地自然日窗口内的 keyset 游标;窗口日期变化时重置。 */
|
||||
private volatile long lastTaskId = 0L;
|
||||
private volatile int windowDayOfYear = -1;
|
||||
private volatile int windowYear = -1;
|
||||
@@ -52,8 +52,8 @@ public class PrcBillingTaskNightRetryTask
|
||||
private IPrcBillingTaskService billingTaskService;
|
||||
|
||||
/**
|
||||
* Fire every hour by default; if a previous run is still paging batches, skip.
|
||||
* Hour gate uses bms.billing.night-retry start/end.
|
||||
* 默认每小时触发;若上一次运行仍在分页处理则跳过。
|
||||
* 小时门槛使用 bms.billing.night-retry 的 start/end。
|
||||
*/
|
||||
@Scheduled(cron = "${bms.billing.night-retry.cron:0 0 * * * ?}")
|
||||
public void scheduledTick()
|
||||
@@ -87,7 +87,7 @@ public class PrcBillingTaskNightRetryTask
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull batch -> bill all -> pull next batch, until no data or outside window.
|
||||
* 拉一批 -> 全部计费 -> 再拉下一批,直到没有数据或超出时间窗。
|
||||
*/
|
||||
private void runUntilEmptyOrEnd(BillingProperties.NightRetry cfg)
|
||||
{
|
||||
@@ -158,7 +158,7 @@ public class PrcBillingTaskNightRetryTask
|
||||
totalFail += fail;
|
||||
log.info("billing night-retry page={} done fetched={} ok={} fail={} lastTaskId={}",
|
||||
page, ids.size(), ok, fail, lastTaskId);
|
||||
// This page billed; loop immediately pulls the next page.
|
||||
// 本页已计费;循环会立即拉取下一页。
|
||||
}
|
||||
log.info("billing night-retry window closed pages={} ok={} fail={}", page, totalOk, totalFail);
|
||||
}
|
||||
|
||||
+1
-1
@@ -152,7 +152,7 @@ public class PrcBillingTaskRetryAsyncRunner
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Build MyBatis query params map for begin/end time. */
|
||||
/** 构造按开始/结束时间查询的 MyBatis 参数 Map。 */
|
||||
public static Map<String, Object> timeParams(String beginTime, String endTime)
|
||||
{
|
||||
Map<String, Object> params = new HashMap<String, Object>();
|
||||
|
||||
+3
-3
@@ -29,9 +29,9 @@ public class PrcChargeWeightImportAsyncRunner
|
||||
|
||||
public static final String CACHE_KEY_PREFIX = "price:billing:chargeWeight:import:task:";
|
||||
public static final int TTL_MINUTES = 60;
|
||||
/** Parallel rebill workers per import task. */
|
||||
/** 每个导入任务的并行重算线程数。 */
|
||||
private static final int REBILL_CONCURRENCY = 8;
|
||||
/** Redis progress write every N finished rows. */
|
||||
/** 每完成 N 行写一次 Redis 进度。 */
|
||||
private static final int PROGRESS_EVERY = 10;
|
||||
|
||||
@Autowired
|
||||
@@ -196,7 +196,7 @@ public class PrcChargeWeightImportAsyncRunner
|
||||
return msg;
|
||||
}
|
||||
|
||||
/** Pre-validated work item for async import. */
|
||||
/** 异步导入的预校验工作项。 */
|
||||
public static class ChargeWeightImportWorkItem implements java.io.Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ public class PrcOrderBillExportAsyncRunner
|
||||
private RedisCache redisCache;
|
||||
|
||||
/**
|
||||
* @param authentication login from request thread; required because {@code selectBillList} uses {@code @DataScope}
|
||||
* @param authentication 来自请求线程的登录信息;因 {@code selectBillList} 使用 {@code @DataScope} 而必需
|
||||
*/
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void run(String taskId, String exportType, PrcOrderBill query, String fileName,
|
||||
@@ -57,7 +57,7 @@ public class PrcOrderBillExportAsyncRunner
|
||||
SecurityContext previous = SecurityContextHolder.getContext();
|
||||
try
|
||||
{
|
||||
// Async pool threads have no JWT filter; restore caller login for DataScope.
|
||||
// 异步线程池没有 JWT 过滤器;为 DataScope 恢复调用方登录信息。
|
||||
if (authentication != null)
|
||||
{
|
||||
SecurityContext context = SecurityContextHolder.createEmptyContext();
|
||||
|
||||
+14
-14
@@ -38,8 +38,8 @@ import com.bms.price.service.IPrcOrderBillService;
|
||||
import com.bms.price.service.support.OrderBillExportSupport;
|
||||
|
||||
/**
|
||||
* Async local rebill from prc_order_bill only (no Oracle / MQ).
|
||||
* Continuous worker pull (no page barrier); cancel skips remaining billIds.
|
||||
* 仅基于 prc_order_bill 的异步本地重计费(不走 Oracle / MQ)。
|
||||
* 连续由工作线程拉取(无分页屏障);取消时跳过剩余 billId。
|
||||
*/
|
||||
@Component
|
||||
public class PrcOrderBillLocalRebillAsyncRunner
|
||||
@@ -51,7 +51,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
public static final int MIN_THREADS = 2;
|
||||
public static final int MAX_THREADS = 24;
|
||||
private static final int FAIL_DETAIL_MAX = 200;
|
||||
/** Publish Redis progress at most this often while workers run. */
|
||||
/** 工作线程运行期间,Redis 进度最多按此频率发布。 */
|
||||
private static final int PROGRESS_EVERY_N = 50;
|
||||
private static final long PROGRESS_MIN_INTERVAL_MS = 500L;
|
||||
|
||||
@@ -109,7 +109,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails);
|
||||
return;
|
||||
}
|
||||
// Continuous pull: N workers grab next billId — no page barrier idle wait.
|
||||
// 连续拉取:N 个工作线程取下一个 billId —— 没有分页屏障式空等。
|
||||
final List<String> ids = billIds;
|
||||
List<Callable<Boolean>> workers = new ArrayList<Callable<Boolean>>(threads);
|
||||
for (int w = 0; w < threads; w++)
|
||||
@@ -126,7 +126,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
try
|
||||
{
|
||||
TmsPriceQuoteResult result = orderBillService.rebillLocalByBillId(one);
|
||||
// Soft-fail still persists the order bill; count as fail in progress.
|
||||
// 软失败仍会落库订单账单;进度中按失败计数。
|
||||
if (BillBillingFailStatus.isFail(result == null ? null : result.getBillingFailStatus()))
|
||||
{
|
||||
fail.incrementAndGet();
|
||||
@@ -202,7 +202,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
|
||||
/**
|
||||
* Request cooperative cancel. Returns current task snapshot (may still be RUNNING until page ends).
|
||||
* 请求协作式取消。返回当前任务快照(在处理结束前可能仍为 RUNNING)。
|
||||
*/
|
||||
public BatchAddIncomeBillFeeTask requestCancel(String taskId)
|
||||
{
|
||||
@@ -223,7 +223,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
return task;
|
||||
}
|
||||
cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false)).set(true);
|
||||
// Keep RUNNING so clients keep polling until finishCancelled writes CANCELLED.
|
||||
// 保持 RUNNING,让客户端继续轮询,直到 finishCancelled 写入 CANCELLED。
|
||||
put(taskId, BatchAddIncomeBillFeeTask.STATUS_RUNNING,
|
||||
Math.max(task.getPercent(), 1),
|
||||
"cancelling",
|
||||
@@ -304,7 +304,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
// detail enrichment is best-effort
|
||||
// 明细补充信息尽力而为
|
||||
}
|
||||
item.put("orderId", StringUtils.isEmpty(orderNo) ? billId : orderNo);
|
||||
item.put("transferOrderId", transferNo);
|
||||
@@ -322,7 +322,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
&& BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(existing.getStatus())
|
||||
&& !BatchAddIncomeBillFeeTask.STATUS_CANCELLED.equals(status))
|
||||
{
|
||||
// Keep cancelled: do not let a late RUNNING overwrite after user cancel.
|
||||
// 保持已取消状态:用户取消后,不允许迟到的 RUNNING 覆盖。
|
||||
BatchAddIncomeBillFeeTask keep = new BatchAddIncomeBillFeeTask();
|
||||
keep.setTaskId(taskId);
|
||||
keep.setStatus(BatchAddIncomeBillFeeTask.STATUS_CANCELLED);
|
||||
@@ -353,8 +353,8 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttled Redis progress so the UI moves during a large page (not only after 500).
|
||||
* Does not change worker throughput; only how often status is published.
|
||||
* 节流写入 Redis 进度,让大批量处理时 UI 也能推进(不只在 500 条之后)。
|
||||
* 不改变工作线程吞吐,只影响状态发布的频率。
|
||||
*/
|
||||
private void maybePublishProgress(String taskId, int total, int finished, int ok, int fail,
|
||||
AtomicLong lastProgressAt)
|
||||
@@ -400,7 +400,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
catch (ExecutionException e)
|
||||
{
|
||||
// counted in job body
|
||||
// 已在任务体中计数
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -474,7 +474,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Prefer transferNo in UI tips; fall back to orderNo, then billId. */
|
||||
/** UI 提示优先用 transferNo;没有则退回 orderNo,再退回 billId。 */
|
||||
private String failTransferLabel(String billId, String transferNoHint)
|
||||
{
|
||||
if (StringUtils.isNotEmpty(transferNoHint))
|
||||
@@ -502,7 +502,7 @@ public class PrcOrderBillLocalRebillAsyncRunner
|
||||
}
|
||||
catch (Exception ignore)
|
||||
{
|
||||
// tip label is best-effort
|
||||
// 提示标签尽力而为
|
||||
}
|
||||
return billId;
|
||||
}
|
||||
|
||||
+4
-4
@@ -53,7 +53,7 @@ public class PrcPriceSetSaveAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background write of detail surcharge snapshots for many rows. */
|
||||
/** 后台批量写入明细附加费快照。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runSurchargeBatch(String taskId, List<PrcPriceSetDetail> details)
|
||||
{
|
||||
@@ -77,7 +77,7 @@ public class PrcPriceSetSaveAsyncRunner
|
||||
}
|
||||
|
||||
|
||||
/** Background import of quote Excel into package detail prices. */
|
||||
/** 后台将报价 Excel 导入套餐明细价格。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runImportPrices(String taskId, Long priceSetId, Long psvId, byte[] bytes, String filename, String operName)
|
||||
{
|
||||
@@ -116,7 +116,7 @@ public class PrcPriceSetSaveAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Import up to 5 workbooks in one task. Each file commits before the next. */
|
||||
/** 单个任务最多导入 5 个工作簿,每个文件提交后再处理下一个。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runImportPricesFiles(String taskId, Long priceSetId, Long psvId, List<byte[]> bytesList, List<String> names,
|
||||
String operName, boolean updateOnly, boolean replaceByProductCountry, String countryCode)
|
||||
@@ -148,7 +148,7 @@ public class PrcPriceSetSaveAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Background pull of product library into package version details. */
|
||||
/** 后台将产品库拉取到套餐版本明细。 */
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void runPullLibrary(String taskId, Long priceSetId, Long psvId, boolean force, String operName)
|
||||
{
|
||||
|
||||
+5
-5
@@ -41,7 +41,7 @@ public class TabOrderCompareAsyncRunner
|
||||
/** 进度/抽屉中保留的缺失转单号上限 */
|
||||
public static final int MISSING_SAMPLE_LIMIT = 5000;
|
||||
|
||||
/** Fallback when search orderStatuses empty (same as order-source list default). */
|
||||
/** 搜索条件 orderStatuses 为空时的兜底值(与订单数源列表默认值一致)。 */
|
||||
public static final List<Integer> COMPARE_LOCAL_ORDER_STATUSES =
|
||||
Collections.unmodifiableList(Arrays.asList(
|
||||
Integer.valueOf(3), Integer.valueOf(4), Integer.valueOf(5), Integer.valueOf(6),
|
||||
@@ -71,7 +71,7 @@ public class TabOrderCompareAsyncRunner
|
||||
query.setOrgOrderId(null);
|
||||
query.setTransferOrderId(null);
|
||||
}
|
||||
// Use list filter orderStatuses for both upstream scan and local match (no hardcode).
|
||||
// 上游扫描与本地匹配都使用列表筛选的 orderStatuses(不硬编码)。
|
||||
List<Integer> compareStatuses = resolveCompareStatuses(query);
|
||||
if (query != null)
|
||||
{
|
||||
@@ -183,8 +183,8 @@ public class TabOrderCompareAsyncRunner
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefer search filter orderStatuses; if empty, same default as order-source list
|
||||
* (3/4/5/6/7/8/11) so SQL still has an IN list.
|
||||
* 优先使用搜索条件的 orderStatuses;为空时取与订单数源列表相同的默认值
|
||||
* (3/4/5/6/7/8/11),确保 SQL 仍有 IN 列表。
|
||||
*/
|
||||
private static List<Integer> resolveCompareStatuses(TabOrderMonitorQuery query)
|
||||
{
|
||||
@@ -202,7 +202,7 @@ public class TabOrderCompareAsyncRunner
|
||||
return cleaned.isEmpty() ? new ArrayList<Integer>(COMPARE_LOCAL_ORDER_STATUSES) : cleaned;
|
||||
}
|
||||
|
||||
/** transferNo(upper) present locally with the given order_status filter. */
|
||||
/** 按给定的 order_status 筛选,返回本地已存在的 transferNo(大写)。 */
|
||||
private Set<String> findLocalTransferNos(List<String> transferNos, List<String> customerCodes,
|
||||
List<Integer> orderStatuses)
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ public class TabOrderExportAsyncRunner
|
||||
public static final String CACHE_KEY_PREFIX = "monitor:orderSource:export:task:";
|
||||
public static final int TTL_MINUTES = 60;
|
||||
public static final String REL_DIR = "orderSourceExport";
|
||||
/** Must stay <= TabOrderMonitorService.normalizePage max (200). */
|
||||
/** 必须 <= TabOrderMonitorService.normalizePage 允许的最大值(200)。 */
|
||||
public static final int PAGE_SIZE = 200;
|
||||
/** 单次导出行数上限(防 OOM) */
|
||||
public static final int EXPORT_MAX = 20000;
|
||||
@@ -104,8 +104,8 @@ public class TabOrderExportAsyncRunner
|
||||
int pct = Math.min(70, 5 + (int) ((all.size() * 65L) / Math.max(total, 1L)));
|
||||
put(taskId, TabOrderExportTask.STATUS_RUNNING, pct, "querying " + all.size() + "/" + total,
|
||||
null, fileName, null, all.size());
|
||||
// Do not compare rows.size() to PAGE_SIZE: searchPage caps pageSize at 200.
|
||||
// Stop only when all matched rows are collected or hard limit hit.
|
||||
// 不要用 rows.size() 与 PAGE_SIZE 比较:searchPage 会把 pageSize 限制在 200。
|
||||
// 只有全部匹配行都已收集或触达硬上限时才停止。
|
||||
if (all.size() >= total || all.size() >= EXPORT_MAX)
|
||||
{
|
||||
break;
|
||||
|
||||
@@ -193,14 +193,14 @@ public class TabOrderSyncAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Sync selected rows with a short-lived worker pool (for HTTP syncSelected). */
|
||||
/** 用短生命周期的线程池同步选中的行(供 HTTP syncSelected 使用)。 */
|
||||
public Map<String, Object> syncRowsParallel(List<TabOrderMonitorVo> rows)
|
||||
{
|
||||
return syncRowsParallel(rows, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync selected rows (upstream field overwrite only; billType ignored).
|
||||
* 同步选中的行(仅覆盖上游字段;忽略 billType)。
|
||||
*/
|
||||
public Map<String, Object> syncRowsParallel(List<TabOrderMonitorVo> rows, String billType)
|
||||
{
|
||||
@@ -218,8 +218,8 @@ public class TabOrderSyncAsyncRunner
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync selected rows carrying the upstream Oracle full row when present (skip re-query).
|
||||
* Overwrites local bill fields only; does not bill / refund.
|
||||
* 同步选中的行,若带有上游 Oracle 整行数据则直接使用(跳过再次查询)。
|
||||
* 只覆盖本地账单字段;不执行计费 / 退费。
|
||||
*/
|
||||
public Map<String, Object> syncRowsParallelRows(List<com.bms.price.oracle.TabOrderRow> rows, String billType)
|
||||
{
|
||||
@@ -316,7 +316,7 @@ public class TabOrderSyncAsyncRunner
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve Oracle row then overwrite local bill + ext (no billing). */
|
||||
/** 解析出 Oracle 行后覆盖本地账单及其扩展信息(不计费)。 */
|
||||
private String overwriteOne(com.bms.price.oracle.TabOrderRow row)
|
||||
{
|
||||
Map<String, Object> raw = row == null ? null : row.getRaw();
|
||||
@@ -382,7 +382,7 @@ public class TabOrderSyncAsyncRunner
|
||||
}
|
||||
catch (ExecutionException e)
|
||||
{
|
||||
// counted in job body
|
||||
// 已在任务体中计数
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.bms.common.utils.StringUtils;
|
||||
*/
|
||||
public final class ZoneCodeAbbrevs
|
||||
{
|
||||
// fullwidth () and ASCII ()
|
||||
// 全角括号()与 ASCII 括号 ()
|
||||
private static final Pattern ZONE_SUFFIX = Pattern.compile(
|
||||
"^(.+?)[\\(\\uFF08]([^\\)\\uFF09]+)[\\)\\uFF09]\\s*$");
|
||||
|
||||
@@ -31,7 +31,7 @@ public final class ZoneCodeAbbrevs
|
||||
}
|
||||
|
||||
/**
|
||||
* Split country cell like Country(zoneLabel); no parenthesis -> zoneLabel null.
|
||||
* 拆分 Country(zoneLabel) 这种国家单元格;没有括号时 zoneLabel 为 null。
|
||||
*/
|
||||
public static CountryZoneParts splitCountryZone(String raw)
|
||||
{
|
||||
|
||||
+13
-13
@@ -25,7 +25,7 @@ import com.bms.system.service.IBmsProductCodeMappingService;
|
||||
public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
||||
{
|
||||
public static final String RULE_TYPE_THREE_SIDE = "THREE_SIDE";
|
||||
/** Charge weight = max(L*W*H/8000, weightSource). */
|
||||
/** 计费重量 = max(L*W*H/8000, weightSource)。 */
|
||||
public static final String RULE_TYPE_VOL_8000 = "VOL_8000";
|
||||
public static final String WEIGHT_SOURCE_RECEIVE = "RECEIVE_WEIGHT";
|
||||
public static final String WEIGHT_SOURCE_CHARGE = "CHARGE_WEIGHT";
|
||||
@@ -94,7 +94,7 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
||||
}
|
||||
normalizeHeader(row);
|
||||
validateHeader(row);
|
||||
// edit: single dimension row
|
||||
// 编辑:单维度行
|
||||
if (row.getProductCodes() != null && !row.getProductCodes().isEmpty())
|
||||
{
|
||||
row.setProductCode(normalizeCode(row.getProductCodes().get(0)));
|
||||
@@ -136,8 +136,8 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
||||
{
|
||||
return null;
|
||||
}
|
||||
// VOL_8000 is applied on local ingest / bill. THREE_SIDE stays config-only
|
||||
// (upstream already applies that product rule).
|
||||
// VOL_8000 在本地入库 / 计费时应用。THREE_SIDE 仅作为配置
|
||||
// (上游已应用该产品规则)。
|
||||
BigDecimal vol8000 = resolveVol8000(request, systemWeight);
|
||||
if (vol8000 != null)
|
||||
{
|
||||
@@ -147,8 +147,8 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
||||
}
|
||||
|
||||
/**
|
||||
* VOL_8000: chargeWeight = max(L*W*H/8000, receive_weight).
|
||||
* Compare dimensional weight (/8000) with 收货重.
|
||||
* VOL_8000:chargeWeight = max(L*W*H/8000, receive_weight)。
|
||||
* 将体积重(/8000)与收货重比较。
|
||||
*/
|
||||
private BigDecimal resolveVol8000(TmsPriceBillRequest request, BigDecimal systemWeight)
|
||||
{
|
||||
@@ -243,7 +243,7 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
||||
return one;
|
||||
}
|
||||
|
||||
/** empty list / blank => one null dimension (means all) */
|
||||
/** 空列表 / 空白 => 返回一个 null 维度(表示全部) */
|
||||
private static List<String> dimValues(List<String> list, String single)
|
||||
{
|
||||
Set<String> set = new LinkedHashSet<String>();
|
||||
@@ -332,8 +332,8 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer dim: rule may store customer_code or customer_code_dw (对外代码).
|
||||
* Request may carry either form from upstream / local bill.
|
||||
* 客户维度:规则可能存储 customer_code 或 customer_code_dw(对外代码)。
|
||||
* 请求可能携带来自上游 / 本地账单的任一形式。
|
||||
*/
|
||||
private boolean matchCustomerDim(String requestCode, String ruleCode)
|
||||
{
|
||||
@@ -422,7 +422,7 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
||||
{
|
||||
if (WEIGHT_SOURCE_RECEIVE.equalsIgnoreCase(weightSource))
|
||||
{
|
||||
// Prefer actual (实重), then receive weight.
|
||||
// 优先取实重,其次取收货重。
|
||||
BigDecimal rw = firstPositive(request.getActualWeight(), request.getReceiveWeight());
|
||||
return rw != null ? rw : systemWeight;
|
||||
}
|
||||
@@ -541,16 +541,16 @@ public class BmsBizRuleServiceImpl implements IBmsBizRuleService
|
||||
}
|
||||
if (StringUtils.isEmpty(row.getWeightSource()))
|
||||
{
|
||||
throw new ServiceException("\u53d6\u503c\u5b57\u6bb5\u4e0d\u80fd\u4e3a\u7a7a");
|
||||
throw new ServiceException("取值字段不能为空");
|
||||
}
|
||||
if (RULE_TYPE_THREE_SIDE.equalsIgnoreCase(row.getRuleType()) && row.getSideSumMax() == null)
|
||||
{
|
||||
throw new ServiceException("\u4e09\u8fb9\u89c4\u5219\u5fc5\u987b\u586b\u5199\u4e09\u8fb9\u548c\u4e0a\u9650");
|
||||
throw new ServiceException("三边规则必须填写三边和上限");
|
||||
}
|
||||
if (row.getWeightMin() != null && row.getWeightMax() != null
|
||||
&& row.getWeightMin().compareTo(row.getWeightMax()) > 0)
|
||||
{
|
||||
throw new ServiceException("\u91cd\u91cf\u6bb5\u6700\u5c0f\u503c\u4e0d\u80fd\u5927\u4e8e\u6700\u5927\u503c");
|
||||
throw new ServiceException("重量段最小值不能大于最大值");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -182,7 +182,7 @@ public class SysFileTemplateServiceImpl implements ISysFileTemplateService
|
||||
ossStorage.download(row.getFilePath(), response.getOutputStream());
|
||||
return;
|
||||
}
|
||||
// Legacy local path fallback
|
||||
// 兼容旧版本地路径
|
||||
if (!FileUtils.checkAllowDownload(row.getFilePath()))
|
||||
{
|
||||
throw new ServiceException("template path illegal");
|
||||
|
||||
Reference in New Issue
Block a user