增加导出功能

This commit is contained in:
wuweihua
2026-10-26 16:20:46 +08:00
parent 7f4fc3df8c
commit 76cc5b5781
16 changed files with 801 additions and 10 deletions
@@ -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; }
}
@@ -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;
}
@@ -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())
@@ -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
);