2026-06-09 eb832a81fb17402b1dded01459a01d7d99f16262
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
package com.ruoyi.quality.utils;
 
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.HackLoopTableRenderPolicy;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectParam;
import com.ruoyi.quality.service.IQualityInspectParamService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.usermodel.Range;
import org.apache.poi.hwpf.usermodel.Table;
import org.apache.poi.hwpf.usermodel.TableCell;
import org.apache.poi.hwpf.usermodel.TableIterator;
import org.apache.poi.hwpf.usermodel.TableRow;
import org.springframework.stereotype.Component;
 
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
 
/**
 * Word template export helper for process inspection.
 */
@Component
@RequiredArgsConstructor
public class QualityInspectTemplateExportHelper {
 
    private static final String WEILONG_TEMPLATE = "/static/伟龙模版.doc";
    private static final String BAISHI_TEMPLATE = "/static/百事模版.doc";
    private static final String DALI_TEMPLATE = "/static/达利模版.doc";
 
    private final QualityInspectMapper qualityInspectMapper;
    private final IQualityInspectParamService qualityInspectParamService;
 
    public void exportWeiLong(HttpServletResponse response, Long inspectId) {
        export(response, inspectId, WEILONG_TEMPLATE, "伟龙模版检验结果");
    }
 
    public void exportBaiShi(HttpServletResponse response, Long inspectId) {
        if (inspectId == null) {
            throw new ServiceException("检验单ID不能为空");
        }
 
        QualityInspect inspect = qualityInspectMapper.selectById(inspectId);
        if (inspect == null) {
            throw new ServiceException("检验单不存在");
        }
 
        List<QualityInspectParam> paramList = qualityInspectParamService.list(
                Wrappers.<QualityInspectParam>lambdaQuery()
                        .eq(QualityInspectParam::getInspectId, inspectId)
                        .orderByAsc(QualityInspectParam::getId));
        int index = 1;
        for (QualityInspectParam detail : paramList) {
            detail.setIndex(index);
            index++;
        }
 
        try (InputStream inputStream = getClass().getResourceAsStream("/static/百事模版.docx")) {
            if (inputStream == null) {
                throw new ServiceException("模板文件不存在:/static/百事模版.docx");
            }
 
            Configure configure = Configure.builder()
                    .bind("paramList", new HackLoopTableRenderPolicy())
                    .build();
 
            XWPFTemplate template = XWPFTemplate.compile(inputStream, configure).render(
                    new HashMap<String, Object>() {{
                        put("inspect", inspect);
                        put("paramList", paramList);
                    }});
 
            response.reset();
            response.setContentType("application/msword");
            response.setCharacterEncoding("UTF-8");
            String encodedName = URLEncoder.encode("百事模版检验结果", StandardCharsets.UTF_8).replace("+", "%20");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-Disposition", "attachment;filename=" + encodedName + ".docx");
 
            try (OutputStream outputStream = response.getOutputStream()) {
                template.write(outputStream);
                outputStream.flush();
            }
        } catch (IOException e) {
            throw new RuntimeException("导出失败", e);
        }
    }
 
    public void exportDaLi(HttpServletResponse response, Long inspectId) {
        export(response, inspectId, DALI_TEMPLATE, "达利模版检验结果");
    }
 
    private void export(HttpServletResponse response, Long inspectId, String templatePath, String fileName) {
        if (inspectId == null) {
            throw new ServiceException("检验单ID不能为空");
        }
 
        QualityInspect inspect = qualityInspectMapper.selectById(inspectId);
        if (inspect == null) {
            throw new ServiceException("检验单不存在");
        }
 
        List<QualityInspectParam> paramList = qualityInspectParamService.list(
                Wrappers.<QualityInspectParam>lambdaQuery()
                        .eq(QualityInspectParam::getInspectId, inspectId)
                        .orderByAsc(QualityInspectParam::getId));
        Map<String, String> valueMap = buildValueMap(paramList, inspect);
 
        try (InputStream inputStream = getClass().getResourceAsStream(templatePath)) {
            if (inputStream == null) {
                throw new ServiceException("模板文件不存在:" + templatePath);
            }
 
            HWPFDocument document = new HWPFDocument(inputStream);
            fillDocument(document, valueMap);
 
            response.reset();
            response.setContentType("application/msword");
            response.setCharacterEncoding("UTF-8");
            String encodedName = URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-Disposition", "attachment;filename=" + encodedName + ".doc");
 
            try (OutputStream outputStream = response.getOutputStream()) {
                document.write(outputStream);
                outputStream.flush();
            }
        } catch (IOException e) {
            throw new RuntimeException("导出失败", e);
        }
    }
 
    private void fillDocument(HWPFDocument document, Map<String, String> valueMap) {
        Range range = document.getRange();
        TableIterator iterator = new TableIterator(range);
        while (iterator.hasNext()) {
            Table table = iterator.next();
            fillTable(table, valueMap);
        }
    }
 
    private void fillTable(Table table, Map<String, String> valueMap) {
        String currentGroupLabel = "";
        // 记录结论列的位置,避免在结论列下面继续填充
        int conclusionCellIndex = -1;
 
        for (int rowIndex = 0; rowIndex < table.numRows(); rowIndex++) {
            TableRow row = table.getRow(rowIndex);
            String firstCellText = getCellText(row, 0);
            String secondCellText = getCellText(row, 1);
 
            if (StringUtils.isNotBlank(firstCellText)) {
                currentGroupLabel = firstCellText;
            }
 
            // 先判断是否为结论行
            String normalizedFirstCell = normalizeKey(firstCellText);
            boolean isConclusionRow = matchesSummaryRow(normalizedFirstCell);
 
            // 如果是结论行,结论列始终是最后一列
            if (isConclusionRow) {
                conclusionCellIndex = row.numCells() - 1;
            }
 
            String value = resolveValue(valueMap, currentGroupLabel, firstCellText, secondCellText, isConclusionRow);
            if (StringUtils.isBlank(value)) {
                continue;
            }
 
            // 查找结果列位置
            int resultCellIndex;
            if (isConclusionRow) {
                resultCellIndex = conclusionCellIndex;
            } else {
                // 普通行:找第一个空白单元格作为结果列,但要排除结论列
                resultCellIndex = findResultCellIndex(row, conclusionCellIndex);
                if (resultCellIndex < 0) {
                    // 不使用最后一列,避免与结论列冲突
                    int lastCellIndex = row.numCells() - 1;
                    if (lastCellIndex > 1 && lastCellIndex != conclusionCellIndex) {
                        resultCellIndex = lastCellIndex - 1;
                    }
                }
            }
 
            if (resultCellIndex < 0 || resultCellIndex == conclusionCellIndex && !isConclusionRow) {
                continue;
            }
 
            TableCell resultCell = row.getCell(resultCellIndex);
            String cellText = cleanCellText(resultCell.text());
 
            // 严格检查:只有空白或占位符才填充
            if (!isBlankLike(cellText) && !isPlaceholder(cellText)) {
                continue;
            }
 
            // 使用更安全的填充方式
            safeWriteCellText(resultCell, value);
        }
    }
 
    /**
     * 查找结果列,排除结论列
     */
    private int findResultCellIndex(TableRow row, int excludeIndex) {
        for (int i = 1; i < row.numCells(); i++) {
            if (i == excludeIndex) {
                continue;
            }
            String cellText = cleanCellText(row.getCell(i).text());
            if (isBlankLike(cellText) || isPlaceholder(cellText)) {
                return i;
            }
        }
        return -1;
    }
 
    /**
     * 安全写入单元格文本,避免影响其他行
     */
    private void safeWriteCellText(TableCell cell, String value) {
        if (StringUtils.isBlank(value)) {
            return;
        }
 
        String originalText = cell.text();
        String cleanedOriginal = cleanCellText(originalText);
 
        // 如果单元格已有内容且不是占位符,不写入
        if (StringUtils.isNotBlank(cleanedOriginal) && !isPlaceholder(cleanedOriginal)) {
            return;
        }
 
        // 只使用replaceText,不使用insertBefore
        try {
            if (StringUtils.isNotBlank(originalText)) {
                cell.replaceText(originalText, value);
            } else {
                // 空单元格直接设置文本
                cell.insertBefore(value);
            }
        } catch (Exception e) {
            // 备用方案:使用getRange方式
            try {
//                cell.getRange().insertAfter(value);
            } catch (Exception ignored) {}
        }
    }
 
    /**
     * 判断是否为占位符
     */
    private boolean isPlaceholder(String text) {
        if (StringUtils.isBlank(text)) {
            return false;
        }
        String cleaned = text.trim();
        // 常见占位符模式
        return cleaned.equals("—") ||
               cleaned.equals("-") ||
               cleaned.equals("_") ||
               cleaned.equals("/") ||
               cleaned.equals("\\") ||
               cleaned.equals("□") ||
               cleaned.equals("■") ||
               cleaned.equals("○") ||
               cleaned.equals("●") ||
               cleaned.equals("※") ||
               cleaned.equals("*") ||
               cleaned.matches("^\\.{2,}$") || // 多个点
               cleaned.matches("^{2,}$") || // 多个大括号
               cleaned.equalsIgnoreCase("N/A") ||
               cleaned.equalsIgnoreCase("NA") ||
               cleaned.equals("待检") ||
               cleaned.equals("待填") ||
               cleaned.equals("空白");
    }
 
    private int findResultCellIndex(TableRow row) {
        for (int i = 1; i < row.numCells(); i++) {
            if (isBlankLike(row.getCell(i).text())) {
                return i;
            }
        }
        return -1;
    }
 
    private String resolveValue(Map<String, String> valueMap,
                                String currentGroupLabel,
                                String firstCellText,
                                String secondCellText,
                                boolean isConclusionRow) {
        LinkedHashSet<String> candidates = new LinkedHashSet<>();
        addCandidate(candidates, firstCellText);
        addCandidate(candidates, secondCellText);
        addCandidate(candidates, firstCellText + secondCellText);
        if (StringUtils.isNotBlank(currentGroupLabel) && StringUtils.isNotBlank(secondCellText)) {
            addCandidate(candidates, currentGroupLabel + secondCellText);
        }
        if (StringUtils.isNotBlank(currentGroupLabel)
                && StringUtils.isNotBlank(firstCellText)
                && StringUtils.isBlank(secondCellText)) {
            addCandidate(candidates, currentGroupLabel);
        }
 
 
        // 结论行不填充,保留模板原始数据
        if (isConclusionRow) {
            return null;
        }
 
        // 普通行:先查检验参数值,最后才匹配结论行关键词
        for (String candidate : candidates) {
            String normalized = normalizeKey(candidate);
            // 普通行跳过结论关键词匹配
            if (matchesSummaryRow(normalized)) {
                continue;
            }
            String value = lookupValue(valueMap, normalized);
            if (StringUtils.isNotBlank(value)) {
                return value;
            }
        }
 
        return null;
    }
 
    private boolean matchesSummaryRow(String normalizedText) {
        if (StringUtils.isBlank(normalizedText)) {
            return false;
        }
        return normalizedText.contains("质量评定")
                || normalizedText.contains("检验结论")
                || normalizedText.contains("Gradeestimation")
                || normalizedText.contains("Conclusion")
                || normalizedText.contains("Evaluation")
                || normalizedText.contains("评定")
                || normalizedText.contains("结论");
    }
 
    private String lookupValue(Map<String, String> valueMap, String normalizedCandidate) {
        if (StringUtils.isBlank(normalizedCandidate)) {
            return null;
        }
 
        String value = valueMap.get(normalizedCandidate);
        if (StringUtils.isNotBlank(value)) {
            return value;
        }
 
        String chineseCandidate = stripEnglishLetters(normalizedCandidate);
        value = valueMap.get(chineseCandidate);
        if (StringUtils.isNotBlank(value)) {
            return value;
        }
 
        for (Map.Entry<String, String> entry : valueMap.entrySet()) {
            String key = entry.getKey();
            if (StringUtils.contains(key, normalizedCandidate)
                    || StringUtils.contains(normalizedCandidate, key)
                    || StringUtils.contains(key, chineseCandidate)
                    || StringUtils.contains(chineseCandidate, key)) {
                return entry.getValue();
            }
        }
        return null;
    }
 
    private Map<String, String> buildValueMap(List<QualityInspectParam> paramList, QualityInspect inspect) {
        Map<String, String> valueMap = new LinkedHashMap<>();
        for (QualityInspectParam param : paramList) {
            String value = StringUtils.trimToNull(param.getTestValue());
            if (StringUtils.isBlank(value)) {
                continue;
            }
            putValue(valueMap, param.getParameterItem(), value);
        }
 
        String checkResult = StringUtils.trimToNull(inspect.getCheckResult());
        if (StringUtils.isNotBlank(checkResult)) {
            putValue(valueMap, "质量评定", checkResult);
            putValue(valueMap, "检验结果", checkResult);
            putValue(valueMap, "检验结论", checkResult);
            putValue(valueMap, "Grade estimation", checkResult);
            putValue(valueMap, "Test Results", checkResult);
        }
        return valueMap;
    }
 
    private void putValue(Map<String, String> valueMap, String key, String value) {
        String normalizedKey = normalizeKey(key);
        if (StringUtils.isBlank(normalizedKey)) {
            return;
        }
        valueMap.put(normalizedKey, value);
 
        String chineseKey = stripEnglishLetters(normalizedKey);
        if (StringUtils.isNotBlank(chineseKey)) {
            valueMap.putIfAbsent(chineseKey, value);
        }
    }
 
    private void writeCellText(TableCell cell, String value) {
        if (StringUtils.isBlank(value)) {
            return;
        }
 
        String originalText = cell.text();
        try {
            cell.replaceText(originalText, value);
        } catch (Exception ignored) {
            // Fallback below.
        }
 
        String cleanedText = cleanCellText(cell.text());
        if (!cleanedText.contains(value)) {
            cell.insertBefore(value);
        }
    }
 
    private String getCellText(TableRow row, int index) {
        if (row.numCells() <= index) {
            return "";
        }
        return cleanCellText(row.getCell(index).text());
    }
 
    private String cleanCellText(String text) {
        if (text == null) {
            return "";
        }
        return text.replace("\u0007", "")
                .replace("\r", "")
                .replace("\n", "")
                .trim();
    }
 
    private boolean isBlankLike(String text) {
        String cleaned = cleanCellText(text);
        return StringUtils.isBlank(cleaned)
                || "-".equals(cleaned)
                || "_".equals(cleaned)
                || "—".equals(cleaned)
                || "·".equals(cleaned);
    }
 
    private void addCandidate(LinkedHashSet<String> candidates, String text) {
        if (StringUtils.isBlank(text)) {
            return;
        }
        candidates.add(text);
    }
 
    private String normalizeKey(String text) {
        String value = cleanCellText(text);
        if (StringUtils.isBlank(value)) {
            return "";
        }
        value = value.replace("\u00A0", "");
        value = value.replaceAll("\\s+", "");
        value = value.replace("(", "");
        value = value.replace(")", "");
        value = value.replace("(", "");
        value = value.replace(")", "");
        value = value.replace(",", "");
        value = value.replace(",", "");
        value = value.replace("。", "");
        value = value.replace(":", "");
        value = value.replace(":", "");
        value = value.replace(";", "");
        value = value.replace(";", "");
        value = value.replace("、", "");
        value = value.replace("“", "");
        value = value.replace("”", "");
        value = value.replace("【", "");
        value = value.replace("】", "");
        value = value.replace("%", "");
        return value;
    }
 
    private String stripEnglishLetters(String text) {
        if (StringUtils.isBlank(text)) {
            return "";
        }
        return text.replaceAll("[A-Za-z]", "");
    }
 
    /**
     * 调试方法:分析模板表格结构
     */
    public String analyzeTemplate(String templatePath) {
        StringBuilder sb = new StringBuilder();
        try (InputStream inputStream = getClass().getResourceAsStream(templatePath)) {
            if (inputStream == null) {
                return "模板文件不存在:" + templatePath;
            }
 
            HWPFDocument document = new HWPFDocument(inputStream);
            Range range = document.getRange();
            TableIterator iterator = new TableIterator(range);
 
            int tableIndex = 0;
            while (iterator.hasNext()) {
                Table table = iterator.next();
                sb.append("=== 表格 ").append(tableIndex++).append(" ===\n");
                sb.append("行数: ").append(table.numRows()).append("\n");
 
                for (int rowIndex = 0; rowIndex < table.numRows(); rowIndex++) {
                    TableRow row = table.getRow(rowIndex);
                    sb.append("行").append(rowIndex).append(": ");
                    for (int cellIndex = 0; cellIndex < row.numCells(); cellIndex++) {
                        String cellText = cleanCellText(row.getCell(cellIndex).text());
                        sb.append("[列").append(cellIndex).append(": ").append(cellText).append("] ");
                    }
                    sb.append("\n");
                }
                sb.append("\n");
            }
        } catch (Exception e) {
            sb.append("分析失败: ").append(e.getMessage());
        }
        return sb.toString();
    }
}