diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/monitor/PriceLogController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/monitor/PriceLogController.java index 50d20834..b3e3a765 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/monitor/PriceLogController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/monitor/PriceLogController.java @@ -16,6 +16,7 @@ import com.bms.common.core.page.TableSupport; import com.bms.price.search.BillSearchPage; import com.bms.price.search.EsLogType; import com.bms.price.search.IBillSearchStore; +import com.bms.price.search.PriceChangeGroupPage; /** * 来自 ES 的价格变更前后日志(销售价 / 产品价 / 红线价)。 @@ -31,12 +32,40 @@ public class PriceLogController extends BaseController @GetMapping("/list") public TableDataInfo list( @RequestParam(value = "bizId", required = false) String bizId, - @RequestParam(value = "status", required = false) String status) + @RequestParam(value = "status", required = false) String status, + @RequestParam(value = "psvId", required = false) Long psvId, + @RequestParam(value = "priceKind", required = false) String priceKind, + @RequestParam(value = "productCode", required = false) String productCode, + @RequestParam(value = "updateTime", required = false) String updateTime, + @RequestParam(value = "updateTimePrefix", required = false) String updateTimePrefix, + @RequestParam(value = "countryKeyword", required = false) String countryKeyword) { PageDomain pageDomain = TableSupport.buildPageRequest(); int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue(); int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue(); - BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, pageNum, pageSize); + BillSearchPage page = billSearchStore.search(EsLogType.PRICE_CHANGE, bizId, status, psvId, priceKind, + productCode, updateTime, updateTimePrefix, countryKeyword, pageNum, pageSize); + TableDataInfo rsp = new TableDataInfo(); + rsp.setCode(HttpStatus.SUCCESS); + rsp.setMsg("查询成功"); + rsp.setRows(page.getRows()); + rsp.setTotal(page.getTotal()); + return rsp; + } + + /** Group by productCode + write time for package-version drill-down. */ + @PreAuthorize("@ss.hasPermi('monitor:priceLog:list')") + @GetMapping("/groups") + public TableDataInfo groups( + @RequestParam(value = "psvId", required = true) Long psvId, + @RequestParam(value = "status", required = false) String status, + @RequestParam(value = "priceKind", required = false) String priceKind) + { + PageDomain pageDomain = TableSupport.buildPageRequest(); + int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue(); + int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue(); + PriceChangeGroupPage page = billSearchStore.searchPriceChangeGroups(psvId, priceKind, status, pageNum, + pageSize); TableDataInfo rsp = new TableDataInfo(); rsp.setCode(HttpStatus.SUCCESS); rsp.setMsg("查询成功"); diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java index cc8a5d03..4e348900 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/BillingController.java @@ -22,9 +22,12 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import com.bms.common.annotation.Log; +import com.bms.common.constant.HttpStatus; import com.bms.common.core.controller.BaseController; import com.bms.common.core.domain.AjaxResult; +import com.bms.common.core.page.PageDomain; import com.bms.common.core.page.TableDataInfo; +import com.bms.common.core.page.TableSupport; import com.bms.common.enums.BusinessType; import com.bms.common.utils.DateUtils; import com.bms.common.utils.MessageUtils; @@ -54,6 +57,11 @@ import com.bms.price.domain.vo.TmsPriceQuoteRequest; import com.bms.price.domain.vo.TmsPriceQuoteResult; import com.bms.price.enums.BillTaskSuccessStatus; import com.bms.price.enums.BillTaskType; +import com.bms.price.search.BillSearchPage; +import com.bms.price.search.BillSyncSnapshot; +import com.bms.price.search.EsBizLogSupport; +import com.bms.price.search.EsLogType; +import com.bms.price.search.IBillSearchStore; import com.bms.price.service.IPrcBillingTaskService; import com.bms.price.service.IPrcOrderBillService; import com.bms.price.service.ITmsPriceQuoteService; @@ -84,6 +92,9 @@ public class BillingController extends BaseController @Autowired private OrderBillProductCheckService orderBillProductCheckService; + @Autowired + private IBillSearchStore billSearchStore; + /** * Historical quote list. */ @@ -355,6 +366,97 @@ public class BillingController extends BaseController return success(prcOrderBillService.getBill(billId)); } + /** + * ES BILL logs for this order (billing / upstream sync / fail snapshots). + * bizId prefers transferNo then orderNo (same as write path). + */ + @PreAuthorize("@ss.hasPermi('price:billing:bill') or @ss.hasPermi('price:orderBill:list')") + @GetMapping("/bill/{billId}/history") + public TableDataInfo billHistory(@PathVariable("billId") String billId) + { + if (StringUtils.isEmpty(billId)) + { + return getDataTable(new ArrayList()); + } + TmsPriceQuoteResult bill = prcOrderBillService.getBill(billId.trim()); + if (bill == null) + { + TableDataInfo empty = new TableDataInfo(); + empty.setCode(HttpStatus.SUCCESS); + empty.setMsg("查询成功"); + empty.setRows(new ArrayList()); + empty.setTotal(0L); + return empty; + } + PageDomain pageDomain = TableSupport.buildPageRequest(); + int pageNum = pageDomain.getPageNum() == null ? 1 : pageDomain.getPageNum().intValue(); + int pageSize = pageDomain.getPageSize() == null ? 10 : pageDomain.getPageSize().intValue(); + String primary = EsBizLogSupport.billBizId(bill.getTransferNo(), bill.getOrderNo()); + BillSearchPage page = billSearchStore.search(EsLogType.BILL, primary, null, pageNum, pageSize); + // Also pull by orderNo when transferNo was used as bizId, so older orderNo-keyed rows appear. + if (StringUtils.isNotEmpty(bill.getTransferNo()) && StringUtils.isNotEmpty(bill.getOrderNo()) + && !bill.getTransferNo().trim().equalsIgnoreCase(bill.getOrderNo().trim()) + && pageNum == 1) + { + BillSearchPage byOrder = billSearchStore.search(EsLogType.BILL, bill.getOrderNo().trim(), null, 1, + Math.max(pageSize, 50)); + page = mergeBillHistory(page, byOrder, pageSize); + } + TableDataInfo rsp = new TableDataInfo(); + rsp.setCode(HttpStatus.SUCCESS); + rsp.setMsg("查询成功"); + rsp.setRows(page.getRows()); + rsp.setTotal(page.getTotal()); + return rsp; + } + + private BillSearchPage mergeBillHistory(BillSearchPage primary, BillSearchPage secondary, int pageSize) + { + BillSearchPage out = new BillSearchPage(); + Map byId = new HashMap(); + if (primary != null && primary.getRows() != null) + { + for (BillSyncSnapshot row : primary.getRows()) + { + if (row != null && StringUtils.isNotEmpty(row.getDocId())) + { + byId.put(row.getDocId(), row); + } + } + } + if (secondary != null && secondary.getRows() != null) + { + for (BillSyncSnapshot row : secondary.getRows()) + { + if (row != null && StringUtils.isNotEmpty(row.getDocId()) && !byId.containsKey(row.getDocId())) + { + byId.put(row.getDocId(), row); + } + } + } + List merged = new ArrayList(byId.values()); + merged.sort((a, b) -> { + String ta = a == null || a.getUpdateTime() == null ? "" : a.getUpdateTime(); + String tb = b == null || b.getUpdateTime() == null ? "" : b.getUpdateTime(); + return tb.compareTo(ta); + }); + long total = Math.max(primary == null ? 0L : primary.getTotal(), merged.size()); + if (secondary != null) + { + total = Math.max(total, secondary.getTotal()); + } + out.setTotal(total); + if (merged.size() > pageSize) + { + out.setRows(new ArrayList(merged.subList(0, pageSize))); + } + else + { + out.setRows(merged); + } + return out; + } + /** * Refund one receivable bill via billing task billType=REFUND. */ diff --git a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SrvBillAirController.java b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SrvBillAirController.java index 503111b6..9b3d2977 100644 --- a/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SrvBillAirController.java +++ b/bms-back/bms-admin/src/main/java/com/bms/web/controller/price/SrvBillAirController.java @@ -1,18 +1,26 @@ package com.bms.web.controller.price; +import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.bms.common.annotation.Log; import com.bms.common.core.controller.BaseController; import com.bms.common.core.domain.AjaxResult; import com.bms.common.core.page.TableDataInfo; +import com.bms.common.enums.BusinessType; import com.bms.common.utils.StringUtils; +import com.bms.price.airbill.domain.SrvBillAirFeeLog; +import com.bms.price.airbill.domain.SrvBillAirFeeSave; import com.bms.price.domain.vo.SrvBillAirQuery; import com.bms.price.domain.vo.SrvBillAirVo; +import com.bms.price.service.ISrvBillAirFeeService; import com.bms.price.service.ISrvBillAirService; /** @@ -25,6 +33,9 @@ public class SrvBillAirController extends BaseController @Autowired private ISrvBillAirService srvBillAirService; + @Autowired + private ISrvBillAirFeeService srvBillAirFeeService; + /** * 提单列表 */ @@ -55,4 +66,32 @@ public class SrvBillAirController extends BaseController } return success(vo); } + + /** 一票提单下未删除的费用 */ + @PreAuthorize("@ss.hasPermi('price:airBill:fee')") + @GetMapping("/fee/list") + public AjaxResult feeList(@RequestParam("billCode") String billCode) + { + return success(srvBillAirFeeService.listFees(billCode)); + } + + /** 提单费用的新增、修改、删除日志,按页返回 */ + @PreAuthorize("@ss.hasPermi('price:airBill:fee')") + @GetMapping("/fee/log") + public TableDataInfo feeLog(@RequestParam("billCode") String billCode) + { + List list = srvBillAirFeeService.listLogs(billCode); + TableDataInfo table = getDataTable(list); + table.setRows(new ArrayList<>(list)); + return table; + } + + /** 只保存本次新增、修改、删除的费用,并写入提单日志 */ + @PreAuthorize("@ss.hasPermi('price:airBill:fee')") + @Log(title = "提单费用", businessType = BusinessType.UPDATE) + @PostMapping("/fee/save") + public AjaxResult saveFee(@RequestBody SrvBillAirFeeSave command) + { + return success(srvBillAirFeeService.save(command)); + } } diff --git a/bms-back/bms-admin/src/main/resources/application-local.yml b/bms-back/bms-admin/src/main/resources/application-local.yml index d9e07474..f9a2f83b 100644 --- a/bms-back/bms-admin/src/main/resources/application-local.yml +++ b/bms-back/bms-admin/src/main/resources/application-local.yml @@ -38,8 +38,8 @@ spring: initialSize: 5 # 最小连接池数量 minIdle: 10 - # 最大连接池数量 - maxActive: 20 + # 最大连接池数量(local rebill 16 workers + UI/MQ headroom) + maxActive: 40 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置连接超时时间 diff --git a/bms-back/bms-admin/src/main/resources/application-prod.yml b/bms-back/bms-admin/src/main/resources/application-prod.yml index 1ea85a50..ccf6cb81 100644 --- a/bms-back/bms-admin/src/main/resources/application-prod.yml +++ b/bms-back/bms-admin/src/main/resources/application-prod.yml @@ -51,8 +51,8 @@ spring: initialSize: 5 # 最小连接池数量 minIdle: 10 - # 最大连接池数量 - maxActive: 20 + # 最大连接池数量(local rebill 16 workers + UI/MQ headroom) + maxActive: 40 # 配置获取连接等待超时的时间 maxWait: 60000 # 配置连接超时时间 diff --git a/bms-back/bms-admin/src/main/resources/application.yml b/bms-back/bms-admin/src/main/resources/application.yml index a61fcc06..29427a10 100644 --- a/bms-back/bms-admin/src/main/resources/application.yml +++ b/bms-back/bms-admin/src/main/resources/application.yml @@ -40,6 +40,8 @@ bms: # 同步已直接复用订单行(跳过每单 Oracle 公网查询),瓶颈在 MySQL 计费写入, # 同客户行锁冲突由 billWithDbRetry 3 次重试兜底,16 路实测吞吐最高。 order-source-sync-threads: 16 + # Order-list local rebill workers (2~24). Pair with Druid maxActive >= threads + 8. + local-rebill-threads: 16 # Night auto-rebill: hours 0-8, pull 500 (all statuses) -> bill -> next 500 until empty / 09:00. # Empty page => no billing. night-retry: diff --git a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java index fbc69aae..22a501a9 100644 --- a/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java +++ b/bms-back/bms-system/src/main/java/com/bms/customer/customerinfo/service/impl/BmsCustomerLogServiceImpl.java @@ -450,6 +450,11 @@ public class BmsCustomerLogServiceImpl implements IBmsCustomerLogService realChanges.add(c); } } + // Rebind/update with no field diff: skip empty UPDATE noise (full package save upsert) + if (existing != null && realChanges.isEmpty()) + { + return; + } saveLog(bind.getCustomerId(), customer == null ? null : customer.getCustomerCode(), customer == null ? null : customer.getCustomerName(), BmsCustomerLog.MODULE_PRICE_SET, newSet.getPriceSetName(), diff --git a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFee.java b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFee.java new file mode 100644 index 00000000..4b59b770 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFee.java @@ -0,0 +1,40 @@ +package com.bms.price.airbill.domain; + +import java.math.BigDecimal; +import java.util.Date; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** + * 空运提单费用明细 srv_bill_air_fee。 + */ +@Data +public class SrvBillAirFee +{ + private Long id; + + private String billCode; + + /** 字典 bms_fee_type */ + private String feeType; + + private BigDecimal price; + + /** 字典 bms_currency */ + private String currency; + + private String remark; + + private String createBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + private String updateBy; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; + + /** 0未删除 1已删除 */ + private String delFlag; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeLog.java b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeLog.java new file mode 100644 index 00000000..9525a3ba --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeLog.java @@ -0,0 +1,64 @@ +package com.bms.price.airbill.domain; + +import java.util.Date; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** + * 空运提单费用操作日志。 + */ +@Data +public class SrvBillAirFeeLog +{ + public static final String INSERT = "1"; + + public static final String UPDATE = "2"; + + public static final String DELETE = "3"; + + private Long logId; + + private String billCode; + + private Long feeId; + + private String operationType; + + private String summary; + + private String changeContent; + + private String createBy; + + private String createByName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + private List changes; + + @Data + public static class FieldChange + { + private String field; + + private String fieldLabel; + + private String oldValue; + + private String newValue; + + public FieldChange() + { + } + + public FieldChange(String field, String fieldLabel, String oldValue, String newValue) + { + this.field = field; + this.fieldLabel = fieldLabel; + this.oldValue = oldValue; + this.newValue = newValue; + } + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeSave.java b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeSave.java new file mode 100644 index 00000000..daaad8b6 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/airbill/domain/SrvBillAirFeeSave.java @@ -0,0 +1,20 @@ +package com.bms.price.airbill.domain; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +/** + * 保存提单费用时只带有变动的行。未出现的费用保持原样。 + */ +@Data +public class SrvBillAirFeeSave +{ + private String billCode; + + private List inserts = new ArrayList<>(); + + private List updates = new ArrayList<>(); + + private List deletes = new ArrayList<>(); +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/config/BillingProperties.java b/bms-back/bms-system/src/main/java/com/bms/price/config/BillingProperties.java index fd8ecb2e..7dd9c9c0 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/config/BillingProperties.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/config/BillingProperties.java @@ -20,6 +20,11 @@ public class BillingProperties private int consumerThreads = 8; /** Concurrent threads for monitor order-source sync */ private int orderSourceSyncThreads = 8; + /** + * Concurrent threads for order-list local rebill (2~24). + * Keep below Druid maxActive with headroom for other traffic. + */ + private int localRebillThreads = 16; /** Night window auto-retry of prc_billing_task (all statuses) */ private NightRetry nightRetry = new NightRetry(); private Oracle oracle = new Oracle(); @@ -84,6 +89,16 @@ public class BillingProperties this.orderSourceSyncThreads = orderSourceSyncThreads; } + public int getLocalRebillThreads() + { + return localRebillThreads; + } + + public void setLocalRebillThreads(int localRebillThreads) + { + this.localRebillThreads = localRebillThreads; + } + public NightRetry getNightRetry() { return nightRetry; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/BatchAddIncomeBillFeeRequest.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/BatchAddIncomeBillFeeRequest.java index 511ee24e..58ddd1a4 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/BatchAddIncomeBillFeeRequest.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/BatchAddIncomeBillFeeRequest.java @@ -40,6 +40,10 @@ public class BatchAddIncomeBillFeeRequest implements Serializable private String beginBizDate; private String endBizDate; + /** Receive/pickup time range (pickup_date), inclusive. */ + private String beginPickupDate; + private String endPickupDate; + /** Surcharge config id (prc_surcharge.sc_id). */ @NotNull(message = "scId required") private Long scId; diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightTemplateRequest.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightTemplateRequest.java index d6fc692a..f718ccb1 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightTemplateRequest.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/ChargeWeightTemplateRequest.java @@ -25,4 +25,7 @@ public class ChargeWeightTemplateRequest implements Serializable private List orderStatuses; private String beginBizDate; private String endBizDate; + /** Receive/pickup time range (pickup_date), inclusive. */ + private String beginPickupDate; + private String endPickupDate; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcBillingBillListExportRow.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcBillingBillListExportRow.java index 4919e8e0..457b70ea 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcBillingBillListExportRow.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcBillingBillListExportRow.java @@ -56,7 +56,7 @@ public class PrcBillingBillListExportRow implements Serializable @Excel(name = "计费时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date bizDate; - @Excel(name = "提货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss") + @Excel(name = "收货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date pickupDate; @Excel(name = "销售员", width = 12) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/RecalcBillPriceRequest.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/RecalcBillPriceRequest.java index 0f398f58..9a36c6dd 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/RecalcBillPriceRequest.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/RecalcBillPriceRequest.java @@ -31,4 +31,7 @@ public class RecalcBillPriceRequest implements Serializable private List billingFailStatuses; private String beginBizDate; private String endBizDate; + /** Receive/pickup time range (pickup_date), inclusive. */ + private String beginPickupDate; + private String endPickupDate; } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPriceSetDetailMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPriceSetDetailMapper.java index 38b8bfb0..a63c77c1 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPriceSetDetailMapper.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/PrcPriceSetDetailMapper.java @@ -44,6 +44,9 @@ public interface PrcPriceSetDetailMapper public int countByPsvId(Long psvId); + /** Count detail rows including soft-deleted (for deleted-version tab). */ + public int countByPsvIdAll(Long psvId); + public int copyDetailsFromVersion(@Param("srcPsvId") Long srcPsvId, @Param("newPsvId") Long newPsvId); /** Copy version effective time, expire time and status onto catalog detail rows. */ diff --git a/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirFeeMapper.java b/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirFeeMapper.java new file mode 100644 index 00000000..68325483 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/mapper/SrvBillAirFeeMapper.java @@ -0,0 +1,28 @@ +package com.bms.price.mapper; + +import java.util.List; +import org.apache.ibatis.annotations.Param; +import com.bms.price.airbill.domain.SrvBillAirFee; +import com.bms.price.airbill.domain.SrvBillAirFeeLog; + +/** + * 空运提单费用及操作日志。 + */ +public interface SrvBillAirFeeMapper +{ + List selectByBillCode(String billCode); + + /** 只取这一票提单上仍有效的费用,供修改和删除前核对。 */ + SrvBillAirFee selectLive(@Param("id") Long id, @Param("billCode") String billCode); + + int insertFee(SrvBillAirFee fee); + + int updateFee(SrvBillAirFee fee); + + int deleteFee(@Param("id") Long id, @Param("billCode") String billCode, @Param("updateBy") String updateBy, + @Param("updateTime") java.util.Date updateTime); + + int insertLog(SrvBillAirFeeLog log); + + List selectLogs(String billCode); +} 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 1679feb8..41556ed9 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 @@ -1232,14 +1232,10 @@ public class TabOrderMonitorService query.setOrderStatuses(cleaned.isEmpty() ? new ArrayList(DEFAULT_ORDER_STATUSES) : cleaned); } - /** Empty or anything other than 0/1 falls back to 1(已收货). */ + /** Always force BL_PICKUP=1; UI no longer exposes this filter. */ private static void normalizeBlPickup(TabOrderMonitorQuery query) { - Integer blPickup = query.getBlPickup(); - if (blPickup == null || (blPickup.intValue() != 0 && blPickup.intValue() != 1)) - { - query.setBlPickup(Integer.valueOf(1)); - } + query.setBlPickup(Integer.valueOf(1)); } /** diff --git a/bms-back/bms-system/src/main/java/com/bms/price/search/EsBillSearchStore.java b/bms-back/bms-system/src/main/java/com/bms/price/search/EsBillSearchStore.java index 2e0ec3c6..52f99f25 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/search/EsBillSearchStore.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/search/EsBillSearchStore.java @@ -3,7 +3,11 @@ package com.bms.price.search; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpEntity; @@ -148,6 +152,36 @@ public class EsBillSearchStore implements IBillSearchStore @Override public BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize) + { + return search(logType, bizId, status, null, null, pageNum, pageSize); + } + + @Override + public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, + int pageNum, int pageSize) + { + return search(logType, bizId, status, psvId, priceKind, null, null, pageNum, pageSize); + } + + @Override + public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, + String productCode, String updateTime, int pageNum, int pageSize) + { + return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, null, pageNum, pageSize); + } + + @Override + public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, + String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize) + { + return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, updateTimePrefix, + null, pageNum, pageSize); + } + + @Override + public BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, + String productCode, String updateTime, String updateTimePrefix, String countryKeyword, + int pageNum, int pageSize) { BillSearchPage page = new BillSearchPage(); if (pageNum < 1) @@ -171,7 +205,8 @@ public class EsBillSearchStore implements IBillSearchStore sortItem.put("updatedAt.keyword", sortOpt); sort.add(sortItem); body.put("sort", sort); - body.put("query", buildQuery(logType, bizId, status)); + body.put("query", buildQuery(logType, bizId, status, psvId, priceKind, productCode, updateTime, + updateTimePrefix, countryKeyword)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity entity = new HttpEntity(body.toJSONString(), headers); @@ -217,6 +252,108 @@ public class EsBillSearchStore implements IBillSearchStore return page; } + @Override + public PriceChangeGroupPage searchPriceChangeGroups(Long psvId, String priceKind, String status, + int pageNum, int pageSize) + { + PriceChangeGroupPage page = new PriceChangeGroupPage(); + if (psvId == null) + { + return page; + } + if (pageNum < 1) + { + pageNum = 1; + } + if (pageSize < 1) + { + pageSize = 10; + } + List all = loadAllPriceChanges(psvId, priceKind, status, 10000); + Map grouped = new LinkedHashMap(); + for (BillSyncSnapshot snap : all) + { + if (snap == null) + { + continue; + } + JSONObject req = parseRequestObject(snap.getRequestJson()); + JSONObject keys = req == null ? null : req.getJSONObject("bizKeys"); + String productCode = keys == null ? "" : StringUtils.nvl(keys.getString("productCode"), ""); + String time = StringUtils.nvl(snap.getUpdateTime(), ""); + String key = productCode + "|" + time; + PriceChangeGroup group = grouped.get(key); + if (group == null) + { + group = new PriceChangeGroup(); + group.setProductCode(StringUtils.isEmpty(productCode) ? "-" : productCode); + group.setUpdateTime(time); + group.setAction(req == null ? null : req.getString("action")); + group.setOperator(req == null ? null : req.getString("operator")); + group.setStatus(snap.getStatus()); + group.setChangeCount(0); + grouped.put(key, group); + } + group.setChangeCount(group.getChangeCount() + 1); + if (EsBizLogSupport.STATUS_FAIL.equals(snap.getStatus())) + { + group.setStatus(EsBizLogSupport.STATUS_FAIL); + } + } + List list = new ArrayList(grouped.values()); + page.setTotal(list.size()); + int from = (pageNum - 1) * pageSize; + if (from >= list.size()) + { + return page; + } + int to = Math.min(from + pageSize, list.size()); + page.setRows(new ArrayList(list.subList(from, to))); + return page; + } + + private List loadAllPriceChanges(Long psvId, String priceKind, String status, int max) + { + List all = new ArrayList(); + int batch = 1000; + int pageNum = 1; + while (all.size() < max) + { + int size = Math.min(batch, max - all.size()); + BillSearchPage chunk = search(EsLogType.PRICE_CHANGE, null, status, psvId, priceKind, null, null, + pageNum, size); + List rows = chunk.getRows(); + if (rows == null || rows.isEmpty()) + { + break; + } + all.addAll(rows); + if (rows.size() < size || all.size() >= chunk.getTotal()) + { + break; + } + pageNum++; + } + return all; + } + + private JSONObject parseRequestObject(String requestJson) + { + if (StringUtils.isEmpty(requestJson)) + { + return null; + } + try + { + Object parsed = JSON.parse(requestJson); + return parsed instanceof JSONObject ? (JSONObject) parsed : null; + } + catch (Exception e) + { + return null; + } + } + private BillSyncSnapshot toSnapshot(JSONObject source, String id) { if (source == null) @@ -249,12 +386,19 @@ public class EsBillSearchStore implements IBillSearchStore return snap; } - private JSONObject buildQuery(String logType, String bizId, String status) + private JSONObject buildQuery(String logType, String bizId, String status, Long psvId, String priceKind, + String productCode, String updateTime, String updateTimePrefix, String countryKeyword) { JSONArray filters = new JSONArray(); addTerm(filters, "logType", logType); addWildcard(filters, "bizId", bizId); addTerm(filters, "status", status); + addPsvIdFilter(filters, psvId); + addTerm(filters, "request.priceKind", priceKind); + addProductCodeFilter(filters, productCode); + addCountryKeywordFilter(filters, countryKeyword); + addUpdateTimeFilter(filters, updateTime); + addUpdateTimePrefixFilter(filters, updateTimePrefix); if (filters.isEmpty()) { JSONObject matchAll = new JSONObject(); @@ -268,6 +412,195 @@ public class EsBillSearchStore implements IBillSearchStore return query; } + private void addProductCodeFilter(JSONArray filters, String productCode) + { + if (StringUtils.isEmpty(productCode)) + { + return; + } + String code = productCode.trim(); + JSONArray should = new JSONArray(); + JSONObject termKw = new JSONObject(); + termKw.put("request.bizKeys.productCode.keyword", code); + JSONObject wrapKw = new JSONObject(); + wrapKw.put("term", termKw); + should.add(wrapKw); + + JSONObject term = new JSONObject(); + term.put("request.bizKeys.productCode", code); + JSONObject wrap = new JSONObject(); + wrap.put("term", term); + should.add(wrap); + + JSONObject inner = new JSONObject(); + inner.put("should", should); + inner.put("minimum_should_match", Integer.valueOf(1)); + JSONObject boolWrap = new JSONObject(); + boolWrap.put("bool", inner); + filters.add(boolWrap); + } + + /** Match country code / name (exact or contains). */ + private void addCountryKeywordFilter(JSONArray filters, String countryKeyword) + { + if (StringUtils.isEmpty(countryKeyword)) + { + return; + } + String kw = countryKeyword.trim(); + String wild = "*" + escapeWildcard(kw) + "*"; + JSONArray should = new JSONArray(); + addShouldTerm(should, "request.bizKeys.countryCode.keyword", kw); + addShouldTerm(should, "request.bizKeys.countryCode", kw); + addShouldWildcard(should, "request.bizKeys.countryCode.keyword", wild); + addShouldWildcard(should, "request.bizKeys.countryCode", wild); + addShouldWildcard(should, "request.bizKeys.countryName.keyword", wild); + addShouldWildcard(should, "request.bizKeys.countryName", wild); + JSONObject matchName = new JSONObject(); + JSONObject matchField = new JSONObject(); + matchField.put("query", kw); + matchName.put("request.bizKeys.countryName", matchField); + JSONObject matchWrap = new JSONObject(); + matchWrap.put("match", matchName); + should.add(matchWrap); + + JSONObject inner = new JSONObject(); + inner.put("should", should); + inner.put("minimum_should_match", Integer.valueOf(1)); + JSONObject boolWrap = new JSONObject(); + boolWrap.put("bool", inner); + filters.add(boolWrap); + } + + private void addShouldTerm(JSONArray should, String field, String value) + { + JSONObject term = new JSONObject(); + term.put(field, value); + JSONObject wrap = new JSONObject(); + wrap.put("term", term); + should.add(wrap); + } + + private void addShouldWildcard(JSONArray should, String field, String value) + { + JSONObject wild = new JSONObject(); + wild.put(field, value); + JSONObject wrap = new JSONObject(); + wrap.put("wildcard", wild); + should.add(wrap); + } + + private static String escapeWildcard(String raw) + { + if (raw == null) + { + return ""; + } + return raw.replace("\\", "\\\\").replace("*", "\\*").replace("?", "\\?"); + } + + private void addUpdateTimeFilter(JSONArray filters, String updateTime) + { + if (StringUtils.isEmpty(updateTime)) + { + return; + } + String time = updateTime.trim(); + JSONArray should = new JSONArray(); + JSONObject t1 = new JSONObject(); + t1.put("updateTime.keyword", time); + JSONObject w1 = new JSONObject(); + w1.put("term", t1); + should.add(w1); + + JSONObject t2 = new JSONObject(); + t2.put("updatedAt.keyword", time); + JSONObject w2 = new JSONObject(); + w2.put("term", t2); + should.add(w2); + + JSONObject t3 = new JSONObject(); + t3.put("updateTime", time); + JSONObject w3 = new JSONObject(); + w3.put("term", t3); + should.add(w3); + + JSONObject t4 = new JSONObject(); + t4.put("updatedAt", time); + JSONObject w4 = new JSONObject(); + w4.put("term", t4); + should.add(w4); + + JSONObject inner = new JSONObject(); + inner.put("should", should); + inner.put("minimum_should_match", Integer.valueOf(1)); + JSONObject wrap = new JSONObject(); + wrap.put("bool", inner); + filters.add(wrap); + } + + /** Match write-time by minute prefix, e.g. 2026-11-05 13:27. */ + private void addUpdateTimePrefixFilter(JSONArray filters, String updateTimePrefix) + { + if (StringUtils.isEmpty(updateTimePrefix)) + { + return; + } + String prefix = escapeWildcard(updateTimePrefix.trim()) + "*"; + JSONArray should = new JSONArray(); + for (String field : new String[] { "updateTime.keyword", "updatedAt.keyword", "updateTime", "updatedAt" }) + { + JSONObject opts = new JSONObject(); + opts.put("value", prefix); + opts.put("case_insensitive", Boolean.TRUE); + JSONObject wildcard = new JSONObject(); + wildcard.put(field, opts); + JSONObject wrap = new JSONObject(); + wrap.put("wildcard", wildcard); + should.add(wrap); + } + JSONObject inner = new JSONObject(); + inner.put("should", should); + inner.put("minimum_should_match", Integer.valueOf(1)); + JSONObject wrap = new JSONObject(); + wrap.put("bool", inner); + filters.add(wrap); + } + + /** Prefer numeric term; also accept keyword/string mapping of bizKeys.psvId. */ + private void addPsvIdFilter(JSONArray filters, Long psvId) + { + if (psvId == null) + { + return; + } + JSONArray should = new JSONArray(); + JSONObject termNum = new JSONObject(); + termNum.put("request.bizKeys.psvId", psvId); + JSONObject wrapNum = new JSONObject(); + wrapNum.put("term", termNum); + should.add(wrapNum); + + JSONObject termKw = new JSONObject(); + termKw.put("request.bizKeys.psvId.keyword", String.valueOf(psvId)); + JSONObject wrapKw = new JSONObject(); + wrapKw.put("term", termKw); + should.add(wrapKw); + + JSONObject termStr = new JSONObject(); + termStr.put("request.bizKeys.psvId", String.valueOf(psvId)); + JSONObject wrapStr = new JSONObject(); + wrapStr.put("term", termStr); + should.add(wrapStr); + + JSONObject inner = new JSONObject(); + inner.put("should", should); + inner.put("minimum_should_match", Integer.valueOf(1)); + JSONObject wrap = new JSONObject(); + wrap.put("bool", inner); + filters.add(wrap); + } + private void addTerm(JSONArray filters, String field, String value) { if (StringUtils.isEmpty(value)) @@ -297,11 +630,6 @@ public class EsBillSearchStore implements IBillSearchStore filters.add(wrap); } - private String escapeWildcard(String value) - { - return value.replace("\\", "\\\\").replace("*", "\\*").replace("?", "\\?"); - } - private long readTotal(Object total) { if (total instanceof JSONObject) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/search/IBillSearchStore.java b/bms-back/bms-system/src/main/java/com/bms/price/search/IBillSearchStore.java index fc162d07..155e17bc 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/search/IBillSearchStore.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/search/IBillSearchStore.java @@ -17,4 +17,48 @@ public interface IBillSearchStore BillSyncSnapshot getByDocId(String docId); BillSearchPage search(String logType, String bizId, String status, int pageNum, int pageSize); + + /** + * Search with optional sales-version / priceKind filters (request.bizKeys.psvId, request.priceKind). + */ + default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, + int pageNum, int pageSize) + { + return search(logType, bizId, status, psvId, priceKind, null, null, pageNum, pageSize); + } + + /** + * Search with optional productCode + updateTime (exact) / updateTimePrefix (e.g. yyyy-MM-dd HH:mm) filters. + */ + default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, + String productCode, String updateTime, int pageNum, int pageSize) + { + return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, null, pageNum, pageSize); + } + + default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, + String productCode, String updateTime, String updateTimePrefix, int pageNum, int pageSize) + { + return search(logType, bizId, status, psvId, priceKind, productCode, updateTime, updateTimePrefix, + null, pageNum, pageSize); + } + + /** + * Search with optional countryKeyword (matches bizKeys.countryCode / countryName). + */ + default BillSearchPage search(String logType, String bizId, String status, Long psvId, String priceKind, + String productCode, String updateTime, String updateTimePrefix, String countryKeyword, + int pageNum, int pageSize) + { + return search(logType, bizId, status, pageNum, pageSize); + } + + /** + * Group PRICE_CHANGE logs by productCode + updateTime for one package version. + */ + default PriceChangeGroupPage searchPriceChangeGroups(Long psvId, String priceKind, String status, + int pageNum, int pageSize) + { + return new PriceChangeGroupPage(); + } } diff --git a/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeEsLog.java b/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeEsLog.java index 954cb482..744b7b77 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeEsLog.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeEsLog.java @@ -25,12 +25,33 @@ public final class PriceChangeEsLog public static final String ACTION_DELETE = "DELETE"; public static final String ACTION_IMPORT = "IMPORT"; + /** Async import workers have no SecurityContext; pass operName via this override. */ + private static final ThreadLocal OPERATOR_OVERRIDE = new ThreadLocal(); + private PriceChangeEsLog() { } + public static void beginOperator(String operName) + { + if (StringUtils.isNotEmpty(operName)) + { + OPERATOR_OVERRIDE.set(operName.trim()); + } + } + + public static void endOperator() + { + OPERATOR_OVERRIDE.remove(); + } + public static String operator() { + String override = OPERATOR_OVERRIDE.get(); + if (StringUtils.isNotEmpty(override)) + { + return override; + } try { String name = SecurityUtils.getUsername(); @@ -131,11 +152,31 @@ public final class PriceChangeEsLog keys.put("countryCode", d.getCountryCode()); keys.put("countryName", d.getCountryName()); keys.put("zoneCode", d.getZoneCode()); + keys.put("zoneName", d.getZoneName()); keys.put("weightMin", d.getWeightMin()); keys.put("weightMax", d.getWeightMax()); + keys.put("weightLabel", weightBandOf(d)); return keys; } + /** Display weight band for sales detail logs (e.g. 0<W≤0.5). */ + public static String weightBandOf(PrcPriceSetDetail d) + { + if (d == null) + { + return ""; + } + if (StringUtils.isNotEmpty(d.getWeightLabel())) + { + return d.getWeightLabel(); + } + if (d.getWeightMin() == null && d.getWeightMax() == null) + { + return ""; + } + return stringify(d.getWeightMin()) + " bizKeys, List> changes) { diff --git a/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeGroup.java b/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeGroup.java new file mode 100644 index 00000000..d059db8c --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeGroup.java @@ -0,0 +1,74 @@ +package com.bms.price.search; + +/** + * Price-change log group key: productCode + write time. + */ +public class PriceChangeGroup +{ + private String productCode; + private String updateTime; + private String action; + private String operator; + private String status; + private int changeCount; + + public String getProductCode() + { + return productCode; + } + + public void setProductCode(String productCode) + { + this.productCode = productCode; + } + + public String getUpdateTime() + { + return updateTime; + } + + public void setUpdateTime(String updateTime) + { + this.updateTime = updateTime; + } + + public String getAction() + { + return action; + } + + public void setAction(String action) + { + this.action = action; + } + + public String getOperator() + { + return operator; + } + + public void setOperator(String operator) + { + this.operator = operator; + } + + public String getStatus() + { + return status; + } + + public void setStatus(String status) + { + this.status = status; + } + + public int getChangeCount() + { + return changeCount; + } + + public void setChangeCount(int changeCount) + { + this.changeCount = changeCount; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeGroupPage.java b/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeGroupPage.java new file mode 100644 index 00000000..ce5159e1 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/search/PriceChangeGroupPage.java @@ -0,0 +1,31 @@ +package com.bms.price.search; + +import java.util.ArrayList; +import java.util.List; + +/** Paginated product+time groups of price-change logs. */ +public class PriceChangeGroupPage +{ + private long total; + private List rows = new ArrayList(); + + public long getTotal() + { + return total; + } + + public void setTotal(long total) + { + this.total = total; + } + + public List getRows() + { + return rows; + } + + public void setRows(List rows) + { + this.rows = rows == null ? new ArrayList() : rows; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java index b5db8a2a..f8139c2e 100644 --- a/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/IPrcOrderBillService.java @@ -159,7 +159,7 @@ public interface IPrcOrderBillService /** * Async local rebill by selected billIds or list filters. Returns taskId for progress poll. - * Large sets: only bill_id loaded, then processed in pages of 500 with parallel workers. + * Large sets: only bill_id loaded, then processed by continuous parallel workers. */ String submitLocalRebill(LocalRebillRequest request); diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirFeeService.java b/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirFeeService.java new file mode 100644 index 00000000..4c2128f7 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/ISrvBillAirFeeService.java @@ -0,0 +1,19 @@ +package com.bms.price.service; + +import java.util.List; +import com.bms.price.airbill.domain.SrvBillAirFee; +import com.bms.price.airbill.domain.SrvBillAirFeeLog; +import com.bms.price.airbill.domain.SrvBillAirFeeSave; + +/** + * 空运提单费用维护。 + */ +public interface ISrvBillAirFeeService +{ + List listFees(String billCode); + + List listLogs(String billCode); + + /** 只写入有变动的行,并按行记录修改前后。返回给页面的提示。 */ + String save(SrvBillAirFeeSave command); +} 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 5b38b12f..8614f22d 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 @@ -183,8 +183,8 @@ 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 + paged parallel). */ - private static final int LOCAL_REBILL_MAX = 100000; + /** Soft max for one local-rebill job (id-only load + continuous workers). */ + private static final int LOCAL_REBILL_MAX = 200000; /** Soft max for one order/billing list Excel export. */ private static final int ORDER_BILL_EXPORT_MAX = 50000; @@ -681,6 +681,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(), request.getCreditArrearsStatus(), request.getOrderStatuses(), request.getBillingFailStatuses(), request.getBeginBizDate(), request.getEndBizDate(), + request.getBeginPickupDate(), request.getEndPickupDate(), "price.bill.recalc.filterRequired", "price.bill.recalc.scopeRequired", "price.bill.recalc.selectedRequired"); if (bills.isEmpty()) @@ -1276,6 +1277,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService request.getCustomerCodes(), request.getProductCodes(), request.getCountryCode(), request.getCreditArrearsStatus(), request.getOrderStatuses(), request.getBillingFailStatuses(), request.getBeginBizDate(), request.getEndBizDate(), + request.getBeginPickupDate(), request.getEndPickupDate(), "price.bill.fee.batch.filterRequired", "price.bill.fee.batch.scopeRequired", "price.bill.fee.batch.selectedRequired"); } @@ -1290,6 +1292,7 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService List customerCodesRaw, List productCodesRaw, String countryCodeRaw, String creditArrearsStatusRaw, List orderStatusesRaw, List billingFailStatusesRaw, String beginBizDateRaw, String endBizDateRaw, + String beginPickupDateRaw, String endPickupDateRaw, String msgFilter, String msgScope, String msgSelected) { String scopeMode = StringUtils.nvl(scopeModeRaw, "").trim().toUpperCase(); @@ -1311,6 +1314,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService : countryCodeRaw.trim().toUpperCase(); String beginBizDate = StringUtils.isEmpty(beginBizDateRaw) ? null : beginBizDateRaw.trim(); String endBizDate = StringUtils.isEmpty(endBizDateRaw) ? null : endBizDateRaw.trim(); + String beginPickupDate = StringUtils.isEmpty(beginPickupDateRaw) ? null : beginPickupDateRaw.trim(); + String endPickupDate = StringUtils.isEmpty(endPickupDateRaw) ? null : endPickupDateRaw.trim(); boolean hasFilter = (orderNos != null && !orderNos.isEmpty()) || (trackingNos != null && !trackingNos.isEmpty()) || (transferNos != null && !transferNos.isEmpty()) @@ -1321,7 +1326,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService || StringUtils.isNotEmpty(creditArrearsStatus) || StringUtils.isNotEmpty(countryCode) || StringUtils.isNotEmpty(beginBizDate) - || StringUtils.isNotEmpty(endBizDate); + || StringUtils.isNotEmpty(endBizDate) + || StringUtils.isNotEmpty(beginPickupDate) + || StringUtils.isNotEmpty(endPickupDate); if (!hasFilter) { throw new ServiceException(MessageUtils.message(msgFilter)); @@ -1356,6 +1363,14 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService { query.getParams().put("endBizDate", endBizDate); } + if (beginPickupDate != null) + { + query.getParams().put("beginPickupDate", beginPickupDate); + } + if (endPickupDate != null) + { + query.getParams().put("endPickupDate", endPickupDate); + } } List list = self.selectBillList(query); return list == null ? new ArrayList() : list; @@ -2754,6 +2769,10 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService : request.getBeginBizDate().trim(); String endBizDate = StringUtils.isEmpty(request.getEndBizDate()) ? null : request.getEndBizDate().trim(); + String beginPickupDate = StringUtils.isEmpty(request.getBeginPickupDate()) ? null + : request.getBeginPickupDate().trim(); + String endPickupDate = StringUtils.isEmpty(request.getEndPickupDate()) ? null + : request.getEndPickupDate().trim(); boolean hasFilter = (orderNos != null && !orderNos.isEmpty()) || (trackingNos != null && !trackingNos.isEmpty()) || (transferNos != null && !transferNos.isEmpty()) @@ -2762,7 +2781,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService || (productCodes != null && !productCodes.isEmpty()) || StringUtils.isNotEmpty(creditArrearsStatus) || StringUtils.isNotEmpty(beginBizDate) - || StringUtils.isNotEmpty(endBizDate); + || StringUtils.isNotEmpty(endBizDate) + || StringUtils.isNotEmpty(beginPickupDate) + || StringUtils.isNotEmpty(endPickupDate); if (!hasFilter) { throw new ServiceException("请先设置列表筛选条件"); @@ -2784,6 +2805,14 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService { query.getParams().put("endBizDate", endBizDate); } + if (beginPickupDate != null) + { + query.getParams().put("beginPickupDate", beginPickupDate); + } + if (endPickupDate != null) + { + query.getParams().put("endPickupDate", endPickupDate); + } List list = self.selectBillList(query); if (list == null || list.isEmpty()) { 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 041cf63a..6c05e563 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 @@ -53,6 +53,11 @@ import com.bms.price.search.PriceChangeEsLog; import com.bms.price.service.IPrcPriceExcelFileService; import com.bms.price.service.IPrcPriceSetService; import com.bms.price.task.PrcPriceSetSaveAsyncRunner; +import com.bms.customer.customerinfo.domain.BmsCustomer; +import com.bms.customer.customerinfo.domain.BmsCustomerPriceSet; +import com.bms.customer.customerinfo.domain.BmsPriceSetOption; +import com.bms.customer.customerinfo.service.IBmsCustomerLogService; +import com.bms.customer.customerinfo.service.IBmsCustomerService; /** * 价格套餐 服务层实现 @@ -130,6 +135,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService @Autowired private IBillSearchStore billSearchStore; + @Autowired + private IBmsCustomerLogService customerLogService; + + @Autowired + private IBmsCustomerService bmsCustomerService; + @Override public PrcPriceSet selectPrcPriceSetById(Long priceSetId) { @@ -1582,6 +1593,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService @Transactional(rollbackFor = Exception.class) 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(). + PriceChangeEsLog.beginOperator(operName); + try + { + return importDetailPricesInOperator(priceSetId, psvId, bytes, filename, operName, taskId, updateOnly, + replaceByProductCountry, countryCode); + } + finally + { + PriceChangeEsLog.endOperator(); + } + } + + private String importDetailPricesInOperator(Long priceSetId, Long psvId, byte[] bytes, String filename, + String operName, String taskId, boolean updateOnly, boolean replaceByProductCountry, String countryCode) + throws Exception { PrcPriceSet db = requireSet(priceSetId); assertChildEditable(db); @@ -2494,7 +2522,12 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } try { - return customerMapper.insertPrcCustomerPriceSet(customer); + int rows = customerMapper.insertPrcCustomerPriceSet(customer); + if (rows > 0) + { + logCustomerPriceSetBind(null, customer, db); + } + return rows; } catch (Exception e) { @@ -2593,6 +2626,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } validateCustomer(row, db); customerMapper.updatePrcCustomerPriceSet(row); + logCustomerPriceSetBind(existing, row, db); replaced++; } else @@ -2613,6 +2647,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } throw e; } + logCustomerPriceSetBind(null, row, db); inserted++; } } @@ -2640,12 +2675,30 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService assertChildEditable(db); customer.setPriceSetId(old.getPriceSetId()); validateCustomer(customer, db); - return customerMapper.updatePrcCustomerPriceSet(customer); + int rows = customerMapper.updatePrcCustomerPriceSet(customer); + if (rows > 0) + { + if (customer.getCustomerId() == null) + { + customer.setCustomerId(old.getCustomerId()); + } + if (StringUtils.isEmpty(customer.getCustomerCode())) + { + customer.setCustomerCode(old.getCustomerCode()); + } + if (StringUtils.isEmpty(customer.getCustomerName())) + { + customer.setCustomerName(old.getCustomerName()); + } + logCustomerPriceSetBind(old, customer, db); + } + return rows; } @Override public int deleteCustomerByIds(Long[] customerPricesetIds) { + List snapshots = new ArrayList(); for (Long id : customerPricesetIds) { PrcCustomerPriceSet old = customerMapper.selectPrcCustomerPriceSetById(id); @@ -2658,8 +2711,17 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { throw new ServiceException(MessageUtils.message("price.set.customer.unbindAudit")); } + snapshots.add(old); } - return customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds); + int rows = customerMapper.deletePrcCustomerPriceSetByIds(customerPricesetIds); + if (rows > 0) + { + for (PrcCustomerPriceSet snapshot : snapshots) + { + logCustomerPriceSetUnbind(snapshot); + } + } + return rows; } private void replaceChildren(PrcPriceSet set) @@ -2730,6 +2792,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService c.setUpdateBy(set.getUpdateBy()); customerMapper.updatePrcCustomerPriceSet(c); keepIds.add(hit.getCustomerPricesetId()); + logCustomerPriceSetBind(hit, c, set); } else { @@ -2740,6 +2803,7 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { keepIds.add(c.getCustomerPricesetId()); } + logCustomerPriceSetBind(null, c, set); } } catch (Exception e) @@ -2761,10 +2825,99 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService } if (!orphanIds.isEmpty()) { + for (Long orphanId : orphanIds) + { + PrcCustomerPriceSet snapshot = byId.get(orphanId); + if (snapshot != null) + { + logCustomerPriceSetUnbind(snapshot); + } + } customerMapper.deletePrcCustomerPriceSetByIds(orphanIds.toArray(new Long[0])); } } + /** 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) + { + return; + } + BmsCustomer customer = resolveCustomerForLog(bind); + if (customer == null && bind.getCustomerId() != null) + { + customer = new BmsCustomer(); + customer.setCustomerId(bind.getCustomerId()); + customer.setCustomerCode(bind.getCustomerCode()); + customer.setCustomerName(bind.getCustomerName()); + } + BmsCustomerPriceSet existingLog = toBmsCustomerPriceSet(existing, resolvePriceSetName(existing)); + BmsCustomerPriceSet bindLog = toBmsCustomerPriceSet(bind, set.getPriceSetName()); + if (bindLog.getCustomerId() == null && customer != null) + { + bindLog.setCustomerId(customer.getCustomerId()); + } + BmsPriceSetOption option = new BmsPriceSetOption(); + option.setPriceSetId(set.getPriceSetId()); + option.setPriceSetCode(set.getPriceSetCode()); + option.setPriceSetName(set.getPriceSetName()); + customerLogService.recordPriceSetBind(customer, existingLog, bindLog, option); + } + + private void logCustomerPriceSetUnbind(PrcCustomerPriceSet bind) + { + if (customerLogService == null || bind == null) + { + return; + } + BmsCustomerPriceSet snapshot = toBmsCustomerPriceSet(bind, resolvePriceSetName(bind)); + customerLogService.recordPriceSetUnbind(snapshot); + } + + private BmsCustomer resolveCustomerForLog(PrcCustomerPriceSet bind) + { + if (bind == null || bmsCustomerService == null) + { + return null; + } + if (bind.getCustomerId() != null) + { + return bmsCustomerService.selectBmsCustomerByCustomerId(bind.getCustomerId()); + } + return null; + } + + private String resolvePriceSetName(PrcCustomerPriceSet bind) + { + if (bind == null || bind.getPriceSetId() == null) + { + return null; + } + PrcPriceSet set = setMapper.selectPrcPriceSetById(bind.getPriceSetId()); + return set == null ? null : set.getPriceSetName(); + } + + private BmsCustomerPriceSet toBmsCustomerPriceSet(PrcCustomerPriceSet src, String priceSetName) + { + if (src == null) + { + return null; + } + BmsCustomerPriceSet dest = new BmsCustomerPriceSet(); + dest.setCustomerPricesetId(src.getCustomerPricesetId()); + dest.setPriceSetId(src.getPriceSetId()); + dest.setCustomerId(src.getCustomerId()); + dest.setCustomerCode(src.getCustomerCode()); + dest.setCustomerName(src.getCustomerName()); + dest.setBindPriority(src.getBindPriority()); + dest.setPrBegindate(src.getPrBegindate()); + dest.setPrEnddate(src.getPrEnddate()); + dest.setPriceSetNote(src.getPriceSetNote()); + dest.setPriceSetName(priceSetName); + return dest; + } + private void reportSaveProgress(PrcPriceSet set, int percent, String message) { if (set == null || StringUtils.isEmpty(set.getSaveTaskId()) || saveAsyncRunner == null) @@ -3465,6 +3618,22 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { fillVersionChildren(version); } + // Soft-deleted EFFECT versions for the "deleted" tab (status may still be EFFECT) + PrcPriceSetVersion dq = new PrcPriceSetVersion(); + dq.setPriceSetId(priceSetId); + dq.setPriceType(com.bms.price.domain.PriceType.SALES); + dq.setIsDel("1"); + List deleted = versionMapper.selectPrcPriceSetVersionList(dq); + if (deleted != null) + { + for (PrcPriceSetVersion version : deleted) + { + version.setProducts(new ArrayList()); + version.setDetails(new ArrayList()); + version.setDetailCount(Integer.valueOf(detailMapper.countByPsvIdAll(version.getPsvId()))); + versions.add(version); + } + } set.setVersions(versions); PrcPriceSetVersion current = pickCurrentVersion(versions); set.setVersion(current); @@ -3491,14 +3660,23 @@ public class PrcPriceSetServiceImpl implements IPrcPriceSetService { return null; } + PrcPriceSetVersion firstAlive = null; for (PrcPriceSetVersion v : versions) { + if (v == null || "1".equals(v.getIsDel())) + { + continue; + } + if (firstAlive == null) + { + firstAlive = v; + } if (VER_EFFECT.equals(v.getStatus())) { return v; } } - return versions.get(0); + return firstAlive; } private void saveOrUpdateVersion(PrcPriceSet set) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirFeeServiceImpl.java b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirFeeServiceImpl.java new file mode 100644 index 00000000..9cc46dca --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/impl/SrvBillAirFeeServiceImpl.java @@ -0,0 +1,382 @@ +package com.bms.price.service.impl; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import com.alibaba.fastjson2.JSON; +import com.bms.common.core.domain.entity.SysDictData; +import com.bms.common.core.domain.entity.SysUser; +import com.bms.common.exception.ServiceException; +import com.bms.common.utils.DateUtils; +import com.bms.common.utils.PageUtils; +import com.bms.common.utils.SecurityUtils; +import com.bms.price.airbill.domain.SrvBillAirFee; +import com.bms.price.airbill.domain.SrvBillAirFeeLog; +import com.bms.price.airbill.domain.SrvBillAirFeeSave; +import com.bms.price.mapper.SrvBillAirFeeMapper; +import com.bms.price.mapper.SrvBillAirMapper; +import com.bms.price.service.ISrvBillAirFeeService; +import com.bms.system.service.ISysDictTypeService; + +/** + * 提单费用只处理本次新增、修改、删除的行。没有变化的费用不更新。 + */ +@Service +public class SrvBillAirFeeServiceImpl implements ISrvBillAirFeeService +{ + private static final String FEE_DICT = "bms_fee_type"; + + private static final String CURRENCY_DICT = "bms_currency"; + + @Autowired + private SrvBillAirFeeMapper feeMapper; + + @Autowired + private SrvBillAirMapper billMapper; + + @Autowired + private ISysDictTypeService dictTypeService; + + @Override + public List listFees(String billCode) + { + requireBill(billCode); + return feeMapper.selectByBillCode(billCode.trim()); + } + + @Override + public List listLogs(String billCode) + { + requireBill(billCode); + PageUtils.startPage(); + List logs = feeMapper.selectLogs(billCode.trim()); + for (SrvBillAirFeeLog log : logs) + { + if (log.getChangeContent() != null && !log.getChangeContent().isBlank()) + { + log.setChanges(JSON.parseArray(log.getChangeContent(), SrvBillAirFeeLog.FieldChange.class)); + } + } + return logs; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String save(SrvBillAirFeeSave command) + { + if (command == null) + { + throw new ServiceException("请提交要保存的费用"); + } + String billCode = requireBill(command.getBillCode()); + List inserts = command.getInserts() == null ? List.of() : command.getInserts(); + List updates = command.getUpdates() == null ? List.of() : command.getUpdates(); + List deletes = command.getDeletes() == null ? List.of() : command.getDeletes(); + if (inserts.isEmpty() && updates.isEmpty() && deletes.isEmpty()) + { + throw new ServiceException("没有需要保存的变更"); + } + Map feeLabels = dictLabels(FEE_DICT, "费用类型"); + Map currencyLabels = dictLabels(CURRENCY_DICT, "币种"); + Operator operator = operator(); + Date now = DateUtils.getNowDate(); + Set seen = new HashSet<>(); + int inserted = 0; + int updated = 0; + int removed = 0; + for (Long id : deletes) + { + if (id == null) + { + throw new ServiceException("要删除的费用缺少编号"); + } + if (!seen.add(id)) + { + throw new ServiceException("同一条费用不能重复提交"); + } + SrvBillAirFee current = liveFee(id, billCode); + if (current == null) + { + throw new ServiceException("费用已删除或不属于该提单"); + } + int rows = feeMapper.deleteFee(current.getId(), billCode, operator.account, now); + if (rows > 0) + { + writeLog(billCode, id, SrvBillAirFeeLog.DELETE, current, null, feeLabels, currencyLabels, operator, now); + removed++; + } + } + for (SrvBillAirFee incoming : updates) + { + if (incoming == null || incoming.getId() == null) + { + throw new ServiceException("要修改的费用缺少编号"); + } + if (!seen.add(incoming.getId())) + { + throw new ServiceException("同一条费用不能重复提交"); + } + SrvBillAirFee current = liveFee(incoming.getId(), billCode); + if (current == null) + { + throw new ServiceException("费用已删除或不属于该提单"); + } + SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels); + List changes = diff(current, next, feeLabels, currencyLabels); + if (changes.isEmpty()) + { + continue; + } + next.setId(current.getId()); + next.setBillCode(billCode); + next.setUpdateBy(operator.account); + next.setUpdateTime(now); + if (feeMapper.updateFee(next) > 0) + { + writeLog(billCode, current.getId(), SrvBillAirFeeLog.UPDATE, current, next, feeLabels, currencyLabels, operator, now); + updated++; + } + } + for (SrvBillAirFee incoming : inserts) + { + if (incoming == null || blank(incoming.getFeeType()) && incoming.getPrice() == null && blank(incoming.getCurrency()) && blank(incoming.getRemark())) + { + continue; + } + SrvBillAirFee next = normalize(incoming, feeLabels, currencyLabels); + next.setId(null); + next.setBillCode(billCode); + next.setCreateBy(operator.account); + next.setCreateTime(now); + next.setUpdateBy(operator.account); + next.setUpdateTime(now); + feeMapper.insertFee(next); + writeLog(billCode, next.getId(), SrvBillAirFeeLog.INSERT, null, next, feeLabels, currencyLabels, operator, now); + inserted++; + } + if (inserted + updated + removed == 0) + { + throw new ServiceException("没有需要保存的变更"); + } + return "已保存:新增 " + inserted + " 条,修改 " + updated + " 条,删除 " + removed + " 条"; + } + + private SrvBillAirFee liveFee(Long id, String billCode) + { + return feeMapper.selectLive(id, billCode); + } + + private SrvBillAirFee normalize(SrvBillAirFee raw, Map feeLabels, Map currencyLabels) + { + String feeType = trim(raw.getFeeType()); + String currency = trim(raw.getCurrency()); + String remark = trim(raw.getRemark()); + if (feeType.isEmpty()) + { + throw new ServiceException("请选择费用项"); + } + if (feeType.length() > 32 || !feeLabels.containsKey(feeType)) + { + throw new ServiceException("费用项不在费用类型字典中"); + } + if (raw.getPrice() == null) + { + throw new ServiceException("请填写价格"); + } + if (raw.getPrice().signum() < 0) + { + throw new ServiceException("价格不能小于0"); + } + if (raw.getPrice().precision() - raw.getPrice().scale() > 16) + { + throw new ServiceException("价格整数部分不能超过16位"); + } + if (currency.isEmpty()) + { + throw new ServiceException("请选择收费币种"); + } + if (currency.length() > 16 || !currencyLabels.containsKey(currency)) + { + throw new ServiceException("收费币种不在币种字典中"); + } + if (remark.length() > 500) + { + throw new ServiceException("备注不能超过500字"); + } + SrvBillAirFee fee = new SrvBillAirFee(); + fee.setFeeType(feeType); + fee.setPrice(raw.getPrice()); + fee.setCurrency(currency); + fee.setRemark(remark); + return fee; + } + + private void writeLog(String billCode, Long feeId, String operation, SrvBillAirFee before, SrvBillAirFee after, + Map feeLabels, Map currencyLabels, Operator operator, Date now) + { + List changes = diff(before, after, feeLabels, currencyLabels); + SrvBillAirFeeLog log = new SrvBillAirFeeLog(); + log.setBillCode(billCode); + log.setFeeId(feeId); + log.setOperationType(operation); + log.setSummary(cut(summary(operation, changes), 1000)); + log.setChangeContent(JSON.toJSONString(changes)); + log.setCreateBy(operator.account); + log.setCreateByName(operator.name); + log.setCreateTime(now); + feeMapper.insertLog(log); + } + + private static List diff(SrvBillAirFee before, SrvBillAirFee after, + Map feeLabels, Map currencyLabels) + { + List changes = new ArrayList<>(); + addChange(changes, "feeType", "费用类型", label(feeLabels, value(before, SrvBillAirFee::getFeeType)), + label(feeLabels, value(after, SrvBillAirFee::getFeeType))); + addChange(changes, "price", "价格", priceText(before), priceText(after)); + addChange(changes, "currency", "收费币种", label(currencyLabels, value(before, SrvBillAirFee::getCurrency)), + label(currencyLabels, value(after, SrvBillAirFee::getCurrency))); + addChange(changes, "remark", "备注", text(before == null ? null : before.getRemark()), text(after == null ? null : after.getRemark())); + return changes; + } + + private static void addChange(List changes, String field, String fieldLabel, String oldValue, String newValue) + { + String left = oldValue == null ? "" : oldValue; + String right = newValue == null ? "" : newValue; + if (left.equals(right)) + { + return; + } + changes.add(new SrvBillAirFeeLog.FieldChange(field, fieldLabel, left, right)); + } + + private static String summary(String operation, List changes) + { + String action = SrvBillAirFeeLog.INSERT.equals(operation) ? "新增费用" + : SrvBillAirFeeLog.DELETE.equals(operation) ? "删除费用" : "修改费用"; + StringBuilder text = new StringBuilder(action); + for (SrvBillAirFeeLog.FieldChange change : changes) + { + text.append(";").append(change.getFieldLabel()).append(":") + .append(show(change.getOldValue())).append(" → ").append(show(change.getNewValue())); + } + return text.toString(); + } + + private Map dictLabels(String dictType, String name) + { + List rows = dictTypeService.selectDictDataByType(dictType); + Map labels = new LinkedHashMap<>(); + if (rows != null) + { + for (SysDictData row : rows) + { + if (row != null && row.getDictValue() != null && !row.getDictValue().isBlank()) + { + labels.put(row.getDictValue().trim(), row.getDictLabel() == null ? row.getDictValue().trim() : row.getDictLabel().trim()); + } + } + } + if (labels.isEmpty()) + { + throw new ServiceException(name + "字典还没有数据"); + } + return labels; + } + + private String requireBill(String billCode) + { + if (billCode == null || billCode.isBlank()) + { + throw new ServiceException("提单号不能为空"); + } + String code = billCode.trim(); + if (billMapper.selectSrvBillAirByBillCode(code) == null) + { + throw new ServiceException("提单不存在"); + } + return code; + } + + private static Operator operator() + { + SysUser user = SecurityUtils.getLoginUser().getUser(); + String account = user.getUserName() == null ? "" : user.getUserName(); + String name = user.getNickName() == null || user.getNickName().isBlank() ? account : user.getNickName(); + return new Operator(account, name); + } + + private static String value(SrvBillAirFee fee, java.util.function.Function getter) + { + return fee == null ? "" : text(getter.apply(fee)); + } + + private static String priceText(SrvBillAirFee fee) + { + if (fee == null || fee.getPrice() == null) + { + return ""; + } + return fee.getPrice().stripTrailingZeros().toPlainString(); + } + + private static String label(Map labels, String value) + { + if (value == null || value.isBlank()) + { + return ""; + } + return labels.getOrDefault(value, value); + } + + private static String show(String value) + { + return value == null || value.isBlank() ? "空" : value; + } + + private static String text(String value) + { + return value == null ? "" : value.trim(); + } + + private static String trim(String value) + { + return value == null ? "" : value.trim(); + } + + private static boolean blank(String value) + { + return value == null || value.trim().isEmpty(); + } + + private static String cut(String value, int max) + { + if (value == null || value.length() <= max) + { + return value; + } + return value.substring(0, max); + } + + private static final class Operator + { + private final String account; + + private final String name; + + private Operator(String account, String name) + { + this.account = account; + this.name = name; + } + } +} 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 4ca52106..c2905a14 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 @@ -36,8 +36,7 @@ import com.bms.price.service.IPrcOrderBillService; /** * Async local rebill from prc_order_bill only (no Oracle / MQ). - * Large jobs page by {@link #PAGE_SIZE}; each page runs in a short-lived pool. - * Cancel is cooperative: current in-flight bills finish, remaining bills are skipped. + * Continuous worker pull (no page barrier); cancel skips remaining billIds. */ @Component public class PrcOrderBillLocalRebillAsyncRunner @@ -46,13 +45,11 @@ public class PrcOrderBillLocalRebillAsyncRunner public static final String CACHE_KEY_PREFIX = "price:billing:localRebill:task:"; public static final int TTL_MINUTES = 120; - /** Bill this many, wait for page done, then next page. */ - public static final int PAGE_SIZE = 500; public static final int MIN_THREADS = 2; - public static final int MAX_THREADS = 16; + public static final int MAX_THREADS = 24; private static final int FAIL_DETAIL_MAX = 200; - /** Publish Redis progress at most this often while a page is running. */ - private static final int PROGRESS_EVERY_N = 20; + /** Publish Redis progress at most this often while workers run. */ + private static final int PROGRESS_EVERY_N = 50; private static final long PROGRESS_MIN_INTERVAL_MS = 500L; private final ConcurrentHashMap cancelFlags = new ConcurrentHashMap(); @@ -86,10 +83,12 @@ public class PrcOrderBillLocalRebillAsyncRunner AtomicInteger success = new AtomicInteger(0); AtomicInteger fail = new AtomicInteger(0); AtomicInteger done = new AtomicInteger(0); + AtomicInteger nextIndex = new AtomicInteger(0); AtomicLong lastProgressAt = new AtomicLong(0L); StringBuilder failTips = new StringBuilder(); List> failDetails = Collections.synchronizedList(new ArrayList>()); - int threads = clampThreads(billingProperties == null ? null : billingProperties.getOrderSourceSyncThreads()); + int configured = billingProperties == null ? 16 : billingProperties.getLocalRebillThreads(); + int threads = clampThreads(Integer.valueOf(configured)); cancelFlags.computeIfAbsent(taskId, k -> new AtomicBoolean(false)); ExecutorService pool = newWorkerPool(threads, "local-rebill"); try @@ -107,30 +106,24 @@ public class PrcOrderBillLocalRebillAsyncRunner finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails); return; } - boolean cancelled = false; - for (int from = 0; from < total; from += PAGE_SIZE) + // Continuous pull: N workers grab next billId — no page barrier idle wait. + final List ids = billIds; + List> workers = new ArrayList>(threads); + for (int w = 0; w < threads; w++) { - if (isCancelRequested(taskId)) - { - cancelled = true; - break; - } - int to = Math.min(from + PAGE_SIZE, total); - List page = billIds.subList(from, to); - List> jobs = new ArrayList>(page.size()); - for (String billId : page) - { - final String one = billId; - jobs.add(() -> { - if (isCancelRequested(taskId)) + workers.add(() -> { + while (!isCancelRequested(taskId)) + { + int i = nextIndex.getAndIncrement(); + if (i >= total) { - return Boolean.FALSE; + return Boolean.TRUE; } + String one = ids.get(i); try { orderBillService.rebillLocalByBillId(one); success.incrementAndGet(); - return Boolean.TRUE; } catch (Exception e) { @@ -139,26 +132,20 @@ public class PrcOrderBillLocalRebillAsyncRunner collectFailDetail(failDetails, one, e); log.warn("local rebill failed taskId={} billId={} msg={}", taskId, one, e.getMessage()); - return Boolean.FALSE; } finally { int finished = done.incrementAndGet(); - maybePublishProgress(taskId, total, finished, success.get(), fail.get(), lastProgressAt); + maybePublishProgress(taskId, total, finished, success.get(), fail.get(), + lastProgressAt); } - }); - } - invokeAllQuiet(pool, jobs); - if (isCancelRequested(taskId)) - { - cancelled = true; - break; - } - // Page boundary: always flush latest counts (even if throttle skipped mid-page). - publishProgress(taskId, total, done.get(), success.get(), fail.get()); - lastProgressAt.set(System.currentTimeMillis()); + } + return Boolean.FALSE; + }); } - if (cancelled || isCancelRequested(taskId)) + invokeAllQuiet(pool, workers); + publishProgress(taskId, total, done.get(), success.get(), fail.get()); + if (isCancelRequested(taskId)) { finishCancelled(taskId, request, total, success.get(), fail.get(), threads, failDetails); return; @@ -170,8 +157,8 @@ public class PrcOrderBillLocalRebillAsyncRunner String doneStatus = fail.get() > 0 ? EsBizLogSupport.STATUS_FAIL : EsBizLogSupport.STATUS_SUCCESS; String doneErr = fail.get() > 0 ? ("fail=" + fail.get() + ", ok=" + success.get()) : null; saveDoneLog(taskId, request, total, success.get(), fail.get(), threads, failDetails, doneStatus, doneErr); - log.info("local rebill done taskId={} total={} success={} fail={} threads={} pageSize={}", - taskId, total, success.get(), fail.get(), threads, PAGE_SIZE); + log.info("local rebill done taskId={} total={} success={} fail={} threads={}", + taskId, total, success.get(), fail.get(), threads); } catch (Exception e) { @@ -255,7 +242,6 @@ public class PrcOrderBillLocalRebillAsyncRunner resp.put("fail", Integer.valueOf(fail)); resp.put("success", Integer.valueOf(ok)); resp.put("threads", Integer.valueOf(threads)); - resp.put("pageSize", Integer.valueOf(PAGE_SIZE)); if (failDetails != null && !failDetails.isEmpty()) { resp.put("failDetails", new ArrayList>(failDetails)); diff --git a/bms-back/bms-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml index 9f8e7b6c..33d3e605 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/OracleUpstreamOrderMapper.xml @@ -66,14 +66,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" AND o.PICKUP_DATE <= TO_DATE(#{query.endTime}, 'YYYY-MM-DD HH24:MI:SS') - - - AND o.BL_PICKUP = #{query.blPickup} - - - - - + AND o.BL_PICKUP = 1 AND o.ORDER_STATUS IN @@ -281,14 +274,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{bid} - - - AND o.BL_PICKUP = #{blPickup} - - - - - + AND o.BL_PICKUP = 1 AND o.ORDER_STATUS IN (3, 4, 5, 6, 7, 8, 11) AND o.CUSTOMER_CODE IN diff --git a/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml index ca58274a..2726b699 100644 --- a/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml +++ b/bms-back/bms-system/src/main/resources/mapper/price/PrcPriceSetDetailMapper.xml @@ -158,6 +158,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" select count(1) from prc_price_set_detail where psv_id = #{psvId} and ifnull(is_del, '0') = '0' + + - and ifnull(is_del, '0') = '0' + + + and ifnull(is_del, '0') = #{isDel} + + + and ifnull(is_del, '0') = '0' + + AND price_set_id = #{priceSetId} diff --git a/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirFeeMapper.xml b/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirFeeMapper.xml new file mode 100644 index 00000000..86a98d92 --- /dev/null +++ b/bms-back/bms-system/src/main/resources/mapper/price/SrvBillAirFeeMapper.xml @@ -0,0 +1,76 @@ + + + + + + + + + + insert into srv_bill_air_fee ( + bill_code, fee_type, price, currency, remark, + create_by, create_time, update_by, update_time, del_flag + ) values ( + #{billCode}, #{feeType}, #{price}, #{currency}, #{remark}, + #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, '0' + ) + + + + update srv_bill_air_fee + set fee_type = #{feeType}, + price = #{price}, + currency = #{currency}, + remark = #{remark}, + update_by = #{updateBy}, + update_time = #{updateTime} + where id = #{id} + and bill_code = #{billCode} + and del_flag = '0' + + + + update srv_bill_air_fee + set del_flag = '1', + update_by = #{updateBy}, + update_time = #{updateTime} + where id = #{id} + and bill_code = #{billCode} + and del_flag = '0' + + + + insert into srv_bill_air_fee_log ( + bill_code, fee_id, operation_type, summary, change_content, + create_by, create_by_name, create_time + ) values ( + #{billCode}, #{feeId}, #{operationType}, #{summary}, #{changeContent}, + #{createBy}, #{createByName}, #{createTime} + ) + + + + diff --git a/bms-back/sql/monitor/price_log_menu.sql b/bms-back/sql/monitor/price_log_menu.sql index 596e3056..8ce3a43e 100644 --- a/bms-back/sql/monitor/price_log_menu.sql +++ b/bms-back/sql/monitor/price_log_menu.sql @@ -1,4 +1,4 @@ --- Price change log under system monitor +-- Price change log: remove monitor page menu; keep query permission for package version change log SET NAMES utf8mb4; SET @monitorParent := ( @@ -8,29 +8,36 @@ SET @monitorParent := ( LIMIT 1 ); -INSERT INTO sys_menu ( - menu_id, menu_name, parent_id, order_num, path, component, query, route_name, - is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark -) -SELECT 2350, N'价格日志', IFNULL(@monitorParent, 2), 10, - 'priceLog', 'monitor/priceLog/index', '', 'PriceLog', - 1, 0, 'C', '0', '0', 'monitor:priceLog:list', 'log', 'admin', sysdate(), - N'销售价/产品价/红线价改前改后(ES)' - FROM DUAL - WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2350 OR perms = 'monitor:priceLog:list'); +-- Drop legacy monitor page menu +DELETE FROM sys_role_menu WHERE menu_id = 2350; +DELETE FROM sys_menu WHERE menu_id = 2350 + OR (menu_type = 'C' AND (path = 'priceLog' OR component = 'monitor/priceLog/index')); + +-- Normalize existing permission row to button (F), no page component +UPDATE sys_menu + SET parent_id = IFNULL(@monitorParent, parent_id), + menu_type = 'F', + path = '', + component = NULL, + visible = '0', + status = '0', + menu_name = N'价格变更日志查询', + remark = N'套餐版本变更记录等查询权限(无监控页面)' + WHERE menu_id = 2351 + OR perms = 'monitor:priceLog:list'; INSERT INTO sys_menu ( menu_id, menu_name, parent_id, order_num, path, component, - is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time + is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark ) -SELECT 2351, N'查询', 2350, 1, '', NULL, - 1, 0, 'F', '0', '0', 'monitor:priceLog:list', '#', 'admin', sysdate() +SELECT 2351, N'价格变更日志查询', IFNULL(@monitorParent, 2), 10, '', NULL, + 1, 0, 'F', '0', '0', 'monitor:priceLog:list', '#', 'admin', sysdate(), + N'套餐版本变更记录等查询权限(无监控页面)' FROM DUAL - WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2351); + WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2351 OR perms = 'monitor:priceLog:list'); INSERT INTO sys_role_menu (role_id, menu_id) SELECT 1, m.menu_id - FROM (SELECT 2350 AS menu_id UNION SELECT 2351) m - WHERE NOT EXISTS ( - SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id - ); + FROM sys_menu m + WHERE m.perms = 'monitor:priceLog:list' + AND NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); diff --git a/bms-back/sql/price/air_bill_menu.sql b/bms-back/sql/price/air_bill_menu.sql index 5e24971b..8ff30fa4 100644 --- a/bms-back/sql/price/air_bill_menu.sql +++ b/bms-back/sql/price/air_bill_menu.sql @@ -14,6 +14,27 @@ INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, SELECT 2522, N'详情', 2520, 2, '', NULL, 1, 0, 'F', '0', '0', 'price:airBill:query', '#', 'admin', sysdate(), N'提单详情' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2522 OR perms = 'price:airBill:query'); +INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark) +SELECT 2529, N'费用', 2520, 3, '', NULL, 1, 0, 'F', '0', '0', 'price:airBill:fee', '#', 'admin', sysdate(), N'维护空运提单费用' +WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2529 OR perms = 'price:airBill:fee'); + INSERT INTO sys_role_menu (role_id, menu_id) SELECT 1, m.menu_id FROM (SELECT 2520 AS menu_id UNION SELECT 2521 UNION SELECT 2522) m WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = m.menu_id); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, fee.menu_id +FROM sys_menu fee +WHERE fee.perms = 'price:airBill:fee' + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu rm WHERE rm.role_id = 1 AND rm.menu_id = fee.menu_id + ); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT rm.role_id, fee.menu_id +FROM sys_role_menu rm +JOIN sys_menu fee ON fee.perms = 'price:airBill:fee' +WHERE rm.menu_id IN (2520, 2522) + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = fee.menu_id + ); diff --git a/bms-back/sql/price/pay_bill_segment_amount.sql b/bms-back/sql/price/pay_bill_segment_amount.sql new file mode 100644 index 00000000..f577d3ac --- /dev/null +++ b/bms-back/sql/price/pay_bill_segment_amount.sql @@ -0,0 +1,49 @@ +-- 应付账单四段合计。total_amount 为账单合计,等于 A、B、C、D 四段之和。可重复执行。 +SET NAMES utf8mb4; + +SET @sql = ( + SELECT IF( + EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_bill' AND COLUMN_NAME = 'amount_a'), + 'SELECT 1', + 'ALTER TABLE pay_bill ADD COLUMN amount_a decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT ''A段合计'' AFTER order_count' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_bill' AND COLUMN_NAME = 'amount_b'), + 'SELECT 1', + 'ALTER TABLE pay_bill ADD COLUMN amount_b decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT ''B段合计'' AFTER amount_a' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_bill' AND COLUMN_NAME = 'amount_c'), + 'SELECT 1', + 'ALTER TABLE pay_bill ADD COLUMN amount_c decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT ''C段合计'' AFTER amount_b' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @sql = ( + SELECT IF( + EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'pay_bill' AND COLUMN_NAME = 'amount_d'), + 'SELECT 1', + 'ALTER TABLE pay_bill ADD COLUMN amount_d decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT ''D段合计'' AFTER amount_c' + ) +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +ALTER TABLE pay_bill + MODIFY COLUMN total_amount decimal(18,4) NOT NULL DEFAULT 0.0000 COMMENT '账单合计'; diff --git a/bms-front/src/api/monitor/priceLog.js b/bms-front/src/api/monitor/priceLog.js index 5e5accca..309aff4e 100644 --- a/bms-front/src/api/monitor/priceLog.js +++ b/bms-front/src/api/monitor/priceLog.js @@ -8,6 +8,15 @@ export function listPriceLog(query) { }) } +/** Group by productCode + updateTime for a package version. */ +export function listPriceLogGroups(query) { + return request({ + url: '/monitor/priceLog/groups', + method: 'get', + params: query + }) +} + export function getPriceLog(docId) { return request({ url: '/monitor/priceLog/' + encodeURIComponent(docId), diff --git a/bms-front/src/api/price/airBill.js b/bms-front/src/api/price/airBill.js index 1f37b2f5..dd2dc4dd 100644 --- a/bms-front/src/api/price/airBill.js +++ b/bms-front/src/api/price/airBill.js @@ -17,3 +17,30 @@ export function getAirBill(billCode) { params: { billCode } }) } + +/** 一票提单的费用明细 */ +export function listAirBillFee(billCode) { + return request({ + url: '/price/airBill/fee/list', + method: 'get', + params: { billCode } + }) +} + +/** 提单费用操作日志,分页参数 pageNum、pageSize */ +export function listAirBillFeeLog(query) { + return request({ + url: '/price/airBill/fee/log', + method: 'get', + params: query + }) +} + +/** 只提交新增、修改、删除的费用 */ +export function saveAirBillFee(data) { + return request({ + url: '/price/airBill/fee/save', + method: 'post', + data + }) +} diff --git a/bms-front/src/api/price/billing.js b/bms-front/src/api/price/billing.js index 470175bd..b0976de1 100644 --- a/bms-front/src/api/price/billing.js +++ b/bms-front/src/api/price/billing.js @@ -187,6 +187,15 @@ export function getBill(billId) { }) } +/** ES billing history for one bill/order */ +export function listBillHistory(billId, query) { + return request({ + url: '/price/billing/bill/' + encodeURIComponent(billId) + '/history', + method: 'get', + params: query + }) +} + /** Refund receivable bill (billType=REFUND) */ export function refundBill(billId) { return request({ diff --git a/bms-front/src/utils/excelExport.js b/bms-front/src/utils/excelExport.js index 4ca5508e..12bcd0c8 100644 --- a/bms-front/src/utils/excelExport.js +++ b/bms-front/src/utils/excelExport.js @@ -123,11 +123,13 @@ function taskDone(task, options) { * @param {string} [options.filePrefix='export'] * @param {string} [options.loadingText] * @param {(task: object) => void} [options.onProgress] - * @param {number} [options.pollIntervalMs=800] + * @param {boolean} [options.showLoading=true] full-screen ElLoading; set false when page shows its own float progress + * @param {number} [options.pollIntervalMs=1000] * @param {number} [options.maxWaitMs] * @param {string[]} [options.successStatus] * @param {string[]} [options.failStatus] * @param {boolean} [options.showSuccessMessage=true] + * @param {boolean} [options.showErrorMessage=true] */ export async function exportAsync(options = {}) { const { submitUrl, taskUrlPrefix, downloadUrlPrefix } = resolveAsyncUrls(options) @@ -152,11 +154,15 @@ export async function exportAsync(options = {}) { const filePrefix = options.filePrefix || 'export' const loadingText = options.loadingText || '\u6b63\u5728\u5bfc\u51fa\uff0c\u8bf7\u7a0d\u5019' - const pollIntervalMs = options.pollIntervalMs || 800 + const pollIntervalMs = options.pollIntervalMs || 1000 const maxWaitMs = options.maxWaitMs || 30 * 60 * 1000 const showSuccessMessage = options.showSuccessMessage !== false + const showErrorMessage = options.showErrorMessage !== false + const useLoading = options.showLoading !== false - const loading = ElLoading.service({ text: loadingText, background: 'rgba(0, 0, 0, 0.7)' }) + const loading = useLoading + ? ElLoading.service({ text: loadingText, background: 'rgba(0, 0, 0, 0.7)' }) + : null try { const submitRes = await request({ url: submitUrl, @@ -167,15 +173,23 @@ export async function exportAsync(options = {}) { }) const taskId = pickTaskId(submitRes && submitRes.data) if (!taskId) throw new Error('missing export taskId') + if (typeof options.onTaskId === 'function') options.onTaskId(String(taskId)) const started = Date.now() let task = null + let lastSig = '' while (Date.now() - started < maxWaitMs) { + const hidden = typeof document !== 'undefined' && document.visibilityState === 'hidden' const res = await request({ url: taskUrlPrefix + String(taskId), method: 'get' }) task = res.data || {} - if (typeof options.onProgress === 'function') options.onProgress(task) const pct = Number(task.percent) - if (!Number.isNaN(pct) && pct >= 0) { + const st = String(task.status || task.exportStatus || '').toUpperCase() + const sig = [st, pct, task.message, task.rowCount].join('|') + if (typeof options.onProgress === 'function' && sig !== lastSig) { + lastSig = sig + options.onProgress(task) + } + if (loading && !Number.isNaN(pct) && pct >= 0) { loading.setText(loadingText + ' ' + Math.min(100, pct) + '%') } const state = taskDone(task, options) @@ -183,7 +197,7 @@ export async function exportAsync(options = {}) { if (state === 'FAIL') { throw new Error(task.error || task.remark || task.message || 'export failed') } - await sleep(pollIntervalMs) + await sleep(hidden ? 2500 : pollIntervalMs) } if (!task || taskDone(task, options) !== 'SUCCESS') { throw new Error('export timeout') @@ -204,10 +218,10 @@ export async function exportAsync(options = {}) { if (showSuccessMessage) ElMessage.success('\u5bfc\u51fa\u6210\u529f') return { task, taskId } } catch (e) { - ElMessage.error((e && e.message) || '\u5bfc\u51fa\u5931\u8d25') + if (showErrorMessage) ElMessage.error((e && e.message) || '\u5bfc\u51fa\u5931\u8d25') return null } finally { - loading.close() + if (loading) loading.close() } } diff --git a/bms-front/src/utils/exportFieldDefs.js b/bms-front/src/utils/exportFieldDefs.js index a710d035..0603df73 100644 --- a/bms-front/src/utils/exportFieldDefs.js +++ b/bms-front/src/utils/exportFieldDefs.js @@ -68,7 +68,7 @@ export const BILLING_BILL_EXPORT_FIELDS = [ { prop: 'customerActiveStatusLabel', label: '客户状态' }, { prop: 'creditArrearsStatusLabel', label: '额度欠费' }, { prop: 'bizDate', label: '计费时间' }, - { prop: 'pickupDate', label: '提货时间' }, + { prop: 'pickupDate', label: '收货时间' }, { prop: 'belongSaler', label: '销售员' }, { prop: 'settlementMan', label: '结算员' }, { prop: 'serviceMan', label: '客服员' } diff --git a/bms-front/src/views/monitor/orderSource/index.vue b/bms-front/src/views/monitor/orderSource/index.vue index 74a44595..7020a2ae 100644 --- a/bms-front/src/views/monitor/orderSource/index.vue +++ b/bms-front/src/views/monitor/orderSource/index.vue @@ -191,14 +191,8 @@ const orderStatusMap = { const orderStatusOptions = Object.keys(orderStatusMap).map(k => ({ label: orderStatusMap[k], value: Number(k) })) -const blPickupOptions = [ - { label: '已收货', value: 1 }, - { label: '未收货', value: 0 } -] - const defaultParams = { - orderStatuses: [3, 4, 5, 6, 7, 8, 11], - blPickup: 1 + orderStatuses: [3, 4, 5, 6, 7, 8, 11] } const searchColumns = [ @@ -248,14 +242,7 @@ const searchColumns = [ collapseTags: true, collapseTagsTooltip: true } - }, - { - label: '收货状态', - prop: 'blPickup', - component: 'select', - options: blPickupOptions, - placeholder: '请选择' - }, + } ] const columns = [ @@ -402,7 +389,6 @@ function queryList(params) { const customers = splitLines(src.customerKeywords) const fillQuery = { orderIdentifiers: identifiers } if (customers.length) fillQuery.customerKeywords = customers - fillQuery.blPickup = src.blPickup === 0 || src.blPickup === 1 ? src.blPickup : 1 fillQuery.pageNum = params.pageNum fillQuery.pageSize = params.pageSize return listOrderSourceByTransferNos(fillQuery) @@ -435,7 +421,7 @@ function openOrderCompare() { return } proxy.$modal.confirm( - '将按当前客户、提货时间、订单状态和收货状态筛选,异步分页比对上游转单号是否已落库,确认继续?', + '将按当前客户、提货时间和订单状态筛选,异步分页比对上游转单号是否已落库,确认继续?', '二次确认' ).then(() => { // 比对只按客户+时间;不带单号框内容(回填后可能有数千转单号,带上会把比对拖死) diff --git a/bms-front/src/views/monitor/priceLog/index.vue b/bms-front/src/views/monitor/priceLog/index.vue deleted file mode 100644 index fbfa2730..00000000 --- a/bms-front/src/views/monitor/priceLog/index.vue +++ /dev/null @@ -1,265 +0,0 @@ - - - - - diff --git a/bms-front/src/views/price/billing/airBill.vue b/bms-front/src/views/price/billing/airBill.vue index ace29c7b..fead2bb6 100644 --- a/bms-front/src/views/price/billing/airBill.vue +++ b/bms-front/src/views/price/billing/airBill.vue @@ -49,18 +49,22 @@ + + + diff --git a/bms-front/src/views/price/billing/components/BillBillingHistoryDrawer.vue b/bms-front/src/views/price/billing/components/BillBillingHistoryDrawer.vue new file mode 100644 index 00000000..d7a29ec2 --- /dev/null +++ b/bms-front/src/views/price/billing/components/BillBillingHistoryDrawer.vue @@ -0,0 +1,265 @@ + + + + + diff --git a/bms-front/src/views/price/billing/components/BillDetail.vue b/bms-front/src/views/price/billing/components/BillDetail.vue index d46c83d4..914080b1 100644 --- a/bms-front/src/views/price/billing/components/BillDetail.vue +++ b/bms-front/src/views/price/billing/components/BillDetail.vue @@ -41,7 +41,17 @@
-
计费信息
+
+ 计费信息 + 历史计费 +
运费金额
@@ -187,6 +197,11 @@ :current-charge-weight="detail?.chargeWeight" @success="onChargeWeightUpdated" /> + @@ -196,6 +211,8 @@ import EditChargeWeightDialog from './EditChargeWeightDialog.vue' import { deleteIncomeBillFee, getBill } from '@/api/price/billing' import { generatePayBill } from '@/api/price/pay' import { ElMessage, ElMessageBox } from 'element-plus' +import BillBillingHistoryDrawer from './BillBillingHistoryDrawer.vue' +import { deleteIncomeBillFee } from '@/api/price/billing' const { proxy } = getCurrentInstance() @@ -210,6 +227,7 @@ const props = defineProps({ const emit = defineEmits(['update:modelValue', 'updated']) const feeTab = ref('income') +const historyOpen = ref(false) const addFeeOpen = ref(false) const editFeeRow = ref(null) const editCwOpen = ref(false) @@ -229,6 +247,7 @@ const payFees = computed(() => { watch(() => props.detail, () => { feeTab.value = 'income' + historyOpen.value = false }) function handlePayBill() { @@ -493,6 +512,9 @@ function refundLabel(status) { border-radius: 2px; background: var(--el-color-primary); } +.hist-btn { + margin-left: auto; +} .amount-summary { display: grid; grid-template-columns: repeat(5, 1fr); diff --git a/bms-front/src/views/price/orderBill/index.vue b/bms-front/src/views/price/orderBill/index.vue index 8bf80ca8..31ac313d 100644 --- a/bms-front/src/views/price/orderBill/index.vue +++ b/bms-front/src/views/price/orderBill/index.vue @@ -1,5 +1,19 @@ + + +
{{ exportProgressMessage }}
+
+ 导出行数 {{ exportProgressRowCount }} +
+

导出在后台进行,可收起弹窗继续操作;请勿关闭或刷新本页

+ +
@@ -233,6 +275,78 @@ const exportFieldRef = ref() const logDrawerRef = ref() const productCheckDrawerRef = ref() const checkLoading = ref(false) +const exportLoading = ref(false) +const exportFinished = ref(false) +const exportProgressVisible = ref(false) +const exportProgressPercent = ref(0) +const exportProgressMessage = ref('') +const exportProgressStatus = ref(undefined) +const exportProgressRowCount = ref(0) +const exportTaskId = ref('') +let exportFloatHideTimer = null + +const exportFloatVisible = computed(() => exportLoading.value || exportFinished.value) +const exportProgressDegree = computed(() => Math.round(((exportProgressPercent.value || 0) / 100) * 360)) +const exportProgressText = computed(() => (exportProgressPercent.value || 0) + '%') +const exportDialogTitle = computed(() => { + if (exportLoading.value) return '异步导出进行中' + if (exportFinished.value) return '异步导出完成' + return '异步导出' +}) +const exportFloatTitle = computed(() => { + if (exportLoading.value) return '导出中' + if (exportProgressStatus.value === 'exception') return '导出失败' + if (exportFinished.value) return '导出完成' + return '导出' +}) +const exportFloatSummary = computed(() => { + const pct = exportProgressPercent.value || 0 + const rows = exportProgressRowCount.value || 0 + if (rows > 0) return pct + '% / ' + rows + ' 行' + return pct + '%' +}) +const exportFloatTooltip = computed(() => exportProgressMessage.value || '点击查看详情') + +function openExportProgress() { + exportProgressVisible.value = true +} + +function scheduleHideExportFloat() { + if (exportFloatHideTimer) { + clearTimeout(exportFloatHideTimer) + exportFloatHideTimer = null + } + exportFloatHideTimer = window.setTimeout(() => { + if (!exportLoading.value && exportFinished.value) { + exportFinished.value = false + } + exportFloatHideTimer = null + }, 8000) +} + +function applyExportProgress(t) { + const pct = Math.min(100, Number(t && t.percent) || 0) + exportProgressPercent.value = pct + exportProgressRowCount.value = Number(t && t.rowCount) || 0 + const st = String((t && t.status) || '').toUpperCase() + if (st === 'PENDING') { + exportProgressMessage.value = (t && t.message) || '排队中...' + exportProgressStatus.value = undefined + } else if (st === 'RUNNING') { + exportProgressMessage.value = (t && t.message) || ('导出中 ' + pct + '%') + exportProgressStatus.value = undefined + } else if (st === 'SUCCESS') { + exportProgressMessage.value = (t && t.message) || '导出完成,正在下载' + exportProgressStatus.value = 'success' + exportProgressPercent.value = 100 + } else if (st === 'FAIL' || st === 'FAILED') { + exportProgressMessage.value = (t && (t.error || t.message)) || '导出失败' + exportProgressStatus.value = 'exception' + } else { + exportProgressMessage.value = (t && t.message) || '' + } +} + const batchLoading = ref(false) const rebillFinished = ref(false) const rebillProgressVisible = ref(false) @@ -516,21 +630,67 @@ function queryList(params) { } async function handleExport() { + if (exportLoading.value) return const params = tableRef.value?.getQueryParams?.() || {} if (!validateOrderIdentifierLimit(params.orderIdentifiers)) return const fields = await exportFieldRef.value?.open?.() if (!fields || !fields.length) return - return exportAsyncFromTable({ - baseUrl: '/price/billing/orderBill/export', - tableRef: tableRef.value, - normalize: (raw) => { - const q = applySearchRanges(raw) - q.exportFields = fields - return q - }, - filePrefix: 'order_bill', - requireFilter: true - }) + if (exportFloatHideTimer) { + clearTimeout(exportFloatHideTimer) + exportFloatHideTimer = null + } + exportLoading.value = true + exportFinished.value = false + exportProgressVisible.value = false + exportProgressPercent.value = 0 + exportProgressRowCount.value = 0 + exportProgressStatus.value = undefined + exportProgressMessage.value = '导出任务已提交' + exportTaskId.value = '' + try { + const result = await exportAsyncFromTable({ + baseUrl: '/price/billing/orderBill/export', + tableRef: tableRef.value, + normalize: (raw) => { + const q = applySearchRanges(raw) + q.exportFields = fields + return q + }, + filePrefix: 'order_bill', + requireFilter: true, + showLoading: false, + showSuccessMessage: true, + showErrorMessage: true, + onTaskId: (id) => { exportTaskId.value = id }, + onProgress: (task) => applyExportProgress(task || {}) + }) + if (result && result.task) { + applyExportProgress({ ...(result.task || {}), status: 'SUCCESS', percent: 100 }) + exportFinished.value = true + scheduleHideExportFloat() + } else if (!exportTaskId.value) { + // filter rejected / cancelled before task created + exportFinished.value = false + exportProgressPercent.value = 0 + exportProgressMessage.value = '' + exportProgressStatus.value = undefined + } else if (exportProgressStatus.value !== 'exception') { + exportProgressStatus.value = 'exception' + exportProgressMessage.value = exportProgressMessage.value || '导出失败' + exportFinished.value = true + scheduleHideExportFloat() + } else { + exportFinished.value = true + scheduleHideExportFloat() + } + } catch (e) { + exportProgressStatus.value = 'exception' + exportProgressMessage.value = (e && e.message) || '导出失败' + exportFinished.value = true + scheduleHideExportFloat() + } finally { + exportLoading.value = false + } } @@ -889,6 +1049,12 @@ onBeforeUnmount(() => { .rebill-progress__msg { margin-top: 12px; color: var(--el-text-color-regular); } .rebill-progress__text { margin-top: 8px; font-size: 13px; color: var(--el-text-color-secondary); } .rebill-progress__hint { margin-top: 12px; font-size: 12px; color: var(--el-color-warning); } +.order-bill-query .export-status-float { + top: 92px; +} +.order-bill-query .export-status-float.is-offset { + top: 178px; +} .order-bill-query .retry-status-float { position: fixed; top: 92px; diff --git a/bms-front/src/views/price/set/components/detail/SetVersionPriceLogDrawer.vue b/bms-front/src/views/price/set/components/detail/SetVersionPriceLogDrawer.vue new file mode 100644 index 00000000..c082b89c --- /dev/null +++ b/bms-front/src/views/price/set/components/detail/SetVersionPriceLogDrawer.vue @@ -0,0 +1,567 @@ + + + + + diff --git a/bms-front/src/views/price/set/components/detail/SetVersionTable.vue b/bms-front/src/views/price/set/components/detail/SetVersionTable.vue index e569bf64..1aa9c57f 100644 --- a/bms-front/src/views/price/set/components/detail/SetVersionTable.vue +++ b/bms-front/src/views/price/set/components/detail/SetVersionTable.vue @@ -12,13 +12,16 @@ - - + + + + +