From 92000956fca652859f0d18e67dbd2b2fb6a76b67 Mon Sep 17 00:00:00 2001 From: wuweihua <88888888> Date: Wed, 21 Oct 2026 18:06:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=AF=BC=E5=87=BA=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/price/BillingController.java | 79 +++++ .../main/resources/i18n/messages.properties | 6 + .../com/bms/common/utils/poi/ExcelUtil.java | 13 + .../price/domain/PrcOrderBillExportTask.java | 47 +++ .../vo/PrcBillingBillListExportRow.java | 66 ++++ .../domain/vo/PrcOrderBillListExportRow.java | 63 ++++ .../price/service/IPrcOrderBillService.java | 16 + .../service/impl/PrcOrderBillServiceImpl.java | 94 ++++++ .../support/OrderBillExportSupport.java | 297 ++++++++++++++++++ .../task/PrcOrderBillExportAsyncRunner.java | 157 +++++++++ .../sql/price/order_billing_export_menu.sql | 59 ++++ bms-front/src/utils/excelExport.js | 238 ++++++++++++++ bms-front/src/views/price/billing/bill.vue | 29 +- bms-front/src/views/price/orderBill/index.vue | 19 ++ bms-front/src/views/price/set/create.vue | 19 -- 15 files changed, 1181 insertions(+), 21 deletions(-) create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/PrcOrderBillExportTask.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcBillingBillListExportRow.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcOrderBillListExportRow.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java create mode 100644 bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillExportAsyncRunner.java create mode 100644 bms-back/sql/price/order_billing_export_menu.sql create mode 100644 bms-front/src/utils/excelExport.js 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 2a345e71..533eb850 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 @@ -37,6 +37,7 @@ import com.bms.price.domain.BatchAddIncomeBillFeeTask; import com.bms.price.domain.ChargeWeightImportTask; import com.bms.price.domain.PrcBillingTask; import com.bms.price.domain.PrcOrderBill; +import com.bms.price.domain.PrcOrderBillExportTask; import com.bms.price.domain.PrcQuote; import com.bms.price.domain.vo.AddIncomeBillFeeRequest; import com.bms.price.domain.vo.BatchAddIncomeBillFeeRequest; @@ -57,6 +58,7 @@ import com.bms.price.service.IPrcBillingTaskService; import com.bms.price.service.IPrcOrderBillService; import com.bms.price.service.ITmsPriceQuoteService; import com.bms.price.service.OrderBillProductCheckService; +import com.bms.price.service.support.OrderBillExportSupport; /** * Billing center: order quote trial (authenticated). @@ -147,6 +149,50 @@ public class BillingController extends BaseController return getDataTable(list); } + /** + * Async export billing list by current filters (status limited to 0-3). + * Requires at least one meaningful filter. Poll task then download. + */ + @PreAuthorize("@ss.hasPermi('price:billing:export')") + @Log(title = "\u8ba1\u8d39\u5217\u8868", businessType = BusinessType.EXPORT) + @PostMapping("/bill/export") + public AjaxResult exportBillList(@RequestBody(required = false) PrcOrderBill query) + { + if (query == null) + { + query = new PrcOrderBill(); + } + // Filter gate uses the request as-is; defaults applied only for query scope. + if (!OrderBillExportSupport.hasExportFilter(query)) + { + return error(MessageUtils.message("price.bill.export.filterRequired")); + } + query.setBillingFailStatuses(limitBillingListFailStatus(query.getBillingFailStatuses())); + query.setBillingFailStatus(null); + return successTask(prcOrderBillService.submitOrderBillExport( + PrcOrderBillExportTask.TYPE_BILLING, query)); + } + + @PreAuthorize("@ss.hasPermi('price:billing:export')") + @GetMapping("/bill/export/task/{taskId}") + public AjaxResult getBillExportTask(@PathVariable("taskId") String taskId) + { + PrcOrderBillExportTask task = prcOrderBillService.getOrderBillExportTask(taskId); + if (task == null) + { + return error(MessageUtils.message("price.bill.export.taskNotExists")); + } + return success(task); + } + + @PreAuthorize("@ss.hasPermi('price:billing:export')") + @GetMapping("/bill/export/download/{taskId}") + public void downloadBillExport(@PathVariable("taskId") String taskId, HttpServletResponse response) + throws Exception + { + prcOrderBillService.downloadOrderBillExport(taskId, response); + } + private static List limitBillingListFailStatus(List requested) { List allowed = Arrays.asList(Integer.valueOf(0), Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3)); @@ -179,6 +225,39 @@ public class BillingController extends BaseController return getDataTable(list); } + /** + * Async export order list by current filters. + * Requires at least one meaningful filter (default orderStatuses alone is not enough). + */ + @PreAuthorize("@ss.hasPermi('price:orderBill:export')") + @Log(title = "\u8ba2\u5355\u5217\u8868", businessType = BusinessType.EXPORT) + @PostMapping("/orderBill/export") + public AjaxResult exportOrderBillList(@RequestBody(required = false) PrcOrderBill query) + { + return successTask(prcOrderBillService.submitOrderBillExport( + PrcOrderBillExportTask.TYPE_ORDER, query == null ? new PrcOrderBill() : query)); + } + + @PreAuthorize("@ss.hasPermi('price:orderBill:export')") + @GetMapping("/orderBill/export/task/{taskId}") + public AjaxResult getOrderBillExportTask(@PathVariable("taskId") String taskId) + { + PrcOrderBillExportTask task = prcOrderBillService.getOrderBillExportTask(taskId); + if (task == null) + { + return error(MessageUtils.message("price.bill.export.taskNotExists")); + } + return success(task); + } + + @PreAuthorize("@ss.hasPermi('price:orderBill:export')") + @GetMapping("/orderBill/export/download/{taskId}") + public void downloadOrderBillExport(@PathVariable("taskId") String taskId, HttpServletResponse response) + throws Exception + { + prcOrderBillService.downloadOrderBillExport(taskId, response); + } + /** * Compare local order-bill products vs customer bound package SALES products * (includes product_code_mapping). diff --git a/bms-back/bms-admin/src/main/resources/i18n/messages.properties b/bms-back/bms-admin/src/main/resources/i18n/messages.properties index 8852450b..52b715b5 100644 --- a/bms-back/bms-admin/src/main/resources/i18n/messages.properties +++ b/bms-back/bms-admin/src/main/resources/i18n/messages.properties @@ -217,3 +217,9 @@ price.set.detail.duplicate.noZone=无分区 price.bill.billingFail.product=计费失败:未匹配产品 price.bill.billingFail.country=计费失败:未匹配国家 price.bill.billingFail.weight=计费失败:未匹配重量段 +price.bill.export.filterRequired=请先设置列表查询条件后再导出 +price.bill.export.taskNotExists=导出任务不存在或已过期 +price.bill.export.tooMany=匹配订单超过上限 {0} 条,请缩小筛选范围 +price.bill.export.noMatch=未查询到匹配订单,无法导出 +price.bill.export.notReady=导出文件尚未生成或已失败 +price.bill.export.fileMissing=导出文件不存在或路径非法 diff --git a/bms-back/bms-common/src/main/java/com/bms/common/utils/poi/ExcelUtil.java b/bms-back/bms-common/src/main/java/com/bms/common/utils/poi/ExcelUtil.java index 5f711ff2..bc826041 100644 --- a/bms-back/bms-common/src/main/java/com/bms/common/utils/poi/ExcelUtil.java +++ b/bms-back/bms-common/src/main/java/com/bms/common/utils/poi/ExcelUtil.java @@ -568,6 +568,19 @@ public class ExcelUtil exportExcel(response, list, sheetName, StringUtils.EMPTY); } + /** + * Common one-liner export for controllers. + * + * @param response HTTP response + * @param list export rows + * @param clazz row class with @Excel fields + * @param sheetName sheet / file title + */ + public static void export(HttpServletResponse response, List list, Class clazz, String sheetName) + { + new ExcelUtil(clazz).exportExcel(response, list, sheetName); + } + /** * 对list数据源将其里面的数据导入到excel表单 * diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcOrderBillExportTask.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcOrderBillExportTask.java new file mode 100644 index 00000000..0d8bc75f --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/PrcOrderBillExportTask.java @@ -0,0 +1,47 @@ +package com.bms.price.domain; + +import java.io.Serializable; + +/** Async order/billing list Excel export progress (Redis). */ +public class PrcOrderBillExportTask implements Serializable +{ + private static final long serialVersionUID = 1L; + + public static final String STATUS_PENDING = "PENDING"; + public static final String STATUS_RUNNING = "RUNNING"; + public static final String STATUS_SUCCESS = "SUCCESS"; + public static final String STATUS_FAIL = "FAIL"; + + public static final String TYPE_ORDER = "ORDER"; + public static final String TYPE_BILLING = "BILLING"; + + private String taskId; + private String status; + private int percent; + private String message; + private String error; + private String exportType; + private String fileName; + /** Relative path under download root, or empty until success. */ + private String storageKey; + private int rowCount; + + public String getTaskId() { return taskId; } + public void setTaskId(String taskId) { this.taskId = taskId; } + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + public int getPercent() { return percent; } + public void setPercent(int percent) { this.percent = percent; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + public String getError() { return error; } + public void setError(String error) { this.error = error; } + public String getExportType() { return exportType; } + public void setExportType(String exportType) { this.exportType = exportType; } + public String getFileName() { return fileName; } + public void setFileName(String fileName) { this.fileName = fileName; } + public String getStorageKey() { return storageKey; } + public void setStorageKey(String storageKey) { this.storageKey = storageKey; } + public int getRowCount() { return rowCount; } + public void setRowCount(int rowCount) { this.rowCount = rowCount; } +} 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 new file mode 100644 index 00000000..2289b62c --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcBillingBillListExportRow.java @@ -0,0 +1,66 @@ +package com.bms.price.domain.vo; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import com.bms.common.annotation.Excel; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** Billing-list Excel export row. */ +@Data +public class PrcBillingBillListExportRow implements Serializable +{ + private static final long serialVersionUID = 1L; + + @Excel(name = "订单号", width = 22) + private String orderNo; + @Excel(name = "运单号", width = 20) + private String trackingNo; + @Excel(name = "转单号", width = 20) + private String transferNo; + @Excel(name = "客户", width = 20) + private String customerName; + @Excel(name = "客户代码", width = 14) + private String customerCode; + @Excel(name = "产品代码", width = 14) + private String productCode; + @Excel(name = "产品名称", width = 22) + private String productName; + @Excel(name = "国家代码", width = 12) + private String countryCode; + @Excel(name = "国家/地区", width = 16) + private String countryName; + @Excel(name = "件数", width = 10) + private Integer pieces; + @Excel(name = "计费重", scale = 3, width = 12) + private BigDecimal chargeWeight; + @Excel(name = "原计费重", scale = 3, width = 12) + private BigDecimal weight; + @Excel(name = "收货重", scale = 3, width = 12) + private BigDecimal receiveWeight; + @Excel(name = "应收总额", scale = 2, width = 14) + private BigDecimal incomeTotal; + @Excel(name = "应付总额", scale = 2, width = 14) + private BigDecimal payTotal; + @Excel(name = "计价来源", width = 16) + private String matchSourceLabel; + @Excel(name = "计费状态", width = 14) + private String billingFailStatusLabel; + @Excel(name = "客户状态", width = 12) + private String customerActiveStatusLabel; + @Excel(name = "额度欠费", width = 12) + private String creditArrearsStatusLabel; + @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") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date orderTime; + @Excel(name = "销售员", width = 12) + private String belongSaler; + @Excel(name = "结算员", width = 12) + private String settlementMan; + @Excel(name = "客服员", width = 12) + private String serviceMan; +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcOrderBillListExportRow.java b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcOrderBillListExportRow.java new file mode 100644 index 00000000..706680ac --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/domain/vo/PrcOrderBillListExportRow.java @@ -0,0 +1,63 @@ +package com.bms.price.domain.vo; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; +import com.bms.common.annotation.Excel; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +/** Order-list Excel export row. */ +@Data +public class PrcOrderBillListExportRow implements Serializable +{ + private static final long serialVersionUID = 1L; + + @Excel(name = "订单号", width = 22) + private String orderNo; + @Excel(name = "运单号", width = 20) + private String trackingNo; + @Excel(name = "转单号", width = 20) + private String transferNo; + @Excel(name = "客户", width = 20) + private String customerName; + @Excel(name = "客户代码", width = 14) + private String customerCode; + @Excel(name = "产品代码", width = 14) + private String productCode; + @Excel(name = "产品名称", width = 22) + private String productName; + @Excel(name = "国家代码", width = 12) + private String countryCode; + @Excel(name = "国家/地区", width = 16) + private String countryName; + @Excel(name = "件数", width = 10) + private Integer pieces; + @Excel(name = "计费重", scale = 3, width = 12) + private BigDecimal weight; + @Excel(name = "体积重", scale = 3, width = 12) + private BigDecimal volumeWeight; + @Excel(name = "实重", scale = 3, width = 12) + private BigDecimal receiveWeight; + @Excel(name = "收件人邮编", width = 14) + private String destPostcode; + @Excel(name = "订单状态", width = 12) + private String orderStatusLabel; + @Excel(name = "计费状态", width = 16) + private String billingFailStatusLabel; + @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") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date orderTime; + @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) + private String belongSaler; + @Excel(name = "结算员", width = 12) + private String settlementMan; + @Excel(name = "客服员", width = 12) + private String serviceMan; +} 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 8db91dfa..34dc6f15 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 @@ -2,7 +2,9 @@ package com.bms.price.service; import java.math.BigDecimal; import java.util.List; +import jakarta.servlet.http.HttpServletResponse; import com.bms.price.domain.PrcOrderBill; +import com.bms.price.domain.PrcOrderBillExportTask; import com.bms.price.domain.BatchAddIncomeBillFeeTask; import com.bms.price.domain.ChargeWeightImportTask; import com.bms.price.domain.vo.AddIncomeBillFeeRequest; @@ -157,4 +159,18 @@ public interface IPrcOrderBillService /** Data-scoped count for local rebill. */ int countBillIdsForLocalRebill(PrcOrderBill query); + + /** + * Submit async Excel export for order list or billing list. + * Requires at least one meaningful filter; returns taskId for poll/download. + * + * @param exportType {@link PrcOrderBillExportTask#TYPE_ORDER} or {@link PrcOrderBillExportTask#TYPE_BILLING} + */ + String submitOrderBillExport(String exportType, PrcOrderBill query); + + /** Poll Redis progress for order/billing list export task. */ + PrcOrderBillExportTask getOrderBillExportTask(String taskId); + + /** Stream SUCCESS export file to response by taskId. */ + void downloadOrderBillExport(String taskId, HttpServletResponse response) throws Exception; } 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 5892017f..09904a62 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 @@ -2,6 +2,9 @@ package com.bms.price.service.impl; import java.math.BigDecimal; import java.math.RoundingMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.Arrays; @@ -12,10 +15,13 @@ import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionSynchronization; @@ -27,11 +33,13 @@ import com.bms.ar.enums.ArOrderBillStatus; import com.bms.common.annotation.DataScope; import com.bms.common.core.domain.entity.SysUser; import com.bms.common.exception.ServiceException; +import com.bms.common.config.BmsConfig; import com.bms.common.utils.DateUtils; import com.bms.common.utils.DictUtils; import com.bms.common.utils.MessageUtils; import com.bms.common.utils.SecurityUtils; import com.bms.common.utils.StringUtils; +import com.bms.common.utils.file.FileUtils; import com.bms.common.utils.sign.Md5Utils; import com.bms.common.utils.uuid.IdUtils; import com.bms.customer.customerinfo.domain.BmsCustomer; @@ -41,6 +49,7 @@ import com.bms.system.service.IBmsBizRuleService; import com.bms.price.domain.PrcIncomeBill; import com.bms.price.domain.PrcIncomeBillFee; import com.bms.price.domain.PrcOrderBill; +import com.bms.price.domain.PrcOrderBillExportTask; import com.bms.price.domain.PrcOrderBillExt; import com.bms.price.domain.PrcPayBill; import com.bms.price.domain.PrcPayBillFee; @@ -58,8 +67,10 @@ import com.bms.price.domain.vo.UpdateIncomeBillFeeRequest; import com.bms.price.task.PrcBatchAddIncomeFeeAsyncRunner; import com.bms.price.task.PrcChargeWeightImportAsyncRunner; import com.bms.price.task.PrcChargeWeightImportAsyncRunner.ChargeWeightImportWorkItem; +import com.bms.price.task.PrcOrderBillExportAsyncRunner; import com.bms.price.task.PrcOrderBillLocalRebillAsyncRunner; import com.bms.price.task.PrcRecalcBillPriceAsyncRunner; +import com.bms.price.service.support.OrderBillExportSupport; import com.bms.price.domain.vo.TmsPriceBillRequest; import com.bms.price.domain.vo.TmsOrderBillStatusResult; import com.bms.price.domain.vo.TmsPriceQuoteFeeLine; @@ -152,6 +163,9 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService @Autowired private PrcOrderBillLocalRebillAsyncRunner localRebillAsyncRunner; + @Autowired + private PrcOrderBillExportAsyncRunner orderBillExportAsyncRunner; + @Autowired private IBmsBizRuleService bizRuleService; @@ -161,6 +175,8 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService 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 order/billing list Excel export. */ + private static final int ORDER_BILL_EXPORT_MAX = 50000; @Override @Transactional(rollbackFor = Exception.class) @@ -843,6 +859,84 @@ public class PrcOrderBillServiceImpl implements IPrcOrderBillService return localRebillAsyncRunner.get(taskId); } + @Override + public String submitOrderBillExport(String exportType, PrcOrderBill query) + { + String type = StringUtils.nvl(exportType, "").trim().toUpperCase(); + if (!PrcOrderBillExportTask.TYPE_ORDER.equals(type) + && !PrcOrderBillExportTask.TYPE_BILLING.equals(type)) + { + type = PrcOrderBillExportTask.TYPE_ORDER; + } + PrcOrderBill q = query == null ? new PrcOrderBill() : query; + if (!OrderBillExportSupport.hasExportFilter(q)) + { + throw new ServiceException(MessageUtils.message("price.bill.export.filterRequired")); + } + int total = self.countBillIdsForLocalRebill(q); + if (total <= 0) + { + throw new ServiceException(MessageUtils.message("price.bill.export.noMatch")); + } + if (total > ORDER_BILL_EXPORT_MAX) + { + throw new ServiceException(MessageUtils.message("price.bill.export.tooMany", + Integer.valueOf(ORDER_BILL_EXPORT_MAX))); + } + String prefix = PrcOrderBillExportTask.TYPE_BILLING.equals(type) + ? "\u8ba1\u8d39\u5217\u8868" + : "\u8ba2\u5355\u5217\u8868"; + String fileName = prefix + "_" + DateUtils.dateTimeNow() + ".xlsx"; + String taskId = IdUtils.fastSimpleUUID(); + orderBillExportAsyncRunner.put(taskId, PrcOrderBillExportTask.STATUS_PENDING, 0, "queued", null, + type, fileName, null, 0); + // Pass request Authentication into async thread for @DataScope on selectBillList. + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + orderBillExportAsyncRunner.run(taskId, type, q, fileName, authentication); + return taskId; + } + + @Override + public PrcOrderBillExportTask getOrderBillExportTask(String taskId) + { + if (StringUtils.isEmpty(taskId) || orderBillExportAsyncRunner == null) + { + return null; + } + return orderBillExportAsyncRunner.get(taskId); + } + + @Override + public void downloadOrderBillExport(String taskId, HttpServletResponse response) throws Exception + { + PrcOrderBillExportTask task = getOrderBillExportTask(taskId); + if (task == null) + { + throw new ServiceException(MessageUtils.message("price.bill.export.taskNotExists")); + } + if (!PrcOrderBillExportTask.STATUS_SUCCESS.equals(task.getStatus()) + || StringUtils.isEmpty(task.getStorageKey())) + { + throw new ServiceException(MessageUtils.message("price.bill.export.notReady")); + } + if (!FileUtils.checkAllowDownload(task.getStorageKey())) + { + throw new ServiceException(MessageUtils.message("price.bill.export.fileMissing")); + } + Path downloadRoot = Paths.get(BmsConfig.getDownloadPath()).toAbsolutePath().normalize(); + Path filePath = downloadRoot.resolve(task.getStorageKey()).normalize(); + if (!filePath.startsWith(downloadRoot) || !Files.isRegularFile(filePath)) + { + throw new ServiceException(MessageUtils.message("price.bill.export.fileMissing")); + } + String fileName = StringUtils.isNotEmpty(task.getFileName()) + ? task.getFileName() + : (taskId + ".xlsx"); + response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + FileUtils.setAttachmentResponseHeader(response, fileName); + FileUtils.writeBytes(filePath.toString(), response.getOutputStream()); + } + @Override @DataScope(deptAlias = "su", deptField = "dept_id", userAlias = "su", userField = "user_id", permission = "price:billing:bill") public List selectBillIdsForLocalRebill(PrcOrderBill query) diff --git a/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java b/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java new file mode 100644 index 00000000..2bb1ae56 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/service/support/OrderBillExportSupport.java @@ -0,0 +1,297 @@ +package com.bms.price.service.support; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import jakarta.servlet.http.HttpServletResponse; +import com.bms.common.utils.StringUtils; +import com.bms.common.utils.poi.ExcelUtil; +import com.bms.price.domain.PrcOrderBill; +import com.bms.price.domain.vo.PrcBillingBillListExportRow; +import com.bms.price.domain.vo.PrcOrderBillListExportRow; + +/** Map PrcOrderBill rows to Excel export VOs and write response. */ +public final class OrderBillExportSupport +{ + private OrderBillExportSupport() + { + } + + /** + * True when query has at least one meaningful filter. + * Default orderStatuses alone does not count. + */ + public static boolean hasExportFilter(PrcOrderBill query) + { + if (query == null) + { + return false; + } + if (hasTexts(query.getOrderIdentifiers()) || hasTexts(query.getOrderNos()) + || hasTexts(query.getTrackingNos()) || hasTexts(query.getTransferNos()) + || hasTexts(query.getCustomerCodes()) || hasTexts(query.getProductCodes()) + || hasTexts(query.getBillIds())) + { + return true; + } + if (StringUtils.isNotEmpty(query.getCreditArrearsStatus()) + || StringUtils.isNotEmpty(query.getCountryCode()) + || StringUtils.isNotEmpty(query.getCustomerCode()) + || StringUtils.isNotEmpty(query.getOrderNo()) + || StringUtils.isNotEmpty(query.getTrackingNo()) + || StringUtils.isNotEmpty(query.getTransferNo()) + || StringUtils.isNotEmpty(query.getProductCode())) + { + return true; + } + if (query.getBillingFailStatuses() != null && !query.getBillingFailStatuses().isEmpty()) + { + return true; + } + if (query.getBillingFailStatus() != null) + { + return true; + } + Map params = query.getParams(); + if (params != null) + { + if (notBlank(params.get("beginBizDate")) || notBlank(params.get("endBizDate")) + || notBlank(params.get("beginPickupDate")) || notBlank(params.get("endPickupDate"))) + { + return true; + } + } + return false; + } + + private static boolean hasTexts(List list) + { + if (list == null || list.isEmpty()) + { + return false; + } + for (Object item : list) + { + if (item != null && StringUtils.isNotEmpty(String.valueOf(item).trim())) + { + return true; + } + } + return false; + } + + private static boolean notBlank(Object v) + { + return v != null && StringUtils.isNotEmpty(String.valueOf(v).trim()); + } + + /** Export order-list Excel by current query result. */ + public static void exportOrderList(HttpServletResponse response, List list) + { + ExcelUtil.export(response, toOrderListRows(list), PrcOrderBillListExportRow.class, "\u8ba2\u5355\u5217\u8868"); + } + + /** Export billing-list Excel by current query result. */ + public static void exportBillingList(HttpServletResponse response, List list) + { + ExcelUtil.export(response, toBillingListRows(list), PrcBillingBillListExportRow.class, "\u8ba1\u8d39\u5217\u8868"); + } + + public static List toOrderListRows(List list) + { + List rows = new ArrayList(); + if (list == null) + { + return rows; + } + for (PrcOrderBill bill : list) + { + if (bill == null) + { + continue; + } + PrcOrderBillListExportRow row = new PrcOrderBillListExportRow(); + row.setOrderNo(bill.getOrderNo()); + row.setTrackingNo(bill.getTrackingNo()); + row.setTransferNo(bill.getTransferNo()); + row.setCustomerName(bill.getCustomerName()); + row.setCustomerCode(bill.getCustomerCode()); + row.setProductCode(bill.getProductCode()); + row.setProductName(bill.getProductName()); + row.setCountryCode(bill.getCountryCode()); + row.setCountryName(bill.getCountryName()); + row.setPieces(bill.getPieces()); + row.setWeight(bill.getWeight()); + row.setVolumeWeight(bill.getVolumeWeight()); + row.setReceiveWeight(bill.getReceiveWeight()); + row.setDestPostcode(bill.getDestPostcode()); + row.setOrderStatusLabel(orderStatusLabel(bill.getOrderStatus())); + row.setBillingFailStatusLabel(billingFailStatusLabel(bill.getBillingFailStatus())); + row.setBizDate(bill.getBizDate()); + row.setOrderTime(bill.getOrderTime()); + row.setPickupDate(bill.getPickupDate()); + row.setBelongSaler(bill.getBelongSaler()); + row.setSettlementMan(bill.getSettlementMan()); + row.setServiceMan(bill.getServiceMan()); + rows.add(row); + } + return rows; + } + + public static List toBillingListRows(List list) + { + List rows = new ArrayList(); + if (list == null) + { + return rows; + } + for (PrcOrderBill bill : list) + { + if (bill == null) + { + continue; + } + PrcBillingBillListExportRow row = new PrcBillingBillListExportRow(); + row.setOrderNo(bill.getOrderNo()); + row.setTrackingNo(bill.getTrackingNo()); + row.setTransferNo(bill.getTransferNo()); + row.setCustomerName(bill.getCustomerName()); + row.setCustomerCode(bill.getCustomerCode()); + row.setProductCode(bill.getProductCode()); + row.setProductName(bill.getProductName()); + row.setCountryCode(bill.getCountryCode()); + row.setCountryName(bill.getCountryName()); + row.setPieces(bill.getPieces()); + row.setChargeWeight(bill.getChargeWeight()); + row.setWeight(bill.getWeight()); + row.setReceiveWeight(bill.getReceiveWeight()); + row.setIncomeTotal(bill.getIncomeTotal()); + row.setPayTotal(bill.getPayTotal()); + row.setMatchSourceLabel(matchSourceLabel(bill.getMatchSource())); + row.setBillingFailStatusLabel(billingFailStatusLabel(bill.getBillingFailStatus())); + row.setCustomerActiveStatusLabel(customerActiveLabel(bill.getCustomerActiveStatus())); + row.setCreditArrearsStatusLabel(creditArrearsLabel(bill.getCreditArrearsStatus())); + row.setBizDate(bill.getBizDate()); + row.setOrderTime(bill.getOrderTime()); + row.setBelongSaler(bill.getBelongSaler()); + row.setSettlementMan(bill.getSettlementMan()); + row.setServiceMan(bill.getServiceMan()); + rows.add(row); + } + return rows; + } + + public static String orderStatusLabel(Integer status) + { + if (status == null) + { + return ""; + } + switch (status.intValue()) + { + case 0: return "草稿订单"; + case 1: return "确认订单"; + case 2: return "已中转"; + case 3: return "已收货"; + case 4: return "已装箱"; + case 5: return "已出库"; + case 6: return "转运中"; + case 7: return "已上网"; + case 8: return "已签收"; + case 9: return "已退件"; + case 10: return "已取消"; + case 11: return "已复核"; + default: return String.valueOf(status); + } + } + + public static String billingFailStatusLabel(Integer status) + { + if (status == null) + { + return "计费成功"; + } + switch (status.intValue()) + { + case 0: return "计费成功"; + case 1: return "未匹配产品"; + case 2: return "未匹配国家"; + case 3: return "未匹配重量段"; + case 4: return "未绑定套餐"; + case 5: return "无生效套餐版本"; + case 6: return "已生成应收账单"; + case 7: return "产品档案缺失"; + case 8: return "未命中价格"; + case 9: return "分区无价格"; + case 10: return "邮编必填"; + case 11: return "邮编未匹配分区"; + case 12: return "上游订单不存在"; + case 13: return "上游订单号为空"; + case 14: return "上游客户为空"; + case 15: return "上游产品为空"; + case 16: return "上游重量无效"; + case 17: return "计费时间为空"; + case 18: return "退费订单无法解析"; + case 19: return "客户代码为空"; + case 20: return "客户已停用"; + case 21: return "运费为空"; + case 22: return "未知失败"; + default: return String.valueOf(status); + } + } + + public static String matchSourceLabel(String source) + { + if (StringUtils.isEmpty(source)) + { + return ""; + } + if ("PACKAGE_FIXED".equals(source) || "FIXED".equals(source)) + { + return "客户固定价套餐"; + } + if ("PACKAGE_DISCOUNT".equals(source) || "DISCOUNT".equals(source)) + { + return "客户折扣套餐"; + } + if ("PRODUCT_DEFAULT".equals(source)) + { + return "产品默认价"; + } + if ("BILLING_FAIL".equals(source)) + { + return "计费失败"; + } + return source; + } + + public static String customerActiveLabel(String status) + { + if ("ACTIVE".equals(status)) + { + return "正常"; + } + if ("MISSING".equals(status)) + { + return "不存在"; + } + if ("DISABLED".equals(status)) + { + return "停用"; + } + return status == null ? "" : status; + } + + public static String creditArrearsLabel(String status) + { + if ("NORMAL".equals(status)) + { + return "正常"; + } + if ("ARREARS".equals(status)) + { + return "欠费"; + } + return status == null ? "" : status; + } +} diff --git a/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillExportAsyncRunner.java b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillExportAsyncRunner.java new file mode 100644 index 00000000..1f9d8273 --- /dev/null +++ b/bms-back/bms-system/src/main/java/com/bms/price/task/PrcOrderBillExportAsyncRunner.java @@ -0,0 +1,157 @@ +package com.bms.price.task; + +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import com.bms.common.config.BmsConfig; +import com.bms.common.core.redis.RedisCache; +import com.bms.common.utils.StringUtils; +import com.bms.common.utils.poi.ExcelUtil; +import com.bms.price.domain.PrcOrderBill; +import com.bms.price.domain.PrcOrderBillExportTask; +import com.bms.price.domain.vo.PrcBillingBillListExportRow; +import com.bms.price.domain.vo.PrcOrderBillListExportRow; +import com.bms.price.service.IPrcOrderBillService; +import com.bms.price.service.support.OrderBillExportSupport; + +/** + * Async Excel export for order list / billing list. + * Writes under download path; poll task then download by taskId. + */ +@Component +public class PrcOrderBillExportAsyncRunner +{ + private static final Logger log = LoggerFactory.getLogger(PrcOrderBillExportAsyncRunner.class); + + public static final String CACHE_KEY_PREFIX = "price:billing:export:task:"; + public static final int TTL_MINUTES = 60; + public static final String REL_DIR = "orderBillExport"; + + @Autowired + @Lazy + private IPrcOrderBillService orderBillService; + + @Autowired + private RedisCache redisCache; + + /** + * @param authentication login from request thread; required because {@code selectBillList} uses {@code @DataScope} + */ + @Async("threadPoolTaskExecutor") + public void run(String taskId, String exportType, PrcOrderBill query, String fileName, + Authentication authentication) + { + Path file = null; + SecurityContext previous = SecurityContextHolder.getContext(); + try + { + // Async pool threads have no JWT filter; restore caller login for DataScope. + if (authentication != null) + { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + SecurityContextHolder.setContext(context); + } + put(taskId, PrcOrderBillExportTask.STATUS_RUNNING, 5, "querying", null, + exportType, fileName, null, 0); + List list = orderBillService.selectBillList(query == null ? new PrcOrderBill() : query); + int rows = list == null ? 0 : list.size(); + put(taskId, PrcOrderBillExportTask.STATUS_RUNNING, 40, "building excel", null, + exportType, fileName, null, rows); + + Path dir = Paths.get(BmsConfig.getDownloadPath(), REL_DIR).toAbsolutePath().normalize(); + Files.createDirectories(dir); + file = dir.resolve(taskId + ".xlsx").normalize(); + if (!file.startsWith(dir)) + { + throw new IllegalStateException("invalid export path"); + } + try (OutputStream out = Files.newOutputStream(file)) + { + if (PrcOrderBillExportTask.TYPE_BILLING.equals(exportType)) + { + List exportRows = OrderBillExportSupport.toBillingListRows(list); + new ExcelUtil(PrcBillingBillListExportRow.class) + .exportExcelToStream(exportRows, "\u8ba1\u8d39\u5217\u8868", out); + } + else + { + List exportRows = OrderBillExportSupport.toOrderListRows(list); + new ExcelUtil(PrcOrderBillListExportRow.class) + .exportExcelToStream(exportRows, "\u8ba2\u5355\u5217\u8868", out); + } + } + String storageKey = REL_DIR + "/" + taskId + ".xlsx"; + put(taskId, PrcOrderBillExportTask.STATUS_SUCCESS, 100, "done", null, + exportType, fileName, storageKey, rows); + } + catch (Exception e) + { + log.error("order bill export failed taskId={}", taskId, e); + if (file != null) + { + try { Files.deleteIfExists(file); } catch (Exception ignore) { } + } + put(taskId, PrcOrderBillExportTask.STATUS_FAIL, 100, "failed", errorMessage(e), + exportType, fileName, null, 0); + } + finally + { + if (previous == null) + { + SecurityContextHolder.clearContext(); + } + else + { + SecurityContextHolder.setContext(previous); + } + } + } + + public void put(String taskId, String status, int percent, String message, String error, + String exportType, String fileName, String storageKey, int rowCount) + { + PrcOrderBillExportTask task = new PrcOrderBillExportTask(); + task.setTaskId(taskId); + task.setStatus(status); + task.setPercent(percent); + task.setMessage(message); + task.setError(error); + task.setExportType(exportType); + task.setFileName(fileName); + task.setStorageKey(storageKey); + task.setRowCount(rowCount); + redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES); + } + + public PrcOrderBillExportTask get(String taskId) + { + return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId); + } + + private static String errorMessage(Exception e) + { + String msg = e.getMessage(); + if (StringUtils.isEmpty(msg)) + { + msg = e.getClass().getSimpleName(); + } + if (msg.length() > 500) + { + msg = msg.substring(0, 500); + } + return msg; + } +} diff --git a/bms-back/sql/price/order_billing_export_menu.sql b/bms-back/sql/price/order_billing_export_menu.sql new file mode 100644 index 00000000..f2e46594 --- /dev/null +++ b/bms-back/sql/price/order_billing_export_menu.sql @@ -0,0 +1,59 @@ +-- Export buttons for order list + billing list +SET NAMES utf8mb4; + +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 2343, N'导出', 2340, 3, '', NULL, + 1, 0, 'F', '0', '0', 'price:orderBill:export', '#', 'admin', sysdate(), + N'订单列表导出 Excel' + FROM DUAL + WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2343 OR perms = 'price:orderBill:export'); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, 2343 + FROM DUAL + WHERE NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = 1 AND menu_id = 2343); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT rm.role_id, 2343 + FROM sys_role_menu rm + WHERE rm.menu_id = 2340 + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = 2343 + ); + +SET @billMenuId := ( + SELECT menu_id FROM sys_menu + WHERE perms = 'price:billing:bill' AND menu_type = 'C' + ORDER BY menu_id + LIMIT 1 +); + +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 2352, N'导出', @billMenuId, 30, '', NULL, + 1, 0, 'F', '0', '0', 'price:billing:export', '#', 'admin', sysdate(), + N'计费列表导出 Excel' + FROM DUAL + WHERE @billMenuId IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2352 OR perms = 'price:billing:export'); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT 1, 2352 + FROM DUAL + WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2352) + AND NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = 1 AND menu_id = 2352); + +INSERT INTO sys_role_menu (role_id, menu_id) +SELECT DISTINCT rm.role_id, 2352 + FROM sys_role_menu rm + WHERE @billMenuId IS NOT NULL + AND rm.menu_id = @billMenuId + AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2352) + AND NOT EXISTS ( + SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = 2352 + ); diff --git a/bms-front/src/utils/excelExport.js b/bms-front/src/utils/excelExport.js new file mode 100644 index 00000000..10f8407a --- /dev/null +++ b/bms-front/src/utils/excelExport.js @@ -0,0 +1,238 @@ +import { download } from '@/utils/request' +import request from '@/utils/request' +import { ElLoading, ElMessage } from 'element-plus' +import { saveAs } from 'file-saver' + +const DEFAULT_IGNORE_KEYS = ['pageNum', 'pageSize', 'orderByColumn', 'isAsc', 'reasonable'] + +/** + * Sync list export: POST filters and download xlsx (most existing pages). + * @param {string} url e.g. 'system/user/export' + * @param {object} [query] + * @param {string} [filePrefix='export'] + */ +export function exportExcel(url, query, filePrefix = 'export') { + const name = String(filePrefix || 'export') + '_' + Date.now() + '.xlsx' + return download(url, { ...(query || {}) }, name) +} + +/** + * Sync export from basic-table current query. + */ +export function exportTable(url, tableRef, normalize, filePrefix = 'export') { + const raw = (tableRef && tableRef.getQueryParams && tableRef.getQueryParams()) || {} + const query = typeof normalize === 'function' ? normalize(raw) : raw + return exportExcel(url, query, filePrefix) +} + +function isFilled(value) { + if (value == null) return false + if (typeof value === 'string') return value.trim() !== '' + if (typeof value === 'number' || typeof value === 'boolean') return true + if (Array.isArray(value)) return value.some(v => isFilled(v)) + if (value instanceof Date) return !Number.isNaN(value.getTime()) + if (typeof value === 'object') { + return Object.keys(value).some(k => isFilled(value[k])) + } + return false +} + +/** + * Generic filter gate for list export. + * Default: any non-empty field except page/sort keys counts. + * + * @param {object} query + * @param {object} [opts] + * @param {string[]} [opts.ignoreKeys] keys that never count as filter + * @param {string[]} [opts.requireAny] if set, only these keys (and nested path) count + * @param {(q: object) => boolean} [opts.custom] custom predicate; wins when provided + */ +export function hasListFilter(query, opts = {}) { + if (typeof opts.custom === 'function') return !!opts.custom(query) + if (!query || typeof query !== 'object') return false + const ignore = new Set([...(opts.ignoreKeys || []), ...DEFAULT_IGNORE_KEYS]) + const requireAny = Array.isArray(opts.requireAny) && opts.requireAny.length + ? opts.requireAny + : null + const keys = requireAny || Object.keys(query) + for (const key of keys) { + if (ignore.has(key)) continue + if (isFilled(query[key])) return true + } + return false +} + +/** @deprecated use hasListFilter */ +export function hasExportFilter(q, opts = {}) { + const ignoreKeys = [...(opts.ignoreKeys || [])] + if (opts.ignoreOrderStatuses !== false) ignoreKeys.push('orderStatuses') + return hasListFilter(q, { ...opts, ignoreKeys }) +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +function normalizeBaseUrl(url) { + const s = String(url || '').trim() + if (!s) return '' + return s.startsWith('/') ? s.replace(/\/+$/, '') : ('/' + s.replace(/\/+$/, '')) +} + +function resolveAsyncUrls(options) { + const base = normalizeBaseUrl(options.baseUrl || options.submitUrl || '') + const submitUrl = normalizeBaseUrl(options.submitUrl || base) + const taskUrlPrefix = options.taskUrlPrefix + || (base ? base + '/task/' : '') + const downloadUrlPrefix = options.downloadUrlPrefix + || (base ? base + '/download/' : '') + return { submitUrl, taskUrlPrefix, downloadUrlPrefix } +} + +function pickTaskId(payload) { + if (payload == null) return null + if (typeof payload === 'string' || typeof payload === 'number') return String(payload) + return payload.taskId || payload.exportId || payload.id || null +} + +function taskDone(task, options) { + const status = String(task && task.status != null ? task.status : (task && task.exportStatus) || '').toUpperCase() + const successValues = options.successStatus || ['SUCCESS'] + const failValues = options.failStatus || ['FAIL', 'FAILED'] + if (successValues.map(v => String(v).toUpperCase()).includes(status)) return 'SUCCESS' + if (failValues.map(v => String(v).toUpperCase()).includes(status)) return 'FAIL' + return 'RUNNING' +} + +/** + * Async export common flow: submit -> poll -> download. + * Backend convention (recommended): + * POST {baseUrl} -> { taskId } + * GET {baseUrl}/task/{id} -> task progress + * GET {baseUrl}/download/{id}-> file blob + * + * @param {object} options + * @param {string} [options.baseUrl] preferred; derives task/download urls + * @param {string} [options.submitUrl] + * @param {string} [options.taskUrlPrefix] + * @param {string} [options.downloadUrlPrefix] + * @param {object} [options.query] + * @param {boolean} [options.requireFilter=false] + * @param {object} [options.filterOpts] passed to hasListFilter + * @param {string} [options.filterMessage] + * @param {string} [options.filePrefix='export'] + * @param {string} [options.loadingText] + * @param {(task: object) => void} [options.onProgress] + * @param {number} [options.pollIntervalMs=800] + * @param {number} [options.maxWaitMs] + * @param {string[]} [options.successStatus] + * @param {string[]} [options.failStatus] + * @param {boolean} [options.showSuccessMessage=true] + */ +export async function exportAsync(options = {}) { + const { submitUrl, taskUrlPrefix, downloadUrlPrefix } = resolveAsyncUrls(options) + if (!submitUrl || !taskUrlPrefix || !downloadUrlPrefix) { + throw new Error('exportAsync requires baseUrl or submit/task/download urls') + } + const query = { ...(options.query || {}) } + delete query.pageNum + delete query.pageSize + + if (options.requireFilter) { + const extraIgnore = (options.filterOpts && options.filterOpts.ignoreKeys) || [] + const filterOpts = { + ...(options.filterOpts || {}), + ignoreKeys: ['orderStatuses', ...extraIgnore] + } + if (!hasListFilter(query, filterOpts)) { + ElMessage.warning(options.filterMessage || '\u8bf7\u5148\u8bbe\u7f6e\u5217\u8868\u67e5\u8be2\u6761\u4ef6\u540e\u518d\u5bfc\u51fa') + return null + } + } + + const filePrefix = options.filePrefix || 'export' + const loadingText = options.loadingText || '\u6b63\u5728\u5bfc\u51fa\uff0c\u8bf7\u7a0d\u5019' + const pollIntervalMs = options.pollIntervalMs || 800 + const maxWaitMs = options.maxWaitMs || 30 * 60 * 1000 + const showSuccessMessage = options.showSuccessMessage !== false + + const loading = ElLoading.service({ text: loadingText, background: 'rgba(0, 0, 0, 0.7)' }) + try { + const submitRes = await request({ + url: submitUrl, + method: 'post', + data: query, + timeout: 120000, + headers: { repeatSubmit: false } + }) + const taskId = pickTaskId(submitRes && submitRes.data) + if (!taskId) throw new Error('missing export taskId') + + const started = Date.now() + let task = null + while (Date.now() - started < maxWaitMs) { + 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) { + loading.setText(loadingText + ' ' + Math.min(100, pct) + '%') + } + const state = taskDone(task, options) + if (state === 'SUCCESS') break + if (state === 'FAIL') { + throw new Error(task.error || task.remark || task.message || 'export failed') + } + await sleep(pollIntervalMs) + } + if (!task || taskDone(task, options) !== 'SUCCESS') { + throw new Error('export timeout') + } + + const file = await request({ + url: downloadUrlPrefix + String(taskId), + method: 'get', + responseType: 'blob', + timeout: 120000 + }) + if (file && file.type && String(file.type).includes('json')) { + const error = JSON.parse(await file.text()) + throw new Error(error.msg || 'download export file failed') + } + const name = task.fileName || (filePrefix + '_' + Date.now() + '.xlsx') + saveAs(new Blob([file]), name) + if (showSuccessMessage) ElMessage.success('\u5bfc\u51fa\u6210\u529f') + return { task, taskId } + } catch (e) { + ElMessage.error((e && e.message) || '\u5bfc\u51fa\u5931\u8d25') + return null + } finally { + loading.close() + } +} + +/** + * Async export from basic-table. + * @param {object} options same as exportAsync + tableRef/normalize + */ +export async function exportAsyncFromTable(options = {}) { + const raw = (options.tableRef && options.tableRef.getQueryParams && options.tableRef.getQueryParams()) || {} + const query = typeof options.normalize === 'function' ? options.normalize({ ...raw }) : { ...raw } + delete query.pageNum + delete query.pageSize + return exportAsync({ + ...options, + query, + requireFilter: options.requireFilter !== false + }) +} + +/** @deprecated use exportAsyncFromTable */ +export async function exportTableAsyncFromRef(options = {}) { + return exportAsyncFromTable(options) +} + +/** @deprecated use exportAsync */ +export async function exportTableAsync(options = {}) { + return exportAsync(options) +} diff --git a/bms-front/src/views/price/billing/bill.vue b/bms-front/src/views/price/billing/bill.vue index fddc0e54..31c587f6 100644 --- a/bms-front/src/views/price/billing/bill.vue +++ b/bms-front/src/views/price/billing/bill.vue @@ -117,6 +117,15 @@ @click="openRecalcDialog" >重计费 + + 导出 +