feat(oms): 出库/入库业务单导入支持多页签——每个工作表独立生成一张单据

一、行为变更(ReservationStockInOrderImpl)
- 出库导入(parseExcelFile)/入库导入(parseStockInExcel):不再固定读单一页签,枚举Excel全部工作表,每个页签独立解析为一张单据(5个页签=5张单5个单号)
- 全部回滚策略:任一页签校验失败整体不落库,报错按页签聚合列出(页签[x]:原因;页签[y]:原因)
- 空页签(无货品/客户明细)自动跳过;单页签文件行为与原先完全一致
- 出库单号改到落库阶段逐张生成:多页签下校验阶段统一生成会因全部未入库而取到相同单号
- 入库模板OrderNO增加本文件内跨页签查重:各页签都未入库时库查重互相不可见,防两页签同OrderNO重复落库
- 页签解析异常message为空时兜底用异常类名,避免报错显示null
- 方法级@Transactional(rollbackFor=Exception.class):顺带修复旧入库导入因this自调用导致事务代理失效的问题

二、接口与返回结构(IReservationStockInOrderService / ReservationStockInOrderApi)
- 两个导入方法返回类型改为List<SheetImportResultDTO>(每页签一条:页签名/单号/物料行数/数量合计/解析数据)
- /parseExcel与/parse-excel返回:result=第一个页签解析数据(兼容老前端)、results=全部页签结果、msg=成功导入N张单:单号1、单号2…
- 入库接口不传sheetName为多页签模式,显式传页签名保持单页签老行为
This commit is contained in:
rcx
2026-09-04 10:55:18 +08:00
parent ce3bb036fa
commit 1b8511c092
4 changed files with 255 additions and 45 deletions
@@ -6,6 +6,7 @@ import com.mhd.oms.domain.reservationStockInOrder.entity.ReservationStockInOrder
import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.DeliveryDataDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.SheetImportResultDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.StockInDataDTO;
import org.springframework.web.multipart.MultipartFile;
@@ -50,21 +51,23 @@ public interface IReservationStockInOrderService extends IService<ReservationSto
List<ReservationInMaterialDetailPO> queryNoticePrintDetail(ReservationStockInOrderDO stockInOrderDTO);
/**
* 解析Excel文件并获取出库数据
* 解析Excel文件并导入出库单(多页签:文件中每个工作表独立解析为一张出库单,单页签文件行为不变)。
* 任一页签校验失败则整体不落库,报错聚合列出所有失败页签
* @param file Excel文件
* @param sheetName 工作表名称(出库导入/参考数据
* @param sheetName 兼容参数(多页签模式下不再按名字筛选,传空即可
* @param warehouseId 当前仓库id(非空时校验导入物料是否属于该仓库,并按该仓库过滤批次库存)
* @return 出库数据DTO
* @return 每个工作表的导入结果(含生成的出库单号与解析数据)
* @throws Exception 解析异常
*/
DeliveryDataDTO parseExcelFile(MultipartFile file, String sheetName, Long warehouseId) throws Exception;
List<SheetImportResultDTO> parseExcelFile(MultipartFile file, String sheetName, Long warehouseId) throws Exception;
/**
* 解析入库单Excel文件
* 解析并导入入库单Excel(多页签:未指定页签名时文件中每个工作表独立解析为一张入库单,单页签文件行为不变)。
* 任一页签校验失败则整体不落库,报错聚合列出所有失败页签
* @param file Excel文件
* @param sheetName 工作表名称(默认:入库模版)
* @return 入库单完整数据(表头+明细
* @param sheetName 显式指定页签时保持单页签行为;不传/为空则为多页签模式
* @return 每个工作表的导入结果(含生成的入库单号与解析数据
* @throws Exception 解析异常
*/
StockInDataDTO parseStockInExcel(MultipartFile file, String sheetName) throws Exception;
List<SheetImportResultDTO> parseStockInExcel(MultipartFile file, String sheetName) throws Exception;
}
@@ -4,6 +4,8 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelReader;
import com.alibaba.excel.read.metadata.ReadSheet;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -57,6 +59,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
@@ -220,19 +223,90 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
@Override
public DeliveryDataDTO parseExcelFile(MultipartFile file, String sheetName, Long warehouseId) throws Exception {
@Transactional(rollbackFor = Exception.class)
public List<SheetImportResultDTO> parseExcelFile(MultipartFile file, String sheetName, Long warehouseId) throws Exception {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 校验文件和工作表名称(逻辑不变)
// 校验文件(逻辑不变)
if (file.isEmpty()) {
throw new IllegalArgumentException("上传的Excel文件不能为空");
}
if (!"出库导入".equals(sheetName) && !"参考数据".equals(sheetName)) {
sheetName = "出库导入";
// 多页签模式:文件中每个工作表独立解析为一张出库单(老逻辑固定只读"出库导入"页签)
byte[] fileBytes = file.getBytes();
List<String> sheetNames = listSheetNames(fileBytes);
if (sheetNames.isEmpty()) {
throw new IllegalArgumentException("Excel文件中没有工作表");
}
// ====== 第一阶段:逐页签解析+校验+组装(不落库),失败按页签聚合收集,全部页签处理完统一报错 ======
List<PreparedDeliveryOrder> preparedOrders = new ArrayList<>();
List<String> sheetProblems = new ArrayList<>();
for (String name : sheetNames) {
try {
PreparedDeliveryOrder prepared = buildDeliveryOrder(fileBytes, name, warehouseId, loginUser);
if (prepared != null) {
preparedOrders.add(prepared); // null=空页签,跳过
}
} catch (Exception e) {
// 非模板页签的解析异常(如NPE)message 可能为空,兜底用异常类名,避免报错显示"页签[x]null"
sheetProblems.add("页签[" + name + "]" + (e.getMessage() == null ? e.toString() : e.getMessage()));
}
}
if (!sheetProblems.isEmpty()) {
throw new Exception(String.join("", sheetProblems));
}
if (preparedOrders.isEmpty()) {
throw new IllegalArgumentException("Excel中没有可导入的数据页签");
}
// ====== 第二阶段:全部页签校验通过后统一落库(同一事务,任一失败整体回滚) ======
List<SheetImportResultDTO> results = new ArrayList<>();
for (PreparedDeliveryOrder prepared : preparedOrders) {
// 出库单号在落库阶段逐张生成:此时上一张已入库(同事务内可见),取号递增不会重复;
// 校验阶段统一生成会因全部页签都未入库而取到相同号
String orderNumber = reservationStockOutOrderApplicationService.generateReserveOrderNumber();
ReservationStockOutOrderDO order = prepared.order;
order.setOutOrderNumber(orderNumber);
order.setBusinessOrderNo(orderNumber);
if (order.getMaterialDetailList() != null) {
for (ReservationOutMaterialDetailDO detail : order.getMaterialDetailList()) {
detail.setOutOrderNumber(orderNumber);
}
}
reservationStockOutOrderApplicationService.insert(order);
saveDeliveryLinks(prepared);
prepared.result.setOrderNumber(orderNumber);
results.add(prepared.result);
}
return results;
}
/**
* 枚举Excel全部工作表名(EasyExcel 3.xexcelExecutor().sheetList()
*/
private List<String> listSheetNames(byte[] fileBytes) {
List<String> sheetNames = new ArrayList<>();
try (ExcelReader reader = EasyExcel.read(new ByteArrayInputStream(fileBytes)).build()) {
for (ReadSheet readSheet : reader.excelExecutor().sheetList()) {
sheetNames.add(readSheet.getSheetName());
}
}
return sheetNames;
}
/** 出库单页签解析/校验/组装完成后的暂存容器(统一落库阶段使用) */
private static class PreparedDeliveryOrder {
ReservationStockOutOrderDO order;
List<CustomerDeliveryDetail> customerDeliveryDetailList;
Map<String, UserPo> customerCache;
SheetImportResultDTO result;
}
/**
* 单个页签的出库单解析+校验+组装(不落库,由调用方在全部页签通过后统一插入)。
* 空页签(货品明细与客户配送明细均为空)返回 null 由调用方跳过。
*/
private PreparedDeliveryOrder buildDeliveryOrder(byte[] fileBytes, String sheetName, Long warehouseId, LoginUser loginUser) throws Exception {
// 关键变更:使用动态行数监听器
DynamicDeliveryExcelListener listener = new DynamicDeliveryExcelListener();
try (InputStream inputStream = file.getInputStream()) {
try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) {
EasyExcel.read(inputStream, listener)
.sheet(sheetName)
.headRowNumber(0) // 无固定表头,全部行交由监听器处理
@@ -254,8 +328,17 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
deliveryDataDTO.setCustomerDeliveryDetailList(customerDeliveryDetailList);
LOGGER.info("Excel解析完成,货品明细{}条,客户配送明细{}条",
productDetailList.size(), customerDeliveryDetailList.size());
// 空页签跳过:货品明细与客户配送明细均为空时视为非数据页签(说明页/空表),不算失败
if (productDetailList.isEmpty() && customerDeliveryDetailList.isEmpty()) {
LOGGER.info("页签[{}]无货品明细与客户配送明细,跳过", sheetName);
return null;
}
if (productDetailList.isEmpty()) {
throw new Exception("未解析到产品明细数据,请检查模板中是否填写了產品編號");
}
LOGGER.info("页签[{}]解析完成,货品明细{}条,客户配送明细{}条",
sheetName, productDetailList.size(), customerDeliveryDetailList.size());
List<Map<String,Object>> shipperIds = new ArrayList<>();
Set<Long> processedShipperIds = new HashSet<>();
@@ -356,10 +439,8 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
ReservationStockOutOrderDO reservationStockOutOrderDO = new ReservationStockOutOrderDO();
// 生成出库业务单号
String orderNumber = reservationStockOutOrderApplicationService.generateReserveOrderNumber();
reservationStockOutOrderDO.setOutOrderNumber(orderNumber);
reservationStockOutOrderDO.setBusinessOrderNo(orderNumber);
// 出库业务单号不在校验阶段生成:多页签时全部页签落库前都未入库,逐张生成会取到相同号;
// 单号在统一落库阶段(第二阶段)逐张生成后回填单据/明细/结果
// 货主信息:有货品明细时从解析结果取,无货品明细时从登录用户取
if (!shipperIds.isEmpty()) {
reservationStockOutOrderDO.setShipperId((Long) shipperIds.get(0).get("shipperId"));
@@ -480,10 +561,31 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationOutMaterialDetailDOList.add(reservationOutMaterialDetailDO);
}
reservationStockOutOrderDO.setMaterialDetailList(reservationOutMaterialDetailDOList);
reservationStockOutOrderApplicationService.insert(reservationStockOutOrderDO);
// 组装完成暂存不落库:由调用方在全部页签校验通过后统一插入(整体事务,任一页签失败全部回滚)
PreparedDeliveryOrder prepared = new PreparedDeliveryOrder();
prepared.order = reservationStockOutOrderDO;
prepared.customerDeliveryDetailList = customerDeliveryDetailList;
prepared.customerCache = customerCache;
SheetImportResultDTO result = new SheetImportResultDTO();
result.setSheetName(sheetName);
result.setMaterialQuantity(productDetailList.size());
result.setQuantity(reservationOutMaterialDetailDOList.stream()
.map(ReservationOutMaterialDetailDO::getQuantity).filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));
result.setDeliveryData(deliveryDataDTO);
prepared.result = result;
return prepared;
}
/**
* 客户配送关联落库(原出库导入方法内的保存段,挪到统一落库阶段逐页签执行)
*/
private void saveDeliveryLinks(PreparedDeliveryOrder prepared) {
ReservationStockOutOrderDO reservationStockOutOrderDO = prepared.order;
Map<String, UserPo> customerCache = prepared.customerCache;
// 客户配送明细判空处理:为空时跳过关联保存
for (CustomerDeliveryDetail customerDeliveryDetail : customerDeliveryDetailList) {
for (CustomerDeliveryDetail customerDeliveryDetail : prepared.customerDeliveryDetailList) {
ReservationDeliveryDetailsLink reservationDeliveryDetailsLink = new ReservationDeliveryDetailsLink();
reservationDeliveryDetailsLink.setInvoiceNumber(customerDeliveryDetail.getInvoiceNo());
reservationDeliveryDetailsLink.setAmount(customerDeliveryDetail.getAmount());
@@ -507,7 +609,6 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
reservationDeliveryDetailsLinkService.save(reservationDeliveryDetailsLink);
}
return deliveryDataDTO;
}
public String generateDateSerialNumber() {
@@ -525,11 +626,10 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
}
private static final Logger LOGGER = LoggerFactory.getLogger(ReservationStockInOrderImpl.class);
// 默认工作表名称(与Excel一致)
private static final String DEFAULT_SHEET_NAME = "入库模版";
@Override
public StockInDataDTO parseStockInExcel(MultipartFile file, String sheetName) throws Exception {
@Transactional(rollbackFor = Exception.class)
public List<SheetImportResultDTO> parseStockInExcel(MultipartFile file, String sheetName) throws Exception {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 1. 校验文件
if (file.isEmpty()) {
@@ -537,29 +637,78 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
throw new IllegalArgumentException("Excel文件不能为空");
}
// 2. 校验工作表名称(默认使用"入库模版")
String targetSheetName = (sheetName == null || sheetName.trim().isEmpty())
? DEFAULT_SHEET_NAME : sheetName.trim();
// 2. 多页签模式:未显式指定页签名时,文件中每个工作表独立解析为一张入库单;显式指定则保持单页签行为
byte[] fileBytes = file.getBytes();
List<String> sheetNames;
if (sheetName != null && !sheetName.trim().isEmpty()) {
sheetNames = Collections.singletonList(sheetName.trim());
} else {
sheetNames = listSheetNames(fileBytes);
}
// 3. 使用EasyExcel解析(指定监听器)
// ====== 第一阶段:逐页签解析+校验+组装(不落库),失败按页签聚合收集,全部页签处理完统一报错 ======
List<PreparedStockInOrder> preparedOrders = new ArrayList<>();
List<String> sheetProblems = new ArrayList<>();
// 本文件内已使用的模板OrderNO:各页签都未入库时 existsByBusinessOrderNo 查不到彼此,需按页签追加查重防跨页签重复
Set<String> usedBusinessOrderNos = new HashSet<>();
for (String name : sheetNames) {
try {
PreparedStockInOrder prepared = buildStockInOrder(fileBytes, name, loginUser, usedBusinessOrderNos);
if (prepared != null) {
preparedOrders.add(prepared); // null=空页签,跳过
}
} catch (Exception e) {
sheetProblems.add("页签[" + name + "]" + (e.getMessage() == null ? e.toString() : e.getMessage()));
}
}
if (!sheetProblems.isEmpty()) {
throw new Exception(String.join("", sheetProblems));
}
if (preparedOrders.isEmpty()) {
throw new IllegalArgumentException("未解析到产品明细数据,请检查模板中是否填写了產品編號");
}
// ====== 第二阶段:全部页签校验通过后统一落库(同一事务,任一失败整体回滚) ======
List<SheetImportResultDTO> results = new ArrayList<>();
for (PreparedStockInOrder prepared : preparedOrders) {
insertApplication(prepared.order);
// 入库单号在落库阶段生成(insertDomain),回填到页签结果
prepared.result.setOrderNumber(prepared.order.getInOrderNumber());
results.add(prepared.result);
}
return results;
}
/** 入库单页签解析/校验/组装完成后的暂存容器(统一落库阶段使用) */
private static class PreparedStockInOrder {
ReservationStockInOrderDO order;
SheetImportResultDTO result;
}
/**
* 单个页签的入库单解析+校验+组装(不落库,由调用方在全部页签通过后统一插入)。
* 空页签(未解析到产品明细)返回 null 由调用方跳过。
*/
private PreparedStockInOrder buildStockInOrder(byte[] fileBytes, String sheetName, LoginUser loginUser, Set<String> usedBusinessOrderNos) throws Exception {
// 3. 使用EasyExcel解析(指定监听器,多页签模式下 sheetName 为实际页签名)
StockInExcelListener listener = new StockInExcelListener();
try (InputStream inputStream = file.getInputStream()) {
try (InputStream inputStream = new ByteArrayInputStream(fileBytes)) {
EasyExcel.read(inputStream, listener)
.sheet(targetSheetName) // 指定工作表
.sheet(sheetName) // 指定工作表
.headRowNumber(0) // 从第1行开始解析(不跳过任何行)
.doRead();
}
// 4. 封装DTO返回
// 4. 封装DTO
StockInDataDTO resultDTO = new StockInDataDTO();
resultDTO.setStockInHeader(listener.getStockInHeader());
resultDTO.setProductDetailList(listener.getProductDetailList());
LOGGER.info("Excel解析成功,工作表:{},产品明细数:{}",
targetSheetName, resultDTO.getProductDetailList().size());
LOGGER.info("页签[{}]解析成功,产品明细数:{}", sheetName, resultDTO.getProductDetailList().size());
List<StockInProductDetail> productDetailList = resultDTO.getProductDetailList();
// 空页签跳过:该页签未解析到产品明细时视为非数据页签(说明页/空表),不算失败
if (productDetailList == null || productDetailList.isEmpty()) {
throw new ServiceException("未解析到产品明细数据,请检查模板中是否填写了產品編號");
return null;
}
StockInHeader stockInHeader = resultDTO.getStockInHeader();
// ====== 第一步:物料编号存在性校验(模板只填物料编号即可,名称按编号自动匹配) ======
@@ -664,6 +813,10 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
if (stockInOrderMapper.existsByBusinessOrderNo(templateOrderNo) > 0) {
throw new ServiceException("业务单号已存在:" + templateOrderNo + ",请修改模板中的OrderNO后重新导入");
}
// 多页签查重:各页签都未入库时上面的库查重互相看不见,需对本文件内已使用的OrderNO追加拦截
if (!usedBusinessOrderNos.add(templateOrderNo)) {
throw new ServiceException("业务单号已存在:" + templateOrderNo + "(与本文件其他页签的OrderNO重复),请修改模板中的OrderNO后重新导入");
}
reservationStockInOrderDO.setBusinessOrderNo(templateOrderNo);
}
reservationStockInOrderDO.setOrganizationId(loginUser.getUserPo().getOrganizationId());
@@ -740,8 +893,19 @@ public class ReservationStockInOrderImpl extends ServiceImpl<ReservationStockInO
reservationInMaterialDetailDOList.add(reservationInMaterialDetailDO);
}
reservationStockInOrderDO.setMaterialDetailList(reservationInMaterialDetailDOList);
insertApplication(reservationStockInOrderDO);
return resultDTO;
// 组装完成暂存不落库:由调用方在全部页签校验通过后统一插入(整体事务,任一页签失败全部回滚)
SheetImportResultDTO sheetResult = new SheetImportResultDTO();
sheetResult.setSheetName(sheetName);
sheetResult.setMaterialQuantity(productDetailList.size());
sheetResult.setQuantity(reservationInMaterialDetailDOList.stream()
.map(ReservationInMaterialDetailDO::getQuantity).filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));
sheetResult.setStockInData(resultDTO);
PreparedStockInOrder prepared = new PreparedStockInOrder();
prepared.order = reservationStockInOrderDO;
prepared.result = sheetResult;
return prepared;
}
public Boolean insertApplication(ReservationStockInOrderDO stockInOrderDO) {
@@ -0,0 +1,31 @@
package com.mhd.oms.domain.reservationStockInOrder.repository.vo;
import lombok.Data;
import java.math.BigDecimal;
/**
* 多页签导入的单页签结果(出库/入库导入通用)
* 一个Excel文件中每个工作表独立解析为一张单据,每个页签对应一条结果
*/
@Data
public class SheetImportResultDTO {
/** 来源工作表名 */
private String sheetName;
/** 生成的单据号(出库=出库业务单号,入库=入库业务单号;入库单号在落库阶段生成后回填) */
private String orderNumber;
/** 物料明细行数 */
private Integer materialQuantity;
/** 明细数量合计 */
private BigDecimal quantity;
/** 出库单解析数据(仅出库导入时有值) */
private DeliveryDataDTO deliveryData;
/** 入库单解析数据(仅入库导入时有值) */
private StockInDataDTO stockInData;
}
@@ -9,6 +9,7 @@ import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStock
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO;
import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.DeliveryDataDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.SheetImportResultDTO;
import com.mhd.oms.domain.reservationStockInOrder.repository.vo.StockInDataDTO;
import com.mhd.oms.domain.reservationStockInOrder.service.ReservationStockInOrderApplicationService;
import com.mhd.oms.domain.reservationStockInOrder.service.ReservationStockInOrderAssembler;
@@ -220,10 +221,16 @@ public class ReservationStockInOrderApi extends BaseController {
@RequestParam(value = "warehouseId", required = false) Long warehouseId) {
Map<String, Object> result = new HashMap<>();
try {
DeliveryDataDTO deliveryDataDTO = reservationStockInOrderService.parseExcelFile(file, "", warehouseId);
result.put("result", deliveryDataDTO);
// 多页签导入:Excel中每个工作表独立生成一张出库单,任一页签校验失败整体不落库
List<SheetImportResultDTO> results = reservationStockInOrderService.parseExcelFile(file, "", warehouseId);
// result 保留第一个页签的解析数据(兼容老前端);results 为全部页签结果
result.put("result", results.isEmpty() ? null : results.get(0).getDeliveryData());
result.put("results", results);
result.put("code", 200);
result.put("msg", "解析成功");
String orderNumbers = results.stream()
.map(SheetImportResultDTO::getOrderNumber)
.collect(java.util.stream.Collectors.joining(""));
result.put("msg", "成功导入" + results.size() + "张出库单:" + orderNumbers);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
result.put("code", 400);
@@ -237,18 +244,23 @@ public class ReservationStockInOrderApi extends BaseController {
}
/**
* 接口1解析Excel文件(返回解析结果,不保存
* 用途:预览Excel解析数据
* 接口1:解析并导入入库单Excel(多页签:不传sheetName时每个工作表独立生成一张入库单,任一页签校验失败整体不落库
*/
@PostMapping("/parse-excel")
public ResponseEntity<Map<String, Object>> parseStockInExcel(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "sheetName", required = false) String sheetName) {
try {
StockInDataDTO resultDTO = reservationStockInOrderService.parseStockInExcel(file, sheetName);
List<SheetImportResultDTO> results = reservationStockInOrderService.parseStockInExcel(file, sheetName);
Map<String, Object> result = new HashMap<>();
result.put("result", resultDTO);
result.put("code",200);
// result 保留第一个页签的解析数据(兼容老前端);results 为全部页签结果
result.put("result", results.isEmpty() ? null : results.get(0).getStockInData());
result.put("results", results);
result.put("code", 200);
String orderNumbers = results.stream()
.map(SheetImportResultDTO::getOrderNumber)
.collect(java.util.stream.Collectors.joining(""));
result.put("msg", "成功导入" + results.size() + "张入库单:" + orderNumbers);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
// 参数错误(如文件为空)