From b7d0674fda1cfa31f849a80fd339729b77073347 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Wed, 11 Nov 2026 13:35:53 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E4=B8=BA=E7=AE=80=E4=BD=93=E4=B8=AD=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/impl/CustomerAccountApiImpl.java | 6 +- .../price/oracle/CustomerKeywordResolver.java | 2 +- .../price/oracle/OracleOrderBillMapper.java | 12 +- .../oracle/OracleUpstreamOrderService.java | 8 +- .../price/oracle/TabOrderMonitorService.java | 22 +- .../service/impl/BmsProductServiceImpl.java | 182 ++++++++-------- .../impl/BmsRedlineProductServiceImpl.java | 124 +++++------ .../impl/PrcBillingTaskServiceImpl.java | 34 +-- .../service/impl/PrcOrderBillServiceImpl.java | 172 +++++++-------- .../impl/PrcPriceExcelFileServiceImpl.java | 4 +- .../service/impl/PrcPriceSetServiceImpl.java | 206 +++++++++--------- .../service/impl/PrcZoneServiceImpl.java | 16 +- .../impl/TmsPriceQuoteServiceImpl.java | 114 +++++----- .../support/BillingFailReasonSupport.java | 2 +- .../support/CatalogVersionSupport.java | 56 ++--- .../ExclusiveHalaraQuoteExcelParser.java | 108 ++++----- .../support/OrderBillExportSupport.java | 10 +- .../task/BmsProductImportAsyncRunner.java | 18 +- .../BmsRedlineProductImportAsyncRunner.java | 16 +- .../task/PrcBillingTaskNightRetryTask.java | 22 +- .../task/PrcBillingTaskRetryAsyncRunner.java | 2 +- .../PrcChargeWeightImportAsyncRunner.java | 6 +- .../task/PrcOrderBillExportAsyncRunner.java | 4 +- .../PrcOrderBillLocalRebillAsyncRunner.java | 28 +-- .../task/PrcPriceSetSaveAsyncRunner.java | 8 +- .../task/TabOrderCompareAsyncRunner.java | 10 +- .../price/task/TabOrderExportAsyncRunner.java | 6 +- .../price/task/TabOrderSyncAsyncRunner.java | 12 +- .../com/bms/price/util/ZoneCodeAbbrevs.java | 4 +- .../service/impl/BmsBizRuleServiceImpl.java | 26 +-- .../impl/SysFileTemplateServiceImpl.java | 2 +- bms-front/src/utils/excelExport.js | 8 +- .../components/CatalogVersionExcelDrawer.vue | 20 +- bms-front/src/views/price/billing/bill.vue | 24 +- .../set/components/create/StepDetails.vue | 6 +- .../detail/SetVersionDetailDrawer.vue | 4 +- 36 files changed, 652 insertions(+), 652 deletions(-) diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customermoney/api/impl/CustomerAccountApiImpl.java b/bms-back/bms-system/src/main/java/com/bms/customer/customermoney/api/impl/CustomerAccountApiImpl.java index 0fd436b4..c4796ef4 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customermoney/api/impl/CustomerAccountApiImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customermoney/api/impl/CustomerAccountApiImpl.java @@ -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); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/oracle/CustomerKeywordResolver.java b/bms-back/bms-system/src/main/java/com/bms/price/oracle/CustomerKeywordResolver.java index 243f7a3b..ecf0b757 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/oracle/CustomerKeywordResolver.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/oracle/CustomerKeywordResolver.java @@ -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)) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderBillMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderBillMapper.java index dd93978b..3a857bbd 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderBillMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleOrderBillMapper.java @@ -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 row, String... keys) { if (row == null || keys == null) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleUpstreamOrderService.java b/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleUpstreamOrderService.java index 9fae6ad4..6ad89be6 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleUpstreamOrderService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/oracle/OracleUpstreamOrderService.java @@ -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 byTransfer = query("TRANSFER_ORDER_ID", @@ -65,7 +65,7 @@ public class OracleUpstreamOrderService } } - // 2) Fallback: order no + // 2) 回退:按订单号 Map 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)); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/oracle/TabOrderMonitorService.java b/bms-back/bms-system/src/main/java/com/bms/price/oracle/TabOrderMonitorService.java index 41556ed9..2e03cd7c 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/oracle/TabOrderMonitorService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/oracle/TabOrderMonitorService.java @@ -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 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 codes = query.getCustomerCodes(); List cleanedCodes = new ArrayList(); 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 doCollectFilterKeyIds(String table, TabOrderMonitorQuery query) { String fingerprint = "filter|" + collectFingerprint(table, query); @@ -614,7 +614,7 @@ public class TabOrderMonitorService { return new ArrayList(cached.ids); } - // One Oracle key-page (ROWNUM <= limit+1). Avoid deep multi-page ROWNUM which hangs month windows. + // 一次 Oracle 主键分页查询(ROWNUM <= limit+1)。避免深层多页 ROWNUM 导致月窗口查询卡死。 List 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 statuses = query.getOrderStatuses(); @@ -1232,7 +1232,7 @@ public class TabOrderMonitorService query.setOrderStatuses(cleaned.isEmpty() ? new ArrayList(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")); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java index 36979d64..cb956525 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsProductServiceImpl.java @@ -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 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> 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 regionByNameCn = loadRegionByNameCn(); @@ -411,7 +411,7 @@ public class BmsProductServiceImpl implements IBmsProductService } List euRegions = loadEuRegions(regionByNameCn); Map 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 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 enrichInlineProductsFromCatalog(Workbook workbook, List inlineProducts) { Map catalogByCode = new HashMap(); @@ -903,7 +903,7 @@ public class BmsProductServiceImpl implements IBmsProductService } catch (Exception ignored) { - // Catalog optional for inline import. + // 内联导入中产品目录为可选。 } } List out = new ArrayList(); @@ -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> parseProductSheetByInlineCode(Sheet sheet, Map catalogCodeByName, Map 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 buildCatalogCodeByName(Workbook workbook) { Map map = new LinkedHashMap(); @@ -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 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 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 regionByNameCn, List 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 regionByNameCn, List euRegions, Map zoneByCountryAndCode, Set 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 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 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 parseCatalog(Sheet sheet) { List 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 dedupeCatalogProducts(List products) { Map 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 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 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 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 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 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 regionByNameCn, List 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 products, Map regionByNameCn, List 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 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 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 countries, String operName) { product.setCreateBy(operName); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsRedlineProductServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsRedlineProductServiceImpl.java index 027be1ff..f5599c5b 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsRedlineProductServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/BmsRedlineProductServiceImpl.java @@ -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 0) { - // Recalc status when effective/expire dates change + // 生效/失效日期变化时重新计算状态 countryMapper.refreshStatusByDates(); List> 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 regionByNameCn = loadRegionByNameCn(); @@ -389,7 +389,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService } List euRegions = loadEuRegions(regionByNameCn); Map 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 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 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 regionByNameCn, List euRegions, Map zoneByCountryAndCode, Set 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 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 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 parseCatalog(Sheet sheet) { List 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 dedupeCatalogProducts(List products) { Map 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 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 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 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 enrichInlineProductsFromCatalog(Workbook workbook, List inlineProducts) { Map catalogByCode = new HashMap(); @@ -2613,7 +2613,7 @@ public class BmsRedlineProductServiceImpl implements IBmsRedlineProductService } catch (Exception ignored) { - // Catalog optional for inline import. + // 内联导入中产品目录为可选。 } } List out = new ArrayList(); @@ -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> parseProductSheetByInlineCode(Sheet sheet, Map catalogCodeByName, Map 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 buildCatalogCodeByName(Workbook workbook) { Map map = new LinkedHashMap(); @@ -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 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 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 products, Map regionByNameCn, List 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 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 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 countries, String operName) { product.setCreateBy(operName); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcBillingTaskServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcBillingTaskServiceImpl.java index a6683b50..f9c4a913 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcBillingTaskServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcBillingTaskServiceImpl.java @@ -272,7 +272,7 @@ public class PrcBillingTaskServiceImpl implements IPrcBillingTaskService private void billFromUpstreamRow(PrcBillingTask task, Map 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 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\"}"); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java index b4dafebb..e5fba6ad 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcOrderBillServiceImpl.java @@ -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 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). - *

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 思路一致)。 + *

若本地订单属于退费类(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 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 resolveListScopedBills(String scopeModeRaw, List billIdsRaw, List orderNosRaw, List trackingNosRaw, List transferNosRaw, List 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 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 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 filterAutoFeesAgainstManual(String billId, List 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 billByOrderNo = new java.util.HashMap(); List allOrderNos = new ArrayList(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) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceExcelFileServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceExcelFileServiceImpl.java index f5019f5e..3466ff36 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceExcelFileServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceExcelFileServiceImpl.java @@ -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); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java index a9030edf..67604ccf 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcPriceSetServiceImpl.java @@ -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>> countryCacheTl = new ThreadLocal>>(); - /** Request-scoped cache: productCode -> psp_id (bulk upsert). */ + /** 请求级缓存:productCode -> psp_id(批量 upsert)。 */ private final ThreadLocal> setProductCacheTl = new ThreadLocal>(); @@ -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()); @@ -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 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 details = new ArrayList(); - // 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 replaceScopes = collectImportReplaceScopes(byProduct); List deleteIds = new ArrayList(); 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> excelGroups = new LinkedHashMap>(); 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 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 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 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 findDetailDuplicateTips(String setType, List 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()); PrcCustomerPriceSet cquery = new PrcCustomerPriceSet(); cquery.setPriceSetId(priceSetId); @@ -3666,7 +3666,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService pq.setPsvId(version.getPsvId()); List products = setProductMapper.selectPrcPriceSetProductList(pq); version.setProducts(products == null ? new ArrayList() : products); - // details + SALES surcharge rels are loaded on demand (edit price / detail drawer) + // 明细 + SALES 附加费关联按需加载(编辑价格/明细抽屉) version.setDetails(new ArrayList()); } @@ -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 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 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 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 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); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcZoneServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcZoneServiceImpl.java index f868cfcd..dd6ca0ec 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcZoneServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/PrcZoneServiceImpl.java @@ -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> parseZonePostcodes(Sheet sheet) { return parseTwoColZoneMap(sheet, true); } - /** Canada style: remote postcode + remote zone */ + /** 加拿大样式:偏远邮编 + 偏远分区 */ private Map> parseRemoteZonePostcodes(Sheet sheet) { Map> map = new LinkedHashMap<>(); @@ -524,7 +524,7 @@ public class PrcZoneServiceImpl implements IPrcZoneService return map; } - /** Japan style: province + postcode */ + /** 日本样式:州省 + 邮编 */ private Map> parseProvincePostcodes(Sheet sheet) { Map> 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> parseCoveragePostcodes(Sheet sheet) { Map> 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> parseUkSurchargePostcodes(Sheet sheet) { Map> map = new LinkedHashMap<>(); int lastRow = sheet.getLastRowNum(); - // detect header row containing ???? + // 检测包含 ???? 的表头行 int headerRow = -1; for (int r = 0; r <= Math.min(lastRow, 10); r++) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java index 553fcae5..a0319f40 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/TmsPriceQuoteServiceImpl.java @@ -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(byCode.values()); } - /** Prefer version active at asOf; else latest version that has set-product rows. */ + /** 优先取 asOf 时点生效的版本;否则取包含套餐产品行的最新版本。 */ private List 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 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 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 payLines, String feeType) { if (payLines == null || feeType == null) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/support/BillingFailReasonSupport.java b/bms-back/bms-system/src/main/java/com/bms/price/service/support/BillingFailReasonSupport.java index 02c4435e..c9b6db21 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/support/BillingFailReasonSupport.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/support/BillingFailReasonSupport.java @@ -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 { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/support/CatalogVersionSupport.java b/bms-back/bms-system/src/main/java/com/bms/price/service/support/CatalogVersionSupport.java index 6783cd76..6a1a8a61 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/support/CatalogVersionSupport.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/support/CatalogVersionSupport.java @@ -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 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) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/support/ExclusiveHalaraQuoteExcelParser.java b/bms-back/bms-system/src/main/java/com/bms/price/service/support/ExclusiveHalaraQuoteExcelParser.java index bf52f19b..cb10558e 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/support/ExclusiveHalaraQuoteExcelParser.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/support/ExclusiveHalaraQuoteExcelParser.java @@ -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 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> ownersBySheet = new LinkedHashMap>(); @@ -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
" + String.join("
", dupTips)); + throw new ServiceException("专属报价存在重复国家+重量段:
" + String.join("
", 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 parseCatalog(Sheet sheet) { List list = new ArrayList(); - 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; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java b/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java index 9ab6ee9e..7a05a867 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java @@ -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 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 list) { ExcelUtil.export(response, toOrderListRows(list), PrcOrderBillListExportRow.class, "订单列表"); } - /** Export billing-list Excel by current query result. */ + /** 按当前查询结果导出计费列表 Excel。 */ public static void exportBillingList(HttpServletResponse response, List list) { ExcelUtil.export(response, toBillingListRows(list), PrcBillingBillListExportRow.class, "计费列表"); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/BmsProductImportAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/BmsProductImportAsyncRunner.java index ad336875..3027a334 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/BmsProductImportAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/BmsProductImportAsyncRunner.java @@ -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 bytesList, List 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 bytesList, List 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 parseCatalogFiles(String taskId, List bytesList, List 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; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/BmsRedlineProductImportAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/BmsRedlineProductImportAsyncRunner.java index 89423e3f..260b8ec6 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/BmsRedlineProductImportAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/BmsRedlineProductImportAsyncRunner.java @@ -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 bytesList, List 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 bytesList, List 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 parseCatalogFiles(String taskId, List bytesList, List 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; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskNightRetryTask.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskNightRetryTask.java index 340913bd..6bc9f957 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskNightRetryTask.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskNightRetryTask.java @@ -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。 * - *

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).

+ *

在 [startHour, endHour) 内(默认全天 00:00-24:00):拉取最多 {@value #DEFAULT_BATCH} + * 个任务 id(不限 success_status)-> 全部计费 -> 再拉下一页。 + * 空页跳过计费并结束本次触发(新数据由下次 cron 处理)。

* - *

Same path as monitor manual / filter retry (reset to WAITING then process).

+ *

与监控页手工 / 筛选重试走同一路径(先重置为 WAITING 再处理)。

*/ @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); } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskRetryAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskRetryAsyncRunner.java index e86ee666..0425d035 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskRetryAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcBillingTaskRetryAsyncRunner.java @@ -152,7 +152,7 @@ public class PrcBillingTaskRetryAsyncRunner return n; } - /** Build MyBatis query params map for begin/end time. */ + /** 构造按开始/结束时间查询的 MyBatis 参数 Map。 */ public static Map timeParams(String beginTime, String endTime) { Map params = new HashMap(); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcChargeWeightImportAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcChargeWeightImportAsyncRunner.java index ff3aba2b..298d1775 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcChargeWeightImportAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcChargeWeightImportAsyncRunner.java @@ -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; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillExportAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillExportAsyncRunner.java index 90ada3ef..94eb14da 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillExportAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillExportAsyncRunner.java @@ -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(); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillLocalRebillAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillLocalRebillAsyncRunner.java index 6f46763a..2d8c9809 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillLocalRebillAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillLocalRebillAsyncRunner.java @@ -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 ids = billIds; List> workers = new ArrayList>(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; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcPriceSetSaveAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcPriceSetSaveAsyncRunner.java index 63d76b6a..90990b54 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcPriceSetSaveAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcPriceSetSaveAsyncRunner.java @@ -53,7 +53,7 @@ public class PrcPriceSetSaveAsyncRunner } } - /** Background write of detail surcharge snapshots for many rows. */ + /** 后台批量写入明细附加费快照。 */ @Async("threadPoolTaskExecutor") public void runSurchargeBatch(String taskId, List 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 bytesList, List 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) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderCompareAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderCompareAsyncRunner.java index 293e2460..f45fcef6 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderCompareAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderCompareAsyncRunner.java @@ -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 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 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 resolveCompareStatuses(TabOrderMonitorQuery query) { @@ -202,7 +202,7 @@ public class TabOrderCompareAsyncRunner return cleaned.isEmpty() ? new ArrayList(COMPARE_LOCAL_ORDER_STATUSES) : cleaned; } - /** transferNo(upper) present locally with the given order_status filter. */ + /** 按给定的 order_status 筛选,返回本地已存在的 transferNo(大写)。 */ private Set findLocalTransferNos(List transferNos, List customerCodes, List orderStatuses) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderExportAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderExportAsyncRunner.java index 8ee8e0bc..c87fc7e5 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderExportAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderExportAsyncRunner.java @@ -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; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderSyncAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderSyncAsyncRunner.java index 0444d23c..9fb86c25 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderSyncAsyncRunner.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/TabOrderSyncAsyncRunner.java @@ -193,14 +193,14 @@ public class TabOrderSyncAsyncRunner } } - /** Sync selected rows with a short-lived worker pool (for HTTP syncSelected). */ + /** 用短生命周期的线程池同步选中的行(供 HTTP syncSelected 使用)。 */ public Map syncRowsParallel(List rows) { return syncRowsParallel(rows, null); } /** - * Sync selected rows (upstream field overwrite only; billType ignored). + * 同步选中的行(仅覆盖上游字段;忽略 billType)。 */ public Map syncRowsParallel(List 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 syncRowsParallelRows(List 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 raw = row == null ? null : row.getRaw(); @@ -382,7 +382,7 @@ public class TabOrderSyncAsyncRunner } catch (ExecutionException e) { - // counted in job body + // 已在任务体中计数 } } } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/util/ZoneCodeAbbrevs.java b/bms-back/bms-system/src/main/java/com/bms/price/util/ZoneCodeAbbrevs.java index e8808036..ff7a86e4 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/util/ZoneCodeAbbrevs.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/util/ZoneCodeAbbrevs.java @@ -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) { diff --git a/bms-back/bms-system/src/main/java/com/bms/system/service/impl/BmsBizRuleServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/BmsBizRuleServiceImpl.java index 595b086b..253b3991 100644 --- a/bms-back/bms-system/src/main/java/com/bms/system/service/impl/BmsBizRuleServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/BmsBizRuleServiceImpl.java @@ -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 dimValues(List list, String single) { Set set = new LinkedHashSet(); @@ -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("重量段最小值不能大于最大值"); } } } diff --git a/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysFileTemplateServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysFileTemplateServiceImpl.java index f68ecdb7..c108ef34 100644 --- a/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysFileTemplateServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/system/service/impl/SysFileTemplateServiceImpl.java @@ -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"); diff --git a/bms-front/src/utils/excelExport.js b/bms-front/src/utils/excelExport.js index 12bcd0c8..c25f4be9 100644 --- a/bms-front/src/utils/excelExport.js +++ b/bms-front/src/utils/excelExport.js @@ -147,13 +147,13 @@ export async function exportAsync(options = {}) { ignoreKeys: ['orderStatuses', 'exportFields', ...extraIgnore] } if (!hasListFilter(query, filterOpts)) { - ElMessage.warning(options.filterMessage || '\u8bf7\u5148\u8bbe\u7f6e\u5217\u8868\u67e5\u8be2\u6761\u4ef6\u540e\u518d\u5bfc\u51fa') + ElMessage.warning(options.filterMessage || '请先设置列表查询条件后再导出') return null } } const filePrefix = options.filePrefix || 'export' - const loadingText = options.loadingText || '\u6b63\u5728\u5bfc\u51fa\uff0c\u8bf7\u7a0d\u5019' + const loadingText = options.loadingText || '正在导出,请稍候' const pollIntervalMs = options.pollIntervalMs || 1000 const maxWaitMs = options.maxWaitMs || 30 * 60 * 1000 const showSuccessMessage = options.showSuccessMessage !== false @@ -215,10 +215,10 @@ export async function exportAsync(options = {}) { } const name = task.fileName || (filePrefix + '_' + Date.now() + '.xlsx') saveAs(new Blob([file]), name) - if (showSuccessMessage) ElMessage.success('\u5bfc\u51fa\u6210\u529f') + if (showSuccessMessage) ElMessage.success('导出成功') return { task, taskId } } catch (e) { - if (showErrorMessage) ElMessage.error((e && e.message) || '\u5bfc\u51fa\u5931\u8d25') + if (showErrorMessage) ElMessage.error((e && e.message) || '导出失败') return null } finally { if (loading) loading.close() diff --git a/bms-front/src/views/price/basic/components/CatalogVersionExcelDrawer.vue b/bms-front/src/views/price/basic/components/CatalogVersionExcelDrawer.vue index 94a4ca05..86f9a819 100644 --- a/bms-front/src/views/price/basic/components/CatalogVersionExcelDrawer.vue +++ b/bms-front/src/views/price/basic/components/CatalogVersionExcelDrawer.vue @@ -24,16 +24,16 @@ const props = defineProps({ const { proxy } = getCurrentInstance() -const emptyText = '\u8be5\u7248\u672c\u6682\u65e0\u5bfc\u5165\u9644\u4ef6' -const labFile = '\u6587\u4ef6\u540d\u79f0' -const labEff = '\u751f\u6548\u65f6\u95f4' -const labSize = '\u5927\u5c0f' -const labUser = '\u4e0a\u4f20\u4eba' -const labTime = '\u4e0a\u4f20\u65f6\u95f4' -const labAction = '\u64cd\u4f5c' -const labDown = '\u4e0b\u8f7d' -const labTitle = '\u5bfc\u5165\u9644\u4ef6' -const labNoPath = '\u6587\u4ef6\u8def\u5f84\u4e0d\u5b58\u5728' +const emptyText = '该版本暂无导入附件' +const labFile = '文件名称' +const labEff = '生效时间' +const labSize = '大小' +const labUser = '上传人' +const labTime = '上传时间' +const labAction = '操作' +const labDown = '下载' +const labTitle = '导入附件' +const labNoPath = '文件路径不存在' const visible = ref(false) const title = ref('') diff --git a/bms-front/src/views/price/billing/bill.vue b/bms-front/src/views/price/billing/bill.vue index 8713075c..d7cb1363 100644 --- a/bms-front/src/views/price/billing/bill.vue +++ b/bms-front/src/views/price/billing/bill.vue @@ -997,33 +997,33 @@ function applyCwImportProgress(t) { const success = Number(t.success) || 0 const fail = Number(t.fail) || 0 if (t.status === 'PENDING') { - cwImportProgressMessage.value = '\u6392\u961f\u4e2d\u2026' + cwImportProgressMessage.value = '排队中…' } else if (t.status === 'RUNNING') { if (total > 0) { - cwImportProgressMessage.value = '\u6b63\u5728\u5904\u7406 ' + success + '/' + total + - (fail ? ('\uff08\u5931\u8d25 ' + fail + '\uff09') : '') + cwImportProgressMessage.value = '正在处理 ' + success + '/' + total + + (fail ? ('(失败 ' + fail + ')') : '') } else { - cwImportProgressMessage.value = t.message || '\u6b63\u5728\u5bfc\u5165\u2026' + cwImportProgressMessage.value = t.message || '正在导入…' } } else if (t.status === 'SUCCESS') { - cwImportProgressMessage.value = '\u5b8c\u6210\uff1a\u6210\u529f ' + success + '\uff0c\u5931\u8d25 ' + fail + cwImportProgressMessage.value = '完成:成功 ' + success + ',失败 ' + fail cwImportProgressStatus.value = fail > 0 ? 'warning' : 'success' } else if (t.status === 'FAIL') { - cwImportProgressMessage.value = t.error || t.message || '\u5bfc\u5165\u5931\u8d25' + cwImportProgressMessage.value = t.error || t.message || '导入失败' cwImportProgressStatus.value = 'exception' } } async function runChargeWeightImportAsync(uploadFile) { const raw = uploadFile && (uploadFile.raw || uploadFile) - if (!raw) throw new Error('\u8bf7\u5148\u9009\u62e9\u6587\u4ef6') + if (!raw) throw new Error('请先选择文件') // Close import dialog so progress dialog is the only visible wait UI chargeWeightImportRef.value?.close?.() cwImportProgressTitle.value = '正在导入修改计费重' cwImportProgressVisible.value = true cwImportProgressPercent.value = 0 cwImportProgressIndeterminate.value = true - cwImportProgressMessage.value = '\u6b63\u5728\u89e3\u6790Excel\u5e76\u6821\u9a8c\u8ba2\u5355\uff0c\u8bf7\u7a0d\u5019\u2026' + cwImportProgressMessage.value = '正在解析Excel并校验订单,请稍候…' cwImportProgressStatus.value = undefined try { const { task } = await importChargeWeightAsync(raw, applyCwImportProgress) @@ -1031,12 +1031,12 @@ async function runChargeWeightImportAsync(uploadFile) { cwImportProgressPercent.value = 100 const success = Number(task && task.success) || 0 const fail = Number(task && task.fail) || 0 - const tip = '\u5bfc\u5165\u5b8c\u6210\uff1a\u6210\u529f ' + success + '\uff0c\u5931\u8d25 ' + fail + const tip = '导入完成:成功 ' + success + ',失败 ' + fail if (fail > 0) { cwImportProgressStatus.value = 'warning' - cwImportProgressMessage.value = tip + (task && task.error ? ('\uff1b' + task.error) : '') + cwImportProgressMessage.value = tip + (task && task.error ? (';' + task.error) : '') await new Promise(resolve => setTimeout(resolve, 1500)) - return tip + (task && task.error ? ('\uff1b' + task.error) : '') + return tip + (task && task.error ? (';' + task.error) : '') } cwImportProgressStatus.value = 'success' cwImportProgressMessage.value = tip @@ -1045,7 +1045,7 @@ async function runChargeWeightImportAsync(uploadFile) { } catch (e) { cwImportProgressIndeterminate.value = false cwImportProgressStatus.value = 'exception' - const tip = (e && e.task && e.task.error) || (e && e.message) || '\u5bfc\u5165\u5931\u8d25' + const tip = (e && e.task && e.task.error) || (e && e.message) || '导入失败' cwImportProgressMessage.value = tip await new Promise(resolve => setTimeout(resolve, 1200)) throw e diff --git a/bms-front/src/views/price/set/components/create/StepDetails.vue b/bms-front/src/views/price/set/components/create/StepDetails.vue index e7721b91..3e7c4098 100644 --- a/bms-front/src/views/price/set/components/create/StepDetails.vue +++ b/bms-front/src/views/price/set/components/create/StepDetails.vue @@ -339,9 +339,9 @@ const importFreshCardDesc = computed(() => onlyImportFresh.value const importDesc = computed(() => { if (onlyImportFresh.value) { return [ - '\u4e13\u5c5e\u62a5\u4ef7\u4ec5\u652f\u6301\u5927\u5ba2\u6298\u6263\u62a5\u4ef7\u8868\uff08\u542b\u300c\u4ea7\u54c1\u76ee\u5f55\u300d\u4e0e\u5404\u56fd\u4ef7\u8868\uff09\u3002', - '\u8868\u5934\u4e3a\u76ee\u7684\u5730 / \u91cd\u91cf\u6bb5 / \u8fd0\u8d39(CNY/KG) / \u64cd\u4f5c\u8d39(CNY/PCS)\uff0c\u4e0e\u666e\u901a\u5957\u9910 Excel \u6a21\u677f\u4e0d\u540c\u3002', - '\u4ec5\u300cExcel \u65b0\u5efa\u4ef7\u300d\uff1a\u4e0d\u62c9\u53d6\u4ea7\u54c1\u5e93\u3001\u4e0d\u5ef6\u7528\u4e0a\u4e00\u7248\uff0c\u5bfc\u5165\u540e\u76f4\u63a5\u751f\u6210\u9500\u552e\u4ef7\u660e\u7ec6\u3002' + '专属报价仅支持大客折扣报价表(含「产品目录」与各国价表)。', + '表头为目的地 / 重量段 / 运费(CNY/KG) / 操作费(CNY/PCS),与普通套餐 Excel 模板不同。', + '仅「Excel 新建价」:不拉取产品库、不延用上一版,导入后直接生成销售价明细。' ] } if (isModifyImport.value) { diff --git a/bms-front/src/views/price/set/components/detail/SetVersionDetailDrawer.vue b/bms-front/src/views/price/set/components/detail/SetVersionDetailDrawer.vue index a99e86d6..674430c6 100644 --- a/bms-front/src/views/price/set/components/detail/SetVersionDetailDrawer.vue +++ b/bms-front/src/views/price/set/components/detail/SetVersionDetailDrawer.vue @@ -199,9 +199,9 @@ const productCountText = computed(() => { const n = filteredProducts.value.length const total = (products.value || []).length if (n === total) { - return '\u5171 ' + n + ' \u4e2a\u4ea7\u54c1' + return '共 ' + n + ' 个产品' } - return '\u5171 ' + n + ' \u4e2a\u4ea7\u54c1\uff08\u5168\u90e8 ' + total + ' \u4e2a\uff09' + return '共 ' + n + ' 个产品(全部 ' + total + ' 个)' }) function countryKey(row) {