package com.hvlink.service.impl;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import com.hvlink.common.Result;
import com.hvlink.entity.dto.master.PackageExportDTO;
import com.hvlink.entity.dto.master.PackageImportDTO;
import com.hvlink.entity.function.PackingQuantity;
import com.hvlink.entity.param.master.PackageParam;
import com.hvlink.entity.po.master.CompanyPO;
import com.hvlink.entity.po.master.FactoryPO;
import com.hvlink.entity.po.master.PackagePO;
import com.hvlink.entity.po.master.PartPO;
import com.hvlink.entity.vo.master.ImportErrorVO;
import com.hvlink.entity.vo.master.PackageImportResultVO;
import com.hvlink.entity.vo.master.PackageVO;
import com.hvlink.enums.ResultCode;
import com.hvlink.exceptions.BusinessException;
import com.hvlink.mapper.master.CompanyMapper;
import com.hvlink.mapper.master.FactoryMapper;
import com.hvlink.mapper.master.PackageMapper;
import com.hvlink.mapper.master.PartMapper;
import com.hvlink.pagination.PageResult;
import com.hvlink.service.IPackageService;
import com.hvlink.service.constant.RfcSyncConstant;
import com.hvlink.utils.BeanCopyUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Mono;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 包装数量主数据(Package)表服务实现类
*
* @author sunzhuang
* @since 2025-09-10 21:54:19
*/
@Slf4j
@RequiredArgsConstructor
@Service("packageService")
public class PackageServiceImpl extends ServiceImpl<PackageMapper, PackagePO> implements IPackageService {
private final PackageMapper packageMapper;
private final CompanyMapper companyMapper;
private final FactoryMapper factoryMapper;
private final PartMapper partMapper;
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
@Override
public PackageVO selectById(Integer id) {
PackagePO po = packageMapper.selectById(id);
if (Objects.isNull(po)) {
throw new BusinessException(ResultCode.PARAM_ERROR);
}
return BeanCopyUtils.copyBean(po, PackageVO.class);
}
/**
* 修改数据
*
* @param param 更新参数
*/
@Override
public void update(PackageParam param) {
PackagePO po = BeanCopyUtils.copyBean(param, PackagePO.class);
packageMapper.updateById(po);
}
/**
* 分页查询
*
* @param param 查询参数
* @return 物料包装数
*/
@Override
public PageResult<PackageVO> selectPage(PackageParam param) {
PageResult<PackageVO> pageResult = new PageResult<>();
LambdaQueryWrapper<PackagePO> wrapper = Wrappers.<PackagePO>lambdaQuery()
.eq(StringUtils.isNotBlank(param.getCompanyCode()), PackagePO::getCompanyCode, param.getCompanyCode())
.eq(StringUtils.isNotBlank(param.getFactoryCode()), PackagePO::getFactoryCode, param.getFactoryCode())
.eq(StringUtils.isNotBlank(param.getPartCode()), PackagePO::getFactoryCode, param.getFactoryCode())
.orderByDesc(PackagePO::getCreateTime);
Page<PackagePO> poPage = packageMapper.selectPage(new Page<>(param.getPageIndex(), param.getPageSize()), wrapper);
if (poPage.getTotal() > 0) {
// 查询所有公司
Map<String, CompanyPO> companyMap = companyMapper.selectList(Wrappers.emptyWrapper()).stream().collect(Collectors.toMap(CompanyPO::getCompanyCode, Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(CompanyPO::getCreateTime))));
Map<String, FactoryPO> factoryMap = factoryMapper.selectList(Wrappers.emptyWrapper()).stream().collect(Collectors.toMap(FactoryPO::getFactoryCode, Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(FactoryPO::getCreateTime))));
pageResult.setTotal(poPage.getTotal());
pageResult.setPageIndex(poPage.getCurrent());
pageResult.setPageSize(poPage.getSize());
List<PackageVO> voList = Lists.newArrayList();
poPage.getRecords().forEach(po -> {
PackageVO vo = BeanCopyUtils.copyBean(po, PackageVO.class);
CompanyPO companyPO = companyMap.get(po.getCompanyCode());
if (Objects.nonNull(companyPO)) {
vo.setCompanyCode(companyPO.getCompanyCode() + "-" + companyPO.getCompanyName());
}
FactoryPO factoryPO = factoryMap.get(po.getFactoryCode());
if (Objects.nonNull(factoryPO)) {
vo.setFactoryCode(factoryPO.getFactoryCode());
vo.setFactoryName(factoryPO.getFactoryName());
}
voList.add(vo);
});
pageResult.setRecords(voList);
}
return pageResult;
}
/**
* 处理包装数量数据
*
* @param list 同步的包装数据集合
* @return 本次更新时间
*/
@Override
@Transactional(rollbackFor = Exception.class)
public String handlePackingQuantityData(List<PackingQuantity> list) {
if (CollectionUtils.isEmpty(list)) {
log.info("不存在更新的包装信息");
}
//查询数据库中的所有的包装信息
List<PackagePO> poList = packageMapper.selectPackageKey();
// 按照公司代码 + 工厂代码 + 物料编码 分组
Map<String, Integer> warehouseMap = poList.stream().collect(Collectors.toMap(x -> x.getCompanyCode() + x.getFactoryCode() + x.getPartCode(),
PackagePO::getId, (v1, v2) -> v1));
// 新增数据集合
List<PackagePO> insertList = Lists.newArrayList();
// 更新数据集合
List<PackagePO> updateList = Lists.newArrayList();
list.forEach(x -> {
// 构建主键
String key = x.getCompanyCode() + x.getFactoryCode() + x.getPartCode();
PackagePO po = BeanCopyUtils.copyBean(x, PackagePO.class);
if (warehouseMap.containsKey(key)) {
po.setId(warehouseMap.get(key));
po.setUpdateBy(RfcSyncConstant.DEFAULT_RFC_UPDATE_BY);
updateList.add(po);
} else {
po.setUpdateBy(RfcSyncConstant.DEFAULT_RFC_UPDATE_BY);
po.setCreateBy(RfcSyncConstant.DEFAULT_RFC_UPDATE_BY);
insertList.add(po);
}
});
// 新增
if (CollectionUtils.isNotEmpty(insertList)) {
this.saveBatch(insertList, 500);
}
if (CollectionUtils.isNotEmpty(updateList)) {
this.updateBatchById(updateList, 500);
}
return DateFormatUtils.ISO_8601_EXTENDED_DATE_FORMAT.format(new Date());
}
/**
* 批量导出Excel
*/
@Override
public Mono<Void> exportPackageExcel(List<Integer> selectedIds, ServerHttpResponse response) {
return Mono.fromCallable(() -> {
setupResponse(response, "包装数据导出");
return selectedIds;
})
.flatMap(ids -> {
try {
// 查询数据
List<PackagePO> packageList = this.listByIds(ids);
// 转换为导出DTO
List<PackageExportDTO> exportData = packageList.stream()
.map(po -> BeanCopyUtils.copyBean(po, PackageExportDTO.class))
.collect(Collectors.toList());
// 生成Excel数据
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
EasyExcel.write(outputStream, PackageExportDTO.class)
.sheet("包装数据")
.doWrite(exportData);
byte[] data = outputStream.toByteArray();
DataBuffer buffer = response.bufferFactory().wrap(data);
return response.writeWith(Mono.just(buffer));
} catch (Exception e) {
log.error("包装数据导出失败", e);
return Mono.error(new RuntimeException("导出Excel失败", e));
}
})
.doOnSuccess(v -> log.info("包装数据导出成功"))
.doOnError(e -> log.error("包装数据导出失败", e));
}
/**
* 下载导入模板
*/
@Override
public Mono<Void> downloadTemplate(ServerHttpResponse response) {
return Mono.fromCallable(() -> {
setupResponse(response, "包装数据导入模板");
return new ArrayList<PackageImportDTO>();
})
.flatMap(templateData -> {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 创建空的模板数据
EasyExcel.write(outputStream, PackageImportDTO.class)
.sheet("包装数据导入模板")
.doWrite(templateData);
byte[] data = outputStream.toByteArray();
DataBuffer buffer = response.bufferFactory().wrap(data);
return response.writeWith(Mono.just(buffer));
} catch (Exception e) {
log.error("模板下载失败", e);
return Mono.error(new RuntimeException("模板下载失败", e));
}
})
.doOnSuccess(v -> log.info("包装数据导入模板下载成功"))
.doOnError(e -> log.error("包装数据导入模板下载失败", e));
}
/**
* 导入包装数据
*/
@Override
public Mono<Result<PackageImportResultVO>> importPackage(FilePart filePart) {
return Mono.defer(() -> {
// 验证文件
if (filePart == null) {
return Mono.just(Result.<PackageImportResultVO>fail("导入文件不能为空"));
}
String fileName = filePart.filename();
if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {
return Mono.just(Result.<PackageImportResultVO>fail("请上传Excel文件"));
}
log.info("开始处理包装数据文件导入: {}", fileName);
try {
Path tempDir = Files.createTempDirectory("package_import");
Path tempFile = tempDir.resolve(System.currentTimeMillis() + "_" + fileName);
return filePart.transferTo(tempFile)
.then(Mono.fromCallable(() -> {
try {
File file = tempFile.toFile();
Result<PackageImportResultVO> importResult = processPackageExcelImport(file);
log.info("包装数据文件导入完成: {}, 结果: {}", fileName, importResult.getMsg());
return importResult;
} finally {
// 清理临时文件
try {
Files.deleteIfExists(tempFile);
Files.deleteIfExists(tempDir);
} catch (Exception e) {
log.warn("清理临时文件失败: {}", e.getMessage());
}
}
}));
} catch (Exception e) {
log.error("创建临时文件失败", e);
return Mono.just(Result.<PackageImportResultVO>fail("文件处理失败: " + e.getMessage()));
}
})
.onErrorResume(e -> {
log.error("包装数据导入异常", e);
return Mono.just(Result.<PackageImportResultVO>fail("包装数据导入异常: " + e.getMessage()));
});
}
/**
* 处理包装数据Excel导入逻辑
*/
private Result<PackageImportResultVO> processPackageExcelImport(File file) {
PackageImportResultVO result = new PackageImportResultVO();
List<ImportErrorVO> errorList = new ArrayList<>();
try (InputStream inputStream = new FileInputStream(file)) {
// 读取Excel数据
List<PackageImportDTO> importData = new ArrayList<>();
List<PackageImportDTO> validData = new ArrayList<>();
EasyExcel.read(inputStream, PackageImportDTO.class, new AnalysisEventListener<PackageImportDTO>() {
private final List<PackageImportDTO> cachedDataList = new ArrayList<>();
@Override
public void invoke(PackageImportDTO data, AnalysisContext context) {
int rowIndex = context.readRowHolder().getRowIndex() + 1;
// 跳过空行
if (StringUtils.isBlank(data.getCompanyCode()) &&
StringUtils.isBlank(data.getFactoryCode()) &&
StringUtils.isBlank(data.getPartCode())) {
return;
}
// 数据校验
String errorMsg = validatePackageImportData(data, rowIndex);
if (StringUtils.isNotBlank(errorMsg)) {
ImportErrorVO error = new ImportErrorVO();
error.setRowNum(rowIndex);
error.setErrorMsg(errorMsg);
error.setPartCode(data.getPartCode());
errorList.add(error);
} else {
data.setRowNum(rowIndex);
cachedDataList.add(data);
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
importData.addAll(cachedDataList);
validData.addAll(cachedDataList);
}
@Override
public void onException(Exception exception, AnalysisContext context) throws Exception {
log.error("Excel读取异常,行号: {}", context.readRowHolder().getRowIndex(), exception);
throw exception;
}
}).sheet().doRead();
// 如果有错误数据,终止导入
if (!errorList.isEmpty()) {
result.setTotalCount(importData.size() + errorList.size());
result.setSuccessCount(0);
result.setFailCount(errorList.size());
result.setErrorList(errorList);
result.setMessage("导入失败:存在错误数据,请修正后重新导入");
return Result.fail("导入失败:存在错误数据");
}
// 处理有效数据
if (!validData.isEmpty()) {
processPackageImportData(validData, errorList);
}
// 设置返回结果
result.setTotalCount(importData.size());
result.setSuccessCount(validData.size() - errorList.size());
result.setFailCount(errorList.size());
result.setErrorList(errorList);
result.setMessage(String.format("导入完成:成功%d条,失败%d条",
result.getSuccessCount(), result.getFailCount()));
log.info("包装数据导入完成:总数={}, 成功={}, 失败={}",
result.getTotalCount(), result.getSuccessCount(), result.getFailCount());
return Result.success(result);
} catch (Exception e) {
log.error("处理Excel文件异常", e);
return Result.fail("处理Excel文件失败: " + e.getMessage());
}
}
/**
* 验证包装数据导入数据
*/
private String validatePackageImportData(PackageImportDTO data, int rowNum) {
StringBuilder errorMsg = new StringBuilder();
// 必填字段校验
if (StringUtils.isBlank(data.getCompanyCode())) {
errorMsg.append("公司代码不能为空; ");
}
if (StringUtils.isBlank(data.getFactoryCode())) {
errorMsg.append("工厂代码不能为空; ");
} else {
// 工厂校验
FactoryPO factory = factoryMapper.selectOne(
Wrappers.<FactoryPO>lambdaQuery()
.eq(FactoryPO::getFactoryCode, data.getFactoryCode())
.eq(FactoryPO::getIsDeleted, 0)
);
if (factory == null) {
errorMsg.append("工厂代码不存在; ");
} else if ("已禁用".equals(factory.getStatus())) {
errorMsg.append("工厂代码已禁用; ");
}
}
if (StringUtils.isBlank(data.getPartCode())) {
errorMsg.append("零件号不能为空; ");
} else {
// 零件校验
Long partCount = partMapper.selectCount(
Wrappers.<PartPO>lambdaQuery()
.eq(PartPO::getPartCode, data.getPartCode())
.eq(PartPO::getIsDeleted, 0)
);
if (partCount == 0) {
errorMsg.append("零件号不存在; ");
}
}
// 数值校验
if (data.getBoxQuantity() != null && data.getBoxQuantity() < 0) {
errorMsg.append("箱数量不能小于0; ");
}
if (data.getPackageQuantity() != null && data.getPackageQuantity() < 0) {
errorMsg.append("包数量不能小于0; ");
}
if (data.getDragQuantity() != null && data.getDragQuantity() < 0) {
errorMsg.append("托数量不能小于0; ");
}
// 重复性校验
if (StringUtils.isNotBlank(data.getCompanyCode()) &&
StringUtils.isNotBlank(data.getFactoryCode()) &&
StringUtils.isNotBlank(data.getPartCode())) {
Long count = this.baseMapper.selectCount(
Wrappers.<PackagePO>lambdaQuery()
.eq(PackagePO::getCompanyCode, data.getCompanyCode())
.eq(PackagePO::getFactoryCode, data.getFactoryCode())
.eq(PackagePO::getPartCode, data.getPartCode())
.eq(PackagePO::getIsDeleted, 0)
);
if (count > 0) {
errorMsg.append("公司代码+工厂代码+零件号组合已存在; ");
}
}
return errorMsg.length() > 0 ? errorMsg.toString() : null;
}
/**
* 处理包装数据导入
*/
private void processPackageImportData(List<PackageImportDTO> validData, List<ImportErrorVO> errorList) {
String currentUser = "import_user";
for (PackageImportDTO dto : validData) {
try {
// 转换为PO
PackagePO packagePO = BeanCopyUtils.copyBean(dto, PackagePO.class);
packagePO.setCreateBy(currentUser);
packagePO.setUpdateBy(currentUser);
// 保存到数据库
this.baseMapper.insert(packagePO);
log.info("包装数据导入成功 - 公司代码: {}, 工厂代码: {}, 零件号: {}",
dto.getCompanyCode(), dto.getFactoryCode(), dto.getPartCode());
} catch (Exception e) {
log.error("包装数据导入失败 - 公司代码: {}, 工厂代码: {}, 零件号: {}",
dto.getCompanyCode(), dto.getFactoryCode(), dto.getPartCode(), e);
ImportErrorVO error = new ImportErrorVO();
error.setRowNum(dto.getRowNum());
error.setErrorMsg("系统错误: " + e.getMessage());
error.setPartCode(dto.getPartCode());
errorList.add(error);
}
}
}
/**
* 设置HTTP响应头
*/
private void setupResponse(ServerHttpResponse response, String filePrefix) {
try {
response.getHeaders().setContentType(MediaType.APPLICATION_OCTET_STREAM);
String fileName = URLEncoder.encode(filePrefix + "_" +
LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")), "UTF-8");
String contentDisposition = "attachment; filename=" + fileName + ".xlsx";
response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
response.getHeaders().set(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate");
response.getHeaders().set(HttpHeaders.PRAGMA, "no-cache");
response.getHeaders().set(HttpHeaders.EXPIRES, "0");
} catch (Exception e) {
log.error("响应头设置失败", e);
throw new RuntimeException("响应头设置失败", e);
}
}
}
加个校验,如果表头对不上,直接不执行导入,提示导入模板错误