package cn.iocoder.yudao.module.mes.service.pro.route; import cn.hutool.core.util.StrUtil; import cn.idev.excel.ExcelWriter; import cn.idev.excel.FastExcelFactory; import cn.idev.excel.annotation.ExcelProperty; import cn.idev.excel.write.handler.SheetWriteHandler; import cn.idev.excel.write.metadata.holder.WriteSheetHolder; import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteFullExcelVO; import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteWorkbookVO; import jakarta.servlet.http.HttpServletResponse; import jakarta.annotation.Resource; import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper; import cn.iocoder.yudao.module.mes.dal.mysql.pro.process.MesProProcessMapper; import org.apache.poi.ss.usermodel.DataFormatter; import org.apache.poi.ss.usermodel.WorkbookFactory; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.io.InputStream; import java.util.*; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_ROUTE_FULL_EXCEL_INVALID; /** 面向用户的分表格式与既有工艺路线导入模型之间的转换。 */ @Service public class MesProRouteWorkbookService { @Resource private MesMdItemMapper itemMapper; @Resource private MesProProcessMapper processMapper; private static final String BASIC = "基本信息"; private static final String PRODUCT = "产品与仓位"; private static final String PROCESS = "工序"; private static final String OUTPUT = "工序产出"; private static final String MATERIAL = "工序投料"; private static final String CURRENT = "本产品"; private static final String COMMON = "通用"; public void write(HttpServletResponse response, List rows) throws IOException { List basic = new ArrayList<>(); List products = new ArrayList<>(); List processes = new ArrayList<>(); List outputs = new ArrayList<>(); List materials = new ArrayList<>(); String referenceCode = rows.stream().filter(r -> "产品".equals(r.getRecordType()) && "是".equals(r.getTemplateReference())) .map(MesProRouteFullExcelVO::getProductCode).findFirst().orElse(null); for (var row : rows) { switch (row.getRecordType()) { case "路线" -> { var item = BeanUtils.toBean(row, MesProRouteWorkbookVO.Basic.class); item.setGuide("复制新产品:在产品与仓位表修改模板产品编码"); basic.add(item); } case "产品" -> { var item = BeanUtils.toBean(row, MesProRouteWorkbookVO.Product.class); item.setProductName(itemName(row.getProductCode(), referenceCode)); products.add(item); } case "工序" -> { var item = new MesProRouteWorkbookVO.Process(); item.setProcessCode(row.getProcessCode()); item.setProcessName(processName(row.getProcessCode())); item.setProcessSort(row.getProcessSort()); item.setKeyFlag(row.getKeyFlag()); item.setCheckFlag(row.getCheckFlag()); item.setBackflushFlag(row.getBackflushFlag()); item.setPrepareTime(row.getPrepareTime()); item.setWaitTime(row.getWaitTime()); item.setLinkType(linkName(row.getLinkType())); item.setColorCode(row.getColorCode()); item.setProcessRemark(row.getProcessRemark()); processes.add(item); } case "产出" -> { var item = BeanUtils.toBean(row, MesProRouteWorkbookVO.Output.class); item.setProcessName(processName(row.getProcessCode())); item.setItemName(itemName(row.getItemCode(), referenceCode)); item.setProductCode(toDisplayOwner(row.getProductCode())); item.setItemCode(toDisplayItem(row.getItemCode())); outputs.add(item); } case "投料" -> { var item = BeanUtils.toBean(row, MesProRouteWorkbookVO.Material.class); item.setProcessName(processName(row.getProcessCode())); item.setItemName(itemName(row.getItemCode(), referenceCode)); item.setProductCode(toDisplayOwner(row.getProductCode())); materials.add(item); } default -> throw exception(PRO_ROUTE_FULL_EXCEL_INVALID, "未知记录类型:" + row.getRecordType()); } } SheetWriteHandler sheetHandler = new SheetWriteHandler() { @Override public void afterSheetCreate(WriteWorkbookHolder workbook, WriteSheetHolder sheet) { sheet.getSheet().createFreezePane(0, 1); sheet.getSheet().setDefaultColumnWidth(20); if (BASIC.equals(sheet.getSheetName())) sheet.getSheet().setColumnWidth(4, 55 * 256); } }; ExcelWriter writer = FastExcelFactory.write(response.getOutputStream()).autoCloseStream(false) .registerWriteHandler(sheetHandler).build(); try { writer.write(basic, FastExcelFactory.writerSheet(0, BASIC).head(MesProRouteWorkbookVO.Basic.class).build()); writer.write(products, FastExcelFactory.writerSheet(1, PRODUCT).head(MesProRouteWorkbookVO.Product.class).build()); writer.write(processes, FastExcelFactory.writerSheet(2, PROCESS).head(MesProRouteWorkbookVO.Process.class).build()); writer.write(outputs, FastExcelFactory.writerSheet(3, OUTPUT).head(MesProRouteWorkbookVO.Output.class).build()); writer.write(materials, FastExcelFactory.writerSheet(4, MATERIAL).head(MesProRouteWorkbookVO.Material.class).build()); } finally { writer.finish(); writer.close(); } response.addHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8("工艺流程模板.xlsx")); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8"); } public List read(MultipartFile file) throws IOException { boolean legacy; try (InputStream in = file.getInputStream(); var book = WorkbookFactory.create(in)) { legacy = book.getNumberOfSheets() == 1 && "工艺流程".equals(book.getSheetName(0)); if (!legacy) { validateSheet(book, BASIC, MesProRouteWorkbookVO.Basic.class); validateSheet(book, PRODUCT, MesProRouteWorkbookVO.Product.class); validateSheet(book, PROCESS, MesProRouteWorkbookVO.Process.class); validateSheet(book, OUTPUT, MesProRouteWorkbookVO.Output.class); validateSheet(book, MATERIAL, MesProRouteWorkbookVO.Material.class); } } if (legacy) return ExcelUtils.read(file, MesProRouteFullExcelVO.class); List basic = readSheet(file, BASIC, MesProRouteWorkbookVO.Basic.class); if (basic.size() != 1 || StrUtil.isBlank(basic.get(0).getRouteCode())) throw exception(PRO_ROUTE_FULL_EXCEL_INVALID, "基本信息表须填写一条路线,且路线编码不能为空"); String code = basic.get(0).getRouteCode().trim(); List result = new ArrayList<>(); var header = BeanUtils.toBean(basic.get(0), MesProRouteFullExcelVO.class); header.setRecordType("路线"); header.setRouteCode(code); result.add(header); for (MesProRouteWorkbookVO.Product item : readSheet(file, PRODUCT, MesProRouteWorkbookVO.Product.class)) { var row = BeanUtils.toBean(item, MesProRouteFullExcelVO.class); row.setRecordType("产品"); row.setRouteCode(code); result.add(row); } for (MesProRouteWorkbookVO.Process item : readSheet(file, PROCESS, MesProRouteWorkbookVO.Process.class)) { var row = new MesProRouteFullExcelVO(); row.setRecordType("工序"); row.setRouteCode(code); row.setProcessCode(item.getProcessCode()); row.setProcessSort(item.getProcessSort()); row.setKeyFlag(item.getKeyFlag()); row.setCheckFlag(item.getCheckFlag()); row.setBackflushFlag(item.getBackflushFlag()); row.setPrepareTime(item.getPrepareTime()); row.setWaitTime(item.getWaitTime()); row.setLinkType(linkCode(item.getLinkType())); row.setColorCode(item.getColorCode()); row.setProcessRemark(item.getProcessRemark()); result.add(row); } for (MesProRouteWorkbookVO.Output item : readSheet(file, OUTPUT, MesProRouteWorkbookVO.Output.class)) { var row = BeanUtils.toBean(item, MesProRouteFullExcelVO.class); row.setRecordType("产出"); row.setRouteCode(code); row.setProductCode(toImportOwner(item.getProductCode())); row.setItemCode(toImportItem(item.getItemCode())); result.add(row); } for (MesProRouteWorkbookVO.Material item : readSheet(file, MATERIAL, MesProRouteWorkbookVO.Material.class)) { var row = BeanUtils.toBean(item, MesProRouteFullExcelVO.class); row.setRecordType("投料"); row.setRouteCode(code); row.setProductCode(toImportOwner(item.getProductCode())); result.add(row); } return result; } private List readSheet(MultipartFile file, String sheet, Class type) throws IOException { try (InputStream in = file.getInputStream()) { return FastExcelFactory.read(in).head(type).sheet(sheet).doReadSync(); } } private void validateSheet(org.apache.poi.ss.usermodel.Workbook workbook, String name, Class type) { var sheet = workbook.getSheet(name); if (sheet == null || sheet.getRow(0) == null) throw exception(PRO_ROUTE_FULL_EXCEL_INVALID, "缺少工作表或表头:" + name); var header = sheet.getRow(0); var formatter = new DataFormatter(); Set actual = new HashSet<>(); for (var cell : header) { String value = formatter.formatCellValue(cell).trim(); if (!value.isEmpty() && !actual.add(value)) throw exception(PRO_ROUTE_FULL_EXCEL_INVALID, name + "工作表的表头重复:" + value); } Set expected = Arrays.stream(type.getDeclaredFields()).map(f -> f.getAnnotation(ExcelProperty.class)) .filter(Objects::nonNull).map(a -> a.value()[0]).collect(Collectors.toCollection(LinkedHashSet::new)); if (!actual.equals(expected)) throw exception(PRO_ROUTE_FULL_EXCEL_INVALID, name + "工作表的表头已变更,请使用下载的原始模板"); } private String itemName(String code, String referenceCode) { String resolved = MesProRouteFullExcelService.REFERENCE.equals(code) ? referenceCode : code; if (StrUtil.isBlank(resolved)) return null; var item = itemMapper.selectByCode(resolved); return item == null ? null : item.getName(); } private String processName(String code) { if (StrUtil.isBlank(code)) return null; var process = processMapper.selectByCode(code); return process == null ? null : process.getName(); } private String toDisplayOwner(String value) { return StrUtil.isBlank(value) ? COMMON : toDisplayItem(value); } private String toDisplayItem(String value) { return MesProRouteFullExcelService.REFERENCE.equals(value) ? CURRENT : value; } private String toImportOwner(String value) { return StrUtil.isBlank(value) || COMMON.equals(value.trim()) ? null : toImportItem(value); } private String toImportItem(String value) { return StrUtil.isNotBlank(value) && CURRENT.equals(value.trim()) ? MesProRouteFullExcelService.REFERENCE : value; } private String linkName(Integer value) { if (value == null) return null; return switch (value) { case 0 -> "开始-开始"; case 1 -> "结束-结束"; case 2 -> "开始-结束"; case 3 -> "结束-开始"; default -> value.toString(); }; } private Integer linkCode(String value) { if (StrUtil.isBlank(value)) return null; return switch (value.trim()) { case "0", "开始-开始" -> 0; case "1", "结束-结束" -> 1; case "2", "开始-结束" -> 2; case "3", "结束-开始" -> 3; default -> throw exception(PRO_ROUTE_FULL_EXCEL_INVALID, "下一工序关系只能填写开始-开始、结束-结束、开始-结束或结束-开始"); }; } }