增加导出功能
This commit is contained in:
@@ -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<Integer> limitBillingListFailStatus(List<Integer> requested)
|
||||
{
|
||||
List<Integer> 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).
|
||||
|
||||
@@ -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=导出文件不存在或路径非法
|
||||
|
||||
@@ -568,6 +568,19 @@ public class ExcelUtil<T>
|
||||
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 <E> void export(HttpServletResponse response, List<E> list, Class<E> clazz, String sheetName)
|
||||
{
|
||||
new ExcelUtil<E>(clazz).exportExcel(response, list, sheetName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对list数据源将其里面的数据导入到excel表单
|
||||
*
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
+66
@@ -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;
|
||||
}
|
||||
+63
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+94
@@ -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<String> selectBillIdsForLocalRebill(PrcOrderBill query)
|
||||
|
||||
+297
@@ -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<String, Object> 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<PrcOrderBill> 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<PrcOrderBill> list)
|
||||
{
|
||||
ExcelUtil.export(response, toBillingListRows(list), PrcBillingBillListExportRow.class, "\u8ba1\u8d39\u5217\u8868");
|
||||
}
|
||||
|
||||
public static List<PrcOrderBillListExportRow> toOrderListRows(List<PrcOrderBill> list)
|
||||
{
|
||||
List<PrcOrderBillListExportRow> rows = new ArrayList<PrcOrderBillListExportRow>();
|
||||
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<PrcBillingBillListExportRow> toBillingListRows(List<PrcOrderBill> list)
|
||||
{
|
||||
List<PrcBillingBillListExportRow> rows = new ArrayList<PrcBillingBillListExportRow>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
+157
@@ -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<PrcOrderBill> 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<PrcBillingBillListExportRow> exportRows = OrderBillExportSupport.toBillingListRows(list);
|
||||
new ExcelUtil<PrcBillingBillListExportRow>(PrcBillingBillListExportRow.class)
|
||||
.exportExcelToStream(exportRows, "\u8ba1\u8d39\u5217\u8868", out);
|
||||
}
|
||||
else
|
||||
{
|
||||
List<PrcOrderBillListExportRow> exportRows = OrderBillExportSupport.toOrderListRows(list);
|
||||
new ExcelUtil<PrcOrderBillListExportRow>(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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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)
|
||||
}
|
||||
@@ -117,6 +117,15 @@
|
||||
@click="openRecalcDialog"
|
||||
>重计费</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Download"
|
||||
v-hasPermi="['price:billing:export']"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
</template>
|
||||
<template #col-action="{ row }">
|
||||
<el-button link type="primary" icon="View" @click="handleView(row)">查看</el-button>
|
||||
@@ -225,6 +234,7 @@
|
||||
|
||||
<script setup name="BillingBill">
|
||||
import { listBill, getBill, refundBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useProducts } from '@/components/ProductSelect/useProducts'
|
||||
import BillDetail from './components/BillDetail.vue'
|
||||
@@ -390,8 +400,8 @@ const searchColumns = [
|
||||
}
|
||||
]
|
||||
|
||||
function queryList(params) {
|
||||
const { bizDateRange, ...rest } = params
|
||||
function buildBillQuery(params) {
|
||||
const { bizDateRange, ...rest } = params || {}
|
||||
const query = { ...rest }
|
||||
if (Array.isArray(bizDateRange) && (bizDateRange[0] || bizDateRange[1])) {
|
||||
query.params = {
|
||||
@@ -400,9 +410,24 @@ function queryList(params) {
|
||||
endBizDate: bizDateRange[1] || null
|
||||
}
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
function queryList(params) {
|
||||
const query = buildBillQuery(params)
|
||||
return listBill(query)
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
return exportAsyncFromTable({
|
||||
baseUrl: '/price/billing/bill/export',
|
||||
tableRef: tableRef.value,
|
||||
normalize: buildBillQuery,
|
||||
filePrefix: 'billing_bill',
|
||||
requireFilter: true
|
||||
})
|
||||
}
|
||||
|
||||
function handleUpdateArrears(ids, selection) {
|
||||
const billIds = (ids && ids.length)
|
||||
? ids
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Document" @click="logDrawerRef.open()">操作记录</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Download"
|
||||
v-hasPermi="['price:orderBill:export']"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
@@ -129,6 +138,7 @@
|
||||
<script setup name="OrderBillQuery">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listOrderBill, listOrderNoOptions, listTrackingNoOptions, listTransferNoOptions, localRebillAsync } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue'
|
||||
import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue'
|
||||
|
||||
@@ -284,6 +294,15 @@ function queryList(params) {
|
||||
return listOrderBill(applySearchRanges(params))
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
return exportAsyncFromTable({
|
||||
baseUrl: '/price/billing/orderBill/export',
|
||||
tableRef: tableRef.value,
|
||||
normalize: applySearchRanges,
|
||||
filePrefix: 'order_bill',
|
||||
requireFilter: true
|
||||
})
|
||||
}
|
||||
|
||||
const MAX_REBILL = 10000
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
|
||||
<div class="wizard-actions">
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
<el-button v-if="showBasic" type="info" :loading="saving" @click="saveDraft">保存</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="finishEdit">完成</el-button>
|
||||
</div>
|
||||
|
||||
@@ -67,7 +66,6 @@ const route = useRoute()
|
||||
|
||||
const refData = usePriceSetRefData()
|
||||
|
||||
const saving = ref(false)
|
||||
const creating = ref(false)
|
||||
const progressVisible = ref(false)
|
||||
const progressPercent = ref(0)
|
||||
@@ -248,23 +246,6 @@ async function finishEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist basic draft (no detail wipe). */
|
||||
async function saveDraft() {
|
||||
const validBasic = await basicRef.value.validate()
|
||||
if (!validBasic) return
|
||||
saving.value = true
|
||||
try {
|
||||
const priceSetId = await runSaveWithProgress(buildPayload())
|
||||
form.priceSetId = priceSetId
|
||||
proxy.$modal.msgSuccess('已保存(待启用),套餐ID=' + priceSetId)
|
||||
goBack()
|
||||
} catch (e) {
|
||||
// surfaced
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function buildPayload(opts = {}) {
|
||||
const { basic, version } = form
|
||||
const detailRows = form.details.filter(d => d && d.productCode)
|
||||
|
||||
Reference in New Issue
Block a user