增加导出功能
This commit is contained in:
+85
@@ -37,6 +37,16 @@ import com.bms.price.search.EsLogType;
|
||||
import com.bms.price.search.IBillSearchStore;
|
||||
import com.bms.price.search.TabOrderSyncEsLog;
|
||||
import com.bms.price.task.TabOrderCompareAsyncRunner;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import com.bms.common.config.BmsConfig;
|
||||
import com.bms.common.utils.DateUtils;
|
||||
import com.bms.common.utils.file.FileUtils;
|
||||
import com.bms.common.utils.uuid.IdUtils;
|
||||
import com.bms.price.domain.vo.TabOrderExportTask;
|
||||
import com.bms.price.task.TabOrderExportAsyncRunner;
|
||||
import com.bms.price.task.TabOrderSyncAsyncRunner;
|
||||
|
||||
/**
|
||||
@@ -58,6 +68,9 @@ public class TabOrderMonitorController extends BaseController
|
||||
@Autowired
|
||||
private TabOrderCompareAsyncRunner tabOrderCompareAsyncRunner;
|
||||
|
||||
@Autowired
|
||||
private TabOrderExportAsyncRunner tabOrderExportAsyncRunner;
|
||||
|
||||
@Autowired
|
||||
private IBillSearchStore billSearchStore;
|
||||
|
||||
@@ -320,6 +333,78 @@ public class TabOrderMonitorController extends BaseController
|
||||
/**
|
||||
* 列表查询:有任一单号即可查询;无单号时必须客户(名称或代码)+提货时间。
|
||||
*/
|
||||
|
||||
/**
|
||||
* 异步导出订单数源:按当前筛选(须有单号,或客户+提货时间)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:export')")
|
||||
@Log(title = "订单数源", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public AjaxResult export(@RequestBody(required = false) TabOrderMonitorQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
query = new TabOrderMonitorQuery();
|
||||
}
|
||||
if (!hasListFilters(query))
|
||||
{
|
||||
return error("请先设置列表查询条件后再导出");
|
||||
}
|
||||
customerKeywordResolver.resolve(query);
|
||||
String taskId = IdUtils.fastSimpleUUID();
|
||||
String fileName = "订单数源_" + DateUtils.dateTimeNow() + ".xlsx";
|
||||
tabOrderExportAsyncRunner.put(taskId, TabOrderExportTask.STATUS_PENDING, 0, "queued", null,
|
||||
fileName, null, 0);
|
||||
tabOrderExportAsyncRunner.run(taskId, query, fileName);
|
||||
Map<String, Object> data = new HashMap<String, Object>();
|
||||
data.put("taskId", taskId);
|
||||
return success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:export')")
|
||||
@GetMapping("/export/task/{taskId}")
|
||||
public AjaxResult exportTask(@PathVariable("taskId") String taskId)
|
||||
{
|
||||
TabOrderExportTask task = tabOrderExportAsyncRunner.get(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
return error("export task not found or expired");
|
||||
}
|
||||
return success(task);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('monitor:orderSource:export')")
|
||||
@GetMapping("/export/download/{taskId}")
|
||||
public void exportDownload(@PathVariable("taskId") String taskId, HttpServletResponse response)
|
||||
throws Exception
|
||||
{
|
||||
TabOrderExportTask task = tabOrderExportAsyncRunner.get(taskId);
|
||||
if (task == null)
|
||||
{
|
||||
throw new com.bms.common.exception.ServiceException("export task not found or expired");
|
||||
}
|
||||
if (!TabOrderExportTask.STATUS_SUCCESS.equals(task.getStatus())
|
||||
|| StringUtils.isEmpty(task.getStorageKey()))
|
||||
{
|
||||
throw new com.bms.common.exception.ServiceException("export file not ready");
|
||||
}
|
||||
if (!FileUtils.checkAllowDownload(task.getStorageKey()))
|
||||
{
|
||||
throw new com.bms.common.exception.ServiceException("export file missing");
|
||||
}
|
||||
Path downloadRoot = Paths.get(BmsConfig.getDownloadPath()).toAbsolutePath().normalize();
|
||||
Path filePath = downloadRoot.resolve(task.getStorageKey()).normalize();
|
||||
if (!filePath.startsWith(downloadRoot) || !Files.isRegularFile(filePath))
|
||||
{
|
||||
throw new com.bms.common.exception.ServiceException("export file missing");
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
private static boolean hasListFilters(TabOrderMonitorQuery query)
|
||||
{
|
||||
return hasRequiredFilters(query) || hasNonBlankItem(query.getOrderIdentifiers());
|
||||
|
||||
@@ -132,4 +132,7 @@ public class PrcOrderBill extends BaseEntity
|
||||
|
||||
private List<PrcIncomeBillFee> incomeFees;
|
||||
private List<PrcPayBillFee> payFees;
|
||||
|
||||
/** Excel export fields (Java props; export only). */
|
||||
private List<String> exportFields;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.bms.price.domain.vo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** 订单数源异步 Excel 导出进度(Redis)。 */
|
||||
public class TabOrderExportTask 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";
|
||||
|
||||
private String taskId;
|
||||
private String status;
|
||||
private int percent;
|
||||
private String message;
|
||||
private String error;
|
||||
private String fileName;
|
||||
/** Relative path under download root. */
|
||||
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 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; }
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
|
||||
/** 订单数源列表 Excel 导出行。 */
|
||||
@Data
|
||||
public class TabOrderMonitorListExportRow implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "客户订单号", width = 22)
|
||||
private String orderId;
|
||||
@Excel(name = "运单号", width = 20)
|
||||
private String orgOrderId;
|
||||
@Excel(name = "转单号", width = 20)
|
||||
private String transferOrderId;
|
||||
@Excel(name = "轨迹ID", width = 16)
|
||||
private String trackId;
|
||||
@Excel(name = "订单状态", width = 12)
|
||||
private String orderStatusLabel;
|
||||
@Excel(name = "客户代码", width = 14)
|
||||
private String customerCode;
|
||||
@Excel(name = "客户名称", width = 20)
|
||||
private String customerName;
|
||||
@Excel(name = "产品代码", width = 14)
|
||||
private String productCode;
|
||||
@Excel(name = "产品名称", width = 22)
|
||||
private String productName;
|
||||
@Excel(name = "产品底级代码", width = 16)
|
||||
private String productFloorCode;
|
||||
@Excel(name = "国家代码", width = 12)
|
||||
private String country;
|
||||
@Excel(name = "国家/地区", width = 16)
|
||||
private String countryName;
|
||||
@Excel(name = "收件人邮编", width = 14)
|
||||
private String consigneePostcode;
|
||||
@Excel(name = "客户确认重", scale = 3, width = 12)
|
||||
private BigDecimal confirmWeight;
|
||||
@Excel(name = "计费重", scale = 3, width = 12)
|
||||
private BigDecimal dimensionalWeightFactor;
|
||||
@Excel(name = "体积重", scale = 3, width = 12)
|
||||
private BigDecimal volumeWeight;
|
||||
@Excel(name = "实重", scale = 3, width = 12)
|
||||
private BigDecimal weight;
|
||||
@Excel(name = "提货时间", width = 20, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date pickupDate;
|
||||
}
|
||||
@@ -45,4 +45,6 @@ public class TabOrderMonitorQuery implements Serializable
|
||||
private List<Integer> orderStatuses;
|
||||
/** 分批同步计费每批条数 */
|
||||
private Integer batchSize;
|
||||
/** Excel export field names (Java props; export only). */
|
||||
private List<String> exportFields;
|
||||
}
|
||||
|
||||
+25
@@ -64,6 +64,31 @@ public final class OrderBillExportSupport
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/** Null/empty => export all columns; otherwise only named Java props. */
|
||||
public static String[] toExportFieldArray(List<String> exportFields)
|
||||
{
|
||||
if (exportFields == null || exportFields.isEmpty())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
List<String> cleaned = new ArrayList<String>();
|
||||
for (String f : exportFields)
|
||||
{
|
||||
if (f == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String t = f.trim();
|
||||
if (t.isEmpty() || cleaned.contains(t))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
cleaned.add(t);
|
||||
}
|
||||
return cleaned.isEmpty() ? null : cleaned.toArray(new String[cleaned.size()]);
|
||||
}
|
||||
|
||||
private static boolean hasTexts(List<?> list)
|
||||
{
|
||||
if (list == null || list.isEmpty())
|
||||
|
||||
+18
-4
@@ -83,14 +83,28 @@ public class PrcOrderBillExportAsyncRunner
|
||||
if (PrcOrderBillExportTask.TYPE_BILLING.equals(exportType))
|
||||
{
|
||||
List<PrcBillingBillListExportRow> exportRows = OrderBillExportSupport.toBillingListRows(list);
|
||||
new ExcelUtil<PrcBillingBillListExportRow>(PrcBillingBillListExportRow.class)
|
||||
.exportExcelToStream(exportRows, "\u8ba1\u8d39\u5217\u8868", out);
|
||||
ExcelUtil<PrcBillingBillListExportRow> billingUtil =
|
||||
new ExcelUtil<PrcBillingBillListExportRow>(PrcBillingBillListExportRow.class);
|
||||
String[] billingFields = OrderBillExportSupport.toExportFieldArray(
|
||||
query == null ? null : query.getExportFields());
|
||||
if (billingFields != null)
|
||||
{
|
||||
billingUtil.showColumn(billingFields);
|
||||
}
|
||||
billingUtil.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);
|
||||
ExcelUtil<PrcOrderBillListExportRow> orderUtil =
|
||||
new ExcelUtil<PrcOrderBillListExportRow>(PrcOrderBillListExportRow.class);
|
||||
String[] orderFields = OrderBillExportSupport.toExportFieldArray(
|
||||
query == null ? null : query.getExportFields());
|
||||
if (orderFields != null)
|
||||
{
|
||||
orderUtil.showColumn(orderFields);
|
||||
}
|
||||
orderUtil.exportExcelToStream(exportRows, "\u8ba2\u5355\u5217\u8868", out);
|
||||
}
|
||||
}
|
||||
String storageKey = REL_DIR + "/" + taskId + ".xlsx";
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
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.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
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.service.support.OrderBillExportSupport;
|
||||
import com.bms.price.domain.vo.TabOrderExportTask;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorListExportRow;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorQuery;
|
||||
import com.bms.price.domain.vo.TabOrderMonitorVo;
|
||||
import com.bms.price.oracle.TabOrderMonitorService;
|
||||
import com.bms.price.oracle.TabOrderPageResult;
|
||||
|
||||
/**
|
||||
* 订单数源异步 Excel 导出:按当前筛选分页拉取上游列表。
|
||||
*/
|
||||
@Component
|
||||
public class TabOrderExportAsyncRunner
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(TabOrderExportAsyncRunner.class);
|
||||
|
||||
public static final String CACHE_KEY_PREFIX = "monitor:orderSource:export:task:";
|
||||
public static final int TTL_MINUTES = 60;
|
||||
public static final String REL_DIR = "orderSourceExport";
|
||||
public static final int PAGE_SIZE = 500;
|
||||
/** 单次导出行数上限(防 OOM) */
|
||||
public static final int EXPORT_MAX = 20000;
|
||||
|
||||
private static final Map<Integer, String> ORDER_STATUS_LABEL = new HashMap<Integer, String>();
|
||||
static
|
||||
{
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(0), "预报订单");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(1), "确认订单");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(2), "已中转");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(3), "已收货");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(4), "已装箱");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(5), "已出库");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(6), "转运中");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(7), "已上网");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(8), "已签收");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(9), "已退件");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(10), "已删除");
|
||||
ORDER_STATUS_LABEL.put(Integer.valueOf(11), "已复核");
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private TabOrderMonitorService tabOrderMonitorService;
|
||||
|
||||
@Autowired
|
||||
private RedisCache redisCache;
|
||||
|
||||
@Async("threadPoolTaskExecutor")
|
||||
public void run(String taskId, TabOrderMonitorQuery query, String fileName)
|
||||
{
|
||||
Path file = null;
|
||||
try
|
||||
{
|
||||
put(taskId, TabOrderExportTask.STATUS_RUNNING, 5, "querying", null, fileName, null, 0);
|
||||
List<TabOrderMonitorVo> all = new ArrayList<TabOrderMonitorVo>();
|
||||
int pageNum = 1;
|
||||
long total = -1L;
|
||||
while (true)
|
||||
{
|
||||
TabOrderPageResult page = tabOrderMonitorService.searchPage(query, pageNum, PAGE_SIZE);
|
||||
if (page == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (total < 0L)
|
||||
{
|
||||
total = page.getTotal();
|
||||
if (total > EXPORT_MAX)
|
||||
{
|
||||
throw new IllegalStateException("导出数量超过上限 " + EXPORT_MAX
|
||||
+ "(当前 " + total + "),请缩小筛选范围");
|
||||
}
|
||||
if (total <= 0L)
|
||||
{
|
||||
throw new IllegalStateException("当前筛选无匹配数据");
|
||||
}
|
||||
}
|
||||
List<TabOrderMonitorVo> rows = page.getRows();
|
||||
if (rows == null || rows.isEmpty())
|
||||
{
|
||||
break;
|
||||
}
|
||||
all.addAll(rows);
|
||||
int pct = Math.min(70, 5 + (int) ((all.size() * 65L) / Math.max(total, 1L)));
|
||||
put(taskId, TabOrderExportTask.STATUS_RUNNING, pct, "querying " + all.size() + "/" + total,
|
||||
null, fileName, null, all.size());
|
||||
if (all.size() >= total || rows.size() < PAGE_SIZE || all.size() >= EXPORT_MAX)
|
||||
{
|
||||
break;
|
||||
}
|
||||
pageNum++;
|
||||
}
|
||||
put(taskId, TabOrderExportTask.STATUS_RUNNING, 80, "building excel", null, fileName, null, all.size());
|
||||
|
||||
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");
|
||||
}
|
||||
List<TabOrderMonitorListExportRow> exportRows = toExportRows(all);
|
||||
try (OutputStream out = Files.newOutputStream(file))
|
||||
{
|
||||
ExcelUtil<TabOrderMonitorListExportRow> util =
|
||||
new ExcelUtil<TabOrderMonitorListExportRow>(TabOrderMonitorListExportRow.class);
|
||||
String[] fields = OrderBillExportSupport.toExportFieldArray(
|
||||
query == null ? null : query.getExportFields());
|
||||
if (fields != null)
|
||||
{
|
||||
util.showColumn(fields);
|
||||
}
|
||||
util.exportExcelToStream(exportRows, "订单数源", out);
|
||||
}
|
||||
String storageKey = REL_DIR + "/" + taskId + ".xlsx";
|
||||
put(taskId, TabOrderExportTask.STATUS_SUCCESS, 100, "done", null, fileName, storageKey, exportRows.size());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.error("orderSource export failed taskId={}", taskId, e);
|
||||
if (file != null)
|
||||
{
|
||||
try { Files.deleteIfExists(file); } catch (Exception ignore) { }
|
||||
}
|
||||
put(taskId, TabOrderExportTask.STATUS_FAIL, 100, "failed", errorMessage(e), fileName, null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void put(String taskId, String status, int percent, String message, String error,
|
||||
String fileName, String storageKey, int rowCount)
|
||||
{
|
||||
TabOrderExportTask task = new TabOrderExportTask();
|
||||
task.setTaskId(taskId);
|
||||
task.setStatus(status);
|
||||
task.setPercent(percent);
|
||||
task.setMessage(message);
|
||||
task.setError(error);
|
||||
task.setFileName(fileName);
|
||||
task.setStorageKey(storageKey);
|
||||
task.setRowCount(rowCount);
|
||||
redisCache.setCacheObject(CACHE_KEY_PREFIX + taskId, task, TTL_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
public TabOrderExportTask get(String taskId)
|
||||
{
|
||||
return redisCache.getCacheObject(CACHE_KEY_PREFIX + taskId);
|
||||
}
|
||||
|
||||
private static List<TabOrderMonitorListExportRow> toExportRows(List<TabOrderMonitorVo> list)
|
||||
{
|
||||
List<TabOrderMonitorListExportRow> rows = new ArrayList<TabOrderMonitorListExportRow>();
|
||||
if (list == null)
|
||||
{
|
||||
return rows;
|
||||
}
|
||||
for (TabOrderMonitorVo vo : list)
|
||||
{
|
||||
if (vo == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
TabOrderMonitorListExportRow row = new TabOrderMonitorListExportRow();
|
||||
row.setOrderId(vo.getOrderId());
|
||||
row.setOrgOrderId(vo.getOrgOrderId());
|
||||
row.setTransferOrderId(vo.getTransferOrderId());
|
||||
row.setTrackId(vo.getTrackId());
|
||||
row.setOrderStatusLabel(statusLabel(vo.getOrderStatus()));
|
||||
row.setCustomerCode(vo.getCustomerCode());
|
||||
row.setCustomerName(vo.getCustomerName());
|
||||
row.setProductCode(vo.getProductCode());
|
||||
row.setProductName(vo.getProductName());
|
||||
row.setProductFloorCode(vo.getProductFloorCode());
|
||||
row.setCountry(vo.getCountry());
|
||||
row.setCountryName(vo.getCountryName());
|
||||
row.setConsigneePostcode(vo.getConsigneePostcode());
|
||||
row.setConfirmWeight(vo.getConfirmWeight());
|
||||
row.setDimensionalWeightFactor(vo.getDimensionalWeightFactor());
|
||||
row.setVolumeWeight(vo.getVolumeWeight());
|
||||
row.setWeight(vo.getWeight());
|
||||
row.setPickupDate(vo.getPickupDate());
|
||||
rows.add(row);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static String statusLabel(Integer status)
|
||||
{
|
||||
if (status == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
String label = ORDER_STATUS_LABEL.get(status);
|
||||
return label == null ? String.valueOf(status) : label;
|
||||
}
|
||||
|
||||
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,27 @@
|
||||
-- 订单数源导出
|
||||
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 2329, N'导出', 2325, 4, '', NULL,
|
||||
1, 0, 'F', '0', '0', 'monitor:orderSource:export', '#', 'admin', sysdate(),
|
||||
N'订单数源导出 Excel'
|
||||
FROM DUAL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2329 OR perms = 'monitor:orderSource:export');
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT 1, 2329
|
||||
FROM DUAL
|
||||
WHERE EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2329)
|
||||
AND NOT EXISTS (SELECT 1 FROM sys_role_menu WHERE role_id = 1 AND menu_id = 2329);
|
||||
|
||||
INSERT INTO sys_role_menu (role_id, menu_id)
|
||||
SELECT DISTINCT rm.role_id, 2329
|
||||
FROM sys_role_menu rm
|
||||
WHERE rm.menu_id = 2325
|
||||
AND EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2329)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM sys_role_menu x WHERE x.role_id = rm.role_id AND x.menu_id = 2329
|
||||
);
|
||||
@@ -100,3 +100,30 @@ export function listOrderSourceOrdersByProduct(data) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** 异步导出:提交任务 */
|
||||
export function exportOrderSource(data) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/export',
|
||||
method: 'post',
|
||||
data,
|
||||
timeout: 120000,
|
||||
headers: { repeatSubmit: false }
|
||||
})
|
||||
}
|
||||
|
||||
export function getOrderSourceExportTask(taskId) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/export/task/' + taskId,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function downloadOrderSourceExport(taskId) {
|
||||
return request({
|
||||
url: '/monitor/orderSource/export/download/' + taskId,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="560px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@closed="onClosed"
|
||||
>
|
||||
<div class="export-field-toolbar">
|
||||
<el-checkbox
|
||||
:model-value="allChecked"
|
||||
:indeterminate="indeterminate"
|
||||
@change="toggleAll"
|
||||
>全选</el-checkbox>
|
||||
<el-button link type="primary" @click="resetDefault">恢复默认</el-button>
|
||||
</div>
|
||||
<el-checkbox-group v-model="checked" class="export-field-grid">
|
||||
<el-checkbox
|
||||
v-for="f in fields"
|
||||
:key="f.prop"
|
||||
:value="f.prop"
|
||||
>{{ f.label }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :disabled="!checked.length" @click="confirm">确定导出</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const props = defineProps({
|
||||
title: { type: String, default: '选择导出字段' },
|
||||
fields: { type: Array, default: () => [] },
|
||||
/** localStorage key to remember last selection */
|
||||
storageKey: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
const checked = ref([])
|
||||
let resolveFn = null
|
||||
|
||||
const allProps = computed(() => (props.fields || []).map(f => f.prop).filter(Boolean))
|
||||
const allChecked = computed(() => allProps.value.length > 0 && checked.value.length === allProps.value.length)
|
||||
const indeterminate = computed(() => checked.value.length > 0 && checked.value.length < allProps.value.length)
|
||||
|
||||
function defaultChecked() {
|
||||
return allProps.value.slice()
|
||||
}
|
||||
|
||||
function loadStored() {
|
||||
if (!props.storageKey) return null
|
||||
try {
|
||||
const raw = localStorage.getItem(props.storageKey)
|
||||
if (!raw) return null
|
||||
const arr = JSON.parse(raw)
|
||||
if (!Array.isArray(arr)) return null
|
||||
const allow = new Set(allProps.value)
|
||||
return arr.map(String).filter(p => allow.has(p))
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function saveStored(list) {
|
||||
if (!props.storageKey) return
|
||||
try {
|
||||
localStorage.setItem(props.storageKey, JSON.stringify(list || []))
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function toggleAll(val) {
|
||||
checked.value = val ? defaultChecked() : []
|
||||
}
|
||||
|
||||
function resetDefault() {
|
||||
checked.value = defaultChecked()
|
||||
}
|
||||
|
||||
function open() {
|
||||
const stored = loadStored()
|
||||
checked.value = (stored && stored.length) ? stored : defaultChecked()
|
||||
visible.value = true
|
||||
return new Promise(resolve => {
|
||||
resolveFn = resolve
|
||||
})
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (!checked.value.length) {
|
||||
ElMessage.warning('请至少选择一个导出字段')
|
||||
return
|
||||
}
|
||||
// keep declaration order
|
||||
const allow = new Set(checked.value)
|
||||
const ordered = allProps.value.filter(p => allow.has(p))
|
||||
saveStored(ordered)
|
||||
visible.value = false
|
||||
if (resolveFn) {
|
||||
resolveFn(ordered)
|
||||
resolveFn = null
|
||||
}
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
if (resolveFn) {
|
||||
resolveFn(null)
|
||||
resolveFn = null
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.export-field-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.export-field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px 12px;
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
}
|
||||
.export-field-grid :deep(.el-checkbox) {
|
||||
margin-right: 0;
|
||||
height: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
@@ -142,7 +142,7 @@ export async function exportAsync(options = {}) {
|
||||
const extraIgnore = (options.filterOpts && options.filterOpts.ignoreKeys) || []
|
||||
const filterOpts = {
|
||||
...(options.filterOpts || {}),
|
||||
ignoreKeys: ['orderStatuses', ...extraIgnore]
|
||||
ignoreKeys: ['orderStatuses', 'exportFields', ...extraIgnore]
|
||||
}
|
||||
if (!hasListFilter(query, filterOpts)) {
|
||||
ElMessage.warning(options.filterMessage || '\u8bf7\u5148\u8bbe\u7f6e\u5217\u8868\u67e5\u8be2\u6761\u4ef6\u540e\u518d\u5bfc\u51fa')
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/** Excel export field options (prop must match backend export-row Java field names). */
|
||||
|
||||
export const ORDER_SOURCE_EXPORT_FIELDS = [
|
||||
{ prop: 'orderId', label: '客户订单号' },
|
||||
{ prop: 'orgOrderId', label: '运单号' },
|
||||
{ prop: 'transferOrderId', label: '转单号' },
|
||||
{ prop: 'trackId', label: '轨迹ID' },
|
||||
{ prop: 'orderStatusLabel', label: '订单状态' },
|
||||
{ prop: 'customerCode', label: '客户代码' },
|
||||
{ prop: 'customerName', label: '客户名称' },
|
||||
{ prop: 'productCode', label: '产品代码' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'productFloorCode', label: '产品底级代码' },
|
||||
{ prop: 'country', label: '国家代码' },
|
||||
{ prop: 'countryName', label: '国家/地区' },
|
||||
{ prop: 'consigneePostcode', label: '收件人邮编' },
|
||||
{ prop: 'confirmWeight', label: '客户确认重' },
|
||||
{ prop: 'dimensionalWeightFactor', label: '计费重' },
|
||||
{ prop: 'volumeWeight', label: '体积重' },
|
||||
{ prop: 'weight', label: '实重' },
|
||||
{ prop: 'pickupDate', label: '提货时间' }
|
||||
]
|
||||
|
||||
export const ORDER_BILL_EXPORT_FIELDS = [
|
||||
{ prop: 'orderNo', label: '订单号' },
|
||||
{ prop: 'trackingNo', label: '运单号' },
|
||||
{ prop: 'transferNo', label: '转单号' },
|
||||
{ prop: 'customerName', label: '客户' },
|
||||
{ prop: 'customerCode', label: '客户代码' },
|
||||
{ prop: 'productCode', label: '产品代码' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'countryCode', label: '国家代码' },
|
||||
{ prop: 'countryName', label: '国家/地区' },
|
||||
{ prop: 'pieces', label: '件数' },
|
||||
{ prop: 'weight', label: '计费重' },
|
||||
{ prop: 'volumeWeight', label: '体积重' },
|
||||
{ prop: 'receiveWeight', label: '实重' },
|
||||
{ prop: 'destPostcode', label: '收件人邮编' },
|
||||
{ prop: 'orderStatusLabel', label: '订单状态' },
|
||||
{ prop: 'billingFailStatusLabel', label: '计费状态' },
|
||||
{ prop: 'bizDate', label: '计费时间' },
|
||||
{ prop: 'orderTime', label: '下单时间' },
|
||||
{ prop: 'pickupDate', label: '提货时间' },
|
||||
{ prop: 'belongSaler', label: '销售员' },
|
||||
{ prop: 'settlementMan', label: '结算员' },
|
||||
{ prop: 'serviceMan', label: '客服员' }
|
||||
]
|
||||
|
||||
export const BILLING_BILL_EXPORT_FIELDS = [
|
||||
{ prop: 'orderNo', label: '订单号' },
|
||||
{ prop: 'trackingNo', label: '运单号' },
|
||||
{ prop: 'transferNo', label: '转单号' },
|
||||
{ prop: 'customerName', label: '客户' },
|
||||
{ prop: 'customerCode', label: '客户代码' },
|
||||
{ prop: 'productCode', label: '产品代码' },
|
||||
{ prop: 'productName', label: '产品名称' },
|
||||
{ prop: 'countryCode', label: '国家代码' },
|
||||
{ prop: 'countryName', label: '国家/地区' },
|
||||
{ prop: 'pieces', label: '件数' },
|
||||
{ prop: 'chargeWeight', label: '计费重' },
|
||||
{ prop: 'weight', label: '原计费重' },
|
||||
{ prop: 'receiveWeight', label: '收货重' },
|
||||
{ prop: 'incomeTotal', label: '应收总额' },
|
||||
{ prop: 'payTotal', label: '应付总额' },
|
||||
{ prop: 'matchSourceLabel', label: '计价来源' },
|
||||
{ prop: 'billingFailStatusLabel', label: '计费状态' },
|
||||
{ prop: 'customerActiveStatusLabel', label: '客户状态' },
|
||||
{ prop: 'creditArrearsStatusLabel', label: '额度欠费' },
|
||||
{ prop: 'bizDate', label: '计费时间' },
|
||||
{ prop: 'orderTime', label: '下单时间' },
|
||||
{ prop: 'belongSaler', label: '销售员' },
|
||||
{ prop: 'settlementMan', label: '结算员' },
|
||||
{ prop: 'serviceMan', label: '客服员' }
|
||||
]
|
||||
@@ -65,6 +65,15 @@
|
||||
@click="logDrawerRef.open()"
|
||||
>操作记录</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="Download"
|
||||
v-hasPermi="['monitor:orderSource:export']"
|
||||
@click="handleExport"
|
||||
>导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
@@ -131,10 +140,19 @@
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
<order-source-product-check-drawer ref="productCheckDrawerRef" />
|
||||
<order-source-compare-drawer ref="compareDrawerRef" @fill-search="onCompareFillSearch" />
|
||||
|
||||
<excel-export-field-dialog
|
||||
ref="exportFieldRef"
|
||||
:fields="ORDER_SOURCE_EXPORT_FIELDS"
|
||||
storage-key="exportFields:orderSource"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="OrderSource">
|
||||
import { exportAsync } from '@/utils/excelExport'
|
||||
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
|
||||
import { ORDER_SOURCE_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { listOrderSource, listOrderSourceByTransferNos, syncOrderSourceSelected } from '@/api/monitor/orderSource'
|
||||
import OrderSourceSyncDialog from './components/OrderSourceSyncDialog.vue'
|
||||
@@ -144,6 +162,7 @@ import OrderSourceCompareDrawer from './components/OrderSourceCompareDrawer.vue'
|
||||
|
||||
const { proxy } = getCurrentInstance()
|
||||
const tableRef = ref()
|
||||
const exportFieldRef = ref()
|
||||
const syncDialogRef = ref()
|
||||
const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
@@ -330,6 +349,32 @@ function buildFilterQuery(params) {
|
||||
return query
|
||||
}
|
||||
|
||||
|
||||
async function handleExport() {
|
||||
const q = buildFilterQuery()
|
||||
const identifierCount = (q.orderIdentifiers || []).length
|
||||
const hasIdentifier = identifierCount > 0
|
||||
const hasCustomer = !!(q.customerKeywords && q.customerKeywords.length) || !!(q.customerCodes && q.customerCodes.length)
|
||||
const hasTime = !!q.beginTime && !!q.endTime
|
||||
if (!hasIdentifier && !(hasCustomer && hasTime)) {
|
||||
ElMessage.warning('请先设置列表查询条件后再导出')
|
||||
return
|
||||
}
|
||||
if (identifierCount > MAX_ORDER_IDENTIFIERS) {
|
||||
ElMessage.warning(`单次最多导出 ${MAX_ORDER_IDENTIFIERS} 个单号,当前 ${identifierCount} 个`)
|
||||
return
|
||||
}
|
||||
const fields = await exportFieldRef.value?.open?.()
|
||||
if (!fields || !fields.length) return
|
||||
return exportAsync({
|
||||
baseUrl: '/monitor/orderSource/export',
|
||||
query: { ...q, exportFields: fields },
|
||||
requireFilter: false,
|
||||
filePrefix: 'order_source'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function queryList(params) {
|
||||
const src = params || tableRef.value?.getQueryParams?.() || {}
|
||||
const identifiers = splitLines(src.orderIdentifiers)
|
||||
|
||||
@@ -234,12 +234,20 @@
|
||||
<div class="import-progress-msg">{{ cwImportProgressMessage }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<excel-export-field-dialog
|
||||
ref="exportFieldRef"
|
||||
:fields="BILLING_BILL_EXPORT_FIELDS"
|
||||
storage-key="exportFields:billingBill"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="BillingBill">
|
||||
import { listBill, getBill, refundBill, updateCreditArrears, importChargeWeightAsync, downloadChargeWeightTemplate, recalcBillPriceAsync } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
|
||||
import { BILLING_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useProducts } from '@/components/ProductSelect/useProducts'
|
||||
import BillDetail from './components/BillDetail.vue'
|
||||
@@ -281,6 +289,7 @@ const productOptions = computed(() =>
|
||||
)
|
||||
|
||||
const tableRef = ref()
|
||||
const exportFieldRef = ref()
|
||||
const tableHeight = ref(480)
|
||||
let tableResizeObserver = null
|
||||
|
||||
@@ -442,18 +451,25 @@ function queryList(params) {
|
||||
return listBill(query)
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
async function handleExport() {
|
||||
const params = tableRef.value?.getQueryParams?.() || {}
|
||||
if (!validateOrderIdentifierLimit(params.orderIdentifiers)) return
|
||||
const fields = await exportFieldRef.value?.open?.()
|
||||
if (!fields || !fields.length) return
|
||||
return exportAsyncFromTable({
|
||||
baseUrl: '/price/billing/bill/export',
|
||||
tableRef: tableRef.value,
|
||||
normalize: buildBillQuery,
|
||||
normalize: (raw) => {
|
||||
const q = buildBillQuery(raw)
|
||||
q.exportFields = fields
|
||||
return q
|
||||
},
|
||||
filePrefix: 'billing_bill',
|
||||
requireFilter: true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function handleUpdateArrears(ids, selection) {
|
||||
const billIds = (ids && ids.length)
|
||||
? ids
|
||||
|
||||
@@ -188,6 +188,12 @@
|
||||
</el-dialog>
|
||||
<order-source-sync-log-drawer ref="logDrawerRef" />
|
||||
<order-bill-product-check-drawer ref="productCheckDrawerRef" />
|
||||
|
||||
<excel-export-field-dialog
|
||||
ref="exportFieldRef"
|
||||
:fields="ORDER_BILL_EXPORT_FIELDS"
|
||||
storage-key="exportFields:orderBill"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -195,10 +201,13 @@
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listOrderBill, localRebillAsync } from '@/api/price/billing'
|
||||
import { exportAsyncFromTable } from '@/utils/excelExport'
|
||||
import ExcelExportFieldDialog from '@/components/ExcelExportFieldDialog/index.vue'
|
||||
import { ORDER_BILL_EXPORT_FIELDS } from '@/utils/exportFieldDefs'
|
||||
import OrderSourceSyncLogDrawer from '../../monitor/orderSource/components/OrderSourceSyncLogDrawer.vue'
|
||||
import OrderBillProductCheckDrawer from './components/OrderBillProductCheckDrawer.vue'
|
||||
|
||||
const tableRef = ref()
|
||||
const exportFieldRef = ref()
|
||||
const logDrawerRef = ref()
|
||||
const productCheckDrawerRef = ref()
|
||||
const checkLoading = ref(false)
|
||||
@@ -417,19 +426,24 @@ function queryList(params) {
|
||||
return listOrderBill(query)
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
async function handleExport() {
|
||||
const params = tableRef.value?.getQueryParams?.() || {}
|
||||
if (!validateOrderIdentifierLimit(params.orderIdentifiers)) return
|
||||
const fields = await exportFieldRef.value?.open?.()
|
||||
if (!fields || !fields.length) return
|
||||
return exportAsyncFromTable({
|
||||
baseUrl: '/price/billing/orderBill/export',
|
||||
tableRef: tableRef.value,
|
||||
normalize: applySearchRanges,
|
||||
normalize: (raw) => {
|
||||
const q = applySearchRanges(raw)
|
||||
q.exportFields = fields
|
||||
return q
|
||||
},
|
||||
filePrefix: 'order_bill',
|
||||
requireFilter: true
|
||||
})
|
||||
}
|
||||
|
||||
const MAX_REBILL = 10000
|
||||
|
||||
function currentIdentifiers() {
|
||||
const q = tableRef.value?.getQueryParams?.() || {}
|
||||
|
||||
Reference in New Issue
Block a user