云
10 天以前 bf5f52656e128f578ed42c7979c31cf4b476ce46
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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<MesProRouteFullExcelVO> rows) throws IOException {
        List<MesProRouteWorkbookVO.Basic> basic = new ArrayList<>();
        List<MesProRouteWorkbookVO.Product> products = new ArrayList<>();
        List<MesProRouteWorkbookVO.Process> processes = new ArrayList<>();
        List<MesProRouteWorkbookVO.Output> outputs = new ArrayList<>();
        List<MesProRouteWorkbookVO.Material> 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<MesProRouteFullExcelVO> 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<MesProRouteWorkbookVO.Basic> 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<MesProRouteFullExcelVO> 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 <T> List<T> readSheet(MultipartFile file, String sheet, Class<T> 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<String> 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<String> 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, "下一工序关系只能填写开始-开始、结束-结束、开始-结束或结束-开始");
        };
    }
}