traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -42,8 +42,12 @@
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.FormulaEvaluator;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@@ -3781,6 +3785,7 @@
            log.info("汇总工作簿公式重算完成:成功 {} 格,失败 {} 格(失败格保留母版旧缓存值,打开时按 fullCalcOnLoad 重算)", recalc[0], recalc[1]);
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // 还原/改写公式后打开即重算
            appendMissingDataSheet(wb, effPeriod, dbProblems); // 2026-10-12:缺数/体检结果写进生成件(用户要求)
            reportProgress(progress, 95, "写出文件");
            return toBytes(wb);
        } finally {
@@ -3788,6 +3793,106 @@
        }
    }
    /**
     * 生成件《数据缺失清单》页(2026-10-12,用户要求)。
     *
     * <p>把本次导出的「缺数清单」与「货运基数体检」结果显式写进产物:用户拿到文件即可看到本报表期
     * 有哪些数没到、哪些数不合理,不必翻导出日志。只读提示页,不参与本表计算。
     */
    private void appendMissingDataSheet(XSSFWorkbook wb, String period, List<String> problems) {
        String sheetName = "数据缺失清单";
        int old = wb.getSheetIndex(sheetName);
        if (old >= 0) wb.removeSheetAt(old); // 母版由「当期产物」派生时可能已带本页,先移除避免重名
        XSSFSheet sh = wb.createSheet(sheetName);
        CellStyle titleStyle = wb.createCellStyle();
        Font titleFont = wb.createFont();
        titleFont.setBold(true);
        titleFont.setFontHeightInPoints((short) 14);
        titleStyle.setFont(titleFont);
        CellStyle metaStyle = wb.createCellStyle();
        Font metaFont = wb.createFont();
        metaFont.setFontHeightInPoints((short) 10);
        metaFont.setColor(IndexedColors.GREY_50_PERCENT.getIndex());
        metaStyle.setFont(metaFont);
        CellStyle sectionStyle = wb.createCellStyle();
        Font sectionFont = wb.createFont();
        sectionFont.setBold(true);
        sectionStyle.setFont(sectionFont);
        CellStyle bodyStyle = wb.createCellStyle();
        bodyStyle.setWrapText(true);
        bodyStyle.setVerticalAlignment(VerticalAlignment.TOP);
        List<String> missing = new ArrayList<>();
        List<String> check = new ArrayList<>();
        List<String> other = new ArrayList<>();
        if (problems != null) {
            for (String p : problems) {
                if (p == null || p.trim().isEmpty()) continue;
                if (p.startsWith("缺数")) missing.add(p);
                else if (p.startsWith("货运体检")) check.add(p);
                else other.add(p);
            }
        }
        int r = 0;
        Row tRow = sh.createRow(r++);
        Cell tCell = tRow.createCell(0);
        tCell.setCellValue("数据缺失清单");
        tCell.setCellStyle(titleStyle);
        Row mRow = sh.createRow(r++);
        Cell mCell = mRow.createCell(0);
        mCell.setCellValue("报表期 " + period + " 生成时间 "
            + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm").format(new java.util.Date())
            + " 共 " + (missing.size() + check.size() + other.size()) + " 条");
        mCell.setCellStyle(metaStyle);
        Row nRow = sh.createRow(r++);
        Cell nCell = nRow.createCell(0);
        nCell.setCellValue("本页由程序自动生成(只读提示):一、缺数清单=该有的数没有;二、数据体检=数值不合理(如规下为负)。不参与本表计算。");
        nCell.setCellStyle(metaStyle);
        r++;
        r = writeProblemSection(sh, r, "一、缺数清单(该有的数没有)", missing, sectionStyle, bodyStyle);
        r = writeProblemSection(sh, r, "二、数据体检(数值不合理)", check, sectionStyle, bodyStyle);
        if (!other.isEmpty()) {
            r = writeProblemSection(sh, r, "三、其它提示", other, sectionStyle, bodyStyle);
        }
        sh.setColumnWidth(0, 6 * 256);
        sh.setColumnWidth(1, 120 * 256);
        sh.createFreezePane(0, 3);
    }
    /** 写一个清单分区(标题 + 逐条),返回下一个可写行号 */
    private int writeProblemSection(XSSFSheet sh, int rowIdx, String title, List<String> lines,
                                    CellStyle sectionStyle, CellStyle bodyStyle) {
        Row hr = sh.createRow(rowIdx++);
        Cell hc = hr.createCell(0);
        hc.setCellValue(title);
        hc.setCellStyle(sectionStyle);
        sh.addMergedRegion(new CellRangeAddress(rowIdx - 1, rowIdx - 1, 0, 1));
        if (lines.isEmpty()) {
            Row er = sh.createRow(rowIdx++);
            er.createCell(0).setCellValue("—");
            er.createCell(1).setCellValue("无");
            return rowIdx;
        }
        int i = 1;
        for (String line : lines) {
            Row row = sh.createRow(rowIdx++);
            row.createCell(0).setCellValue(i++);
            Cell c = row.createCell(1);
            c.setCellValue(line);
            c.setCellStyle(bodyStyle);
        }
        return rowIdx;
    }
    /** 导出进度回调(异步导出任务用):回调自身异常不影响导出 */
    private static void reportProgress(java.util.function.BiConsumer<Integer, String> progress, int percent, String step) {
        if (progress == null) return;