| HANDOFF.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightReadinessCheck.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traffic-audit-web/src/views/ReportExport.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
HANDOFF.md
@@ -4,6 +4,22 @@ > **每天结束时**:把当天进展、踩过的坑、新需求、未完成事项更新到本文件,然后可以放心开新对话。 > **协作约定(2026-08-31)**:说「提交」= 本地 commit + 推送到远端 git(origin main,Gitblit),不再只做本地 commit。 ### 10-12(14):汇总大表「缺数清单弹窗 + 生成件《数据缺失清单》页」+ 体检假阳性修复 + 公交 9月缺行核查【待验收】 - **动因**:用户要求把缺数摆到台面上(P0-3 剩余),并在生成件里给一页《数据缺失清单》。 - **改动(3 处)**: 1. 后端 `ReportExportService.appendMissingDataSheet(...)`:生成收尾在产物末尾追加《数据缺失清单》页(第 20 页),分「一、缺数清单 / 二、数据体检 / 三、其它提示」,写报表期+生成时间+条数;只读提示页、不参与计算;母版若由产物派生已带同名页会先移除再建。 2. 前端 `ReportExport.vue`:点「导出/生成」先调 `GET /api/report/summaryWorkbook/missing` + `freightCheck`,命中则弹窗(分两段、可滚动)「仍要生成 / 取消」;两项皆空、接口异常或年报模式 → 直接生成。 3. `FreightReadinessCheck` 修假阳性:②全口径整期未导时不再按 `0−规上` 报「规下为负」(上面已报「缺②全口径」)→ 2026-09 命中 68→34,2026-08 仍 0。 - **实测(dev 8090/8080,仅生成/只读)**:missing(2026-09)=4 条(货运②③、网约车订单、网约车 pin);freightCheck(2026-09)=34、2026-08=0;异步生成 2026-09(refresh=true)→ 1,303,490 B / 20 页 / 清单 39 条(4+34+1),落盘缓存同步刷新为 `_cache/summaryWorkbook_2026-09_month__vc0e2afa47b51bc04.xlsx`;前端 `Compiled successfully`、bundle `/js/6.js` 已含新代码;年报模式两接口返回空。 - **报告(本地、不入库)**:`docs/功能测试报告/功能测试报告_汇总大表缺数清单弹窗与生成件页_2026-10-12.md`。状态:**【待验收】**。 - **【待验收 / 待定夺 / 待处理】三项**: 1. 【待验收】导出前弹窗 + 生成件《数据缺失清单》页(产物 `docs/生成汇总大表/输出/生成_道路运输量汇总表_2026-09.xlsx` 末页); 2. 【待用户定夺】排名页缺数期留空:数值列留空符合预期,但《货运周转量排名》`R` 列仍显示 **10.00**(母版 `=10-Q` 在 `Q` 清空后算出;源侧空表无此公式),是否一并清空本次**未改**; 3. 【待处理】公交 2026-09 少 1 行属**源文件缺行**——`随州市·42138100005·广水市安顺公交有限公司` 在 6/7 月源表第 119 行存在、9 月源表没有(`import_batch#551` 记录 141/141/0,非导入跳过)→ 需向数据来源补件或确认企业停报;**未导入任何数据**。 - **顺带复核**:09 月母版 9 月列数值无误(`班线包车!S9=20647.5363`、`公交!S8=61618.09`、`出租!S6=32787.72`、`中口径明细!S5=3517.9161`;全簿 `46266` 只剩 3 个合法表头行)。 - **环境提示**:本机残留 1 个无窗口 `EXCEL.EXE`(PID 23540,`taskkill` 拒绝访问);已确认**母版未被锁**(读写测试通过),下次用 Excel COM 前先复查该进程。 ### 10-12(13):下班关机交接 — 汇总大表缺数/母版脏格修复已提交推送,下一步待办与新会话起步 **接手入口(新会话先读这三处)**:① 根目录 `AGENTS.md`;② 本文件顶部(本节 + 10-12(11)(12));③ 上一会话标题「10.8日比对汇总大表和专项统计的生成逻辑」(因上下文超限报错中断,其工作已由本节这条线做完并补提交)。 traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightReadinessCheck.java
@@ -128,10 +128,16 @@ issues.add(String.format("货运体检:%s %s 缺②全口径周转量(规上 %.4f 在、全口径缺)→ 规下无法计算", period, city, abT)); } double belowF = (totalF == null ? 0 : totalF) - (abF == null ? 0 : abF); double belowT = (totalT == null ? 0 : totalT) - (abT == null ? 0 : abT); if (belowF < -TOL) issues.add(String.format("货运体检:%s %s 规下货运量为负(全口径 %.4f − 规上 %.4f = %.4f)", period, city, totalF, abF, belowF)); if (belowT < -TOL) issues.add(String.format("货运体检:%s %s 规下周转量为负(全口径 %.4f − 规上 %.4f = %.4f)", period, city, totalT, abT, belowT)); // 2026-10-12:②全口径缺(未导)时不再报「规下为负」——上面已单独报「缺②全口径」, // 若这里仍按 0 − 规上 算,会对每个市州多刷一条假阳性(值不是负、是缺),把真问题淹没。 if (totalF != null) { double belowF = totalF - (abF == null ? 0 : abF); if (belowF < -TOL) issues.add(String.format("货运体检:%s %s 规下货运量为负(全口径 %.4f − 规上 %.4f = %.4f)", period, city, totalF, abF, belowF)); } if (totalT != null) { double belowT = totalT - (abT == null ? 0 : abT); if (belowT < -TOL) issues.add(String.format("货运体检:%s %s 规下周转量为负(全口径 %.4f − 规上 %.4f = %.4f)", period, city, totalT, abT, belowT)); } if (totalF != null && abF == null) issues.add(String.format("货运体检:%s %s 有全口径但无规上记录 → 规下等于全口径(%.4f),疑①漏报", period, city, totalF)); if (totalF != null && abF != null && totalF < abF) issues.add(String.format("货运体检:%s %s 全口径 %.4f 小于规上 %.4f", period, city, totalF, abF)); 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; traffic-audit-web/src/views/ReportExport.vue
@@ -190,6 +190,25 @@ <el-button size="small" v-else type="primary" @click="closeTask">关闭</el-button> </span> </el-dialog> <!-- 汇总大表导出前缺数清单弹窗(2026-10-12 用户要求:缺数摆到台面上) --> <el-dialog title="生成前提示 - 数据缺失清单" :visible.sync="missDialogVisible" width="720px" top="6vh"> <div class="miss-body"> <p class="miss-lead">报表期 <b>{{ missDialogPeriod }}</b>:以下数据本次生成会缺失/异常,生成件末尾也会附《数据缺失清单》页。</p> <template v-if="missDialogMissing.length"> <div class="miss-sec">一、缺数清单(该有的数没有) 共 {{ missDialogMissing.length }} 条</div> <ul class="miss-list"><li v-for="(t, i) in missDialogMissing" :key="'m' + i">{{ t }}</li></ul> </template> <template v-if="missDialogCheck.length"> <div class="miss-sec">二、数据体检(数值不合理) 共 {{ missDialogCheck.length }} 条</div> <ul class="miss-list"><li v-for="(t, i) in missDialogCheck" :key="'c' + i">{{ t }}</li></ul> </template> <p class="miss-tip">缺数多为源数据尚未导入,导入后重新生成即可;体检项请核对源文件。</p> </div> <span slot="footer"> <el-button size="small" @click="missDialogVisible = false">取消</el-button> <el-button size="small" type="primary" @click="confirmMissDialog">仍要生成</el-button> </span> </el-dialog> </el-main> </el-container> </el-container> @@ -228,6 +247,12 @@ taskFileName: '', taskFileSize: 0, taskKey: 'summaryWorkbook', // 导出前缺数清单弹窗(2026-10-12):缺数/体检摆到台面上,确认后再生成 missDialogVisible: false, missDialogPeriod: '', missDialogMissing: [], missDialogCheck: [], missDialogRefresh: false, modules: [ { key: 'summary', title: '汇总大表', items: [ @@ -626,6 +651,28 @@ }) .catch(err => this.failWith(err)) }, // 导出前缺数检查:调只读接口把缺数/体检结果摆到用户面前,确认后再走异步生成任务 generateSummaryWorkbook(refresh) { if (!this.period) { this.$message.warning('请先选择周期和月份'); return } const params = { period: this.period } Promise.all([ api.get('/report/summaryWorkbook/missing', { params, timeout: 60000 }).catch(() => null), api.get('/report/summaryWorkbook/freightCheck', { params, timeout: 60000 }).catch(() => null) ]).then(res => { const miss = (res[0] && res[0].code === 200 && res[0].data) || [] const chk = (res[1] && res[1].code === 200 && res[1].data) || [] if (!miss.length && !chk.length) { this.startSummaryTask(refresh); return } this.missDialogPeriod = this.period this.missDialogMissing = miss this.missDialogCheck = chk this.missDialogRefresh = refresh this.missDialogVisible = true }) }, confirmMissDialog() { this.missDialogVisible = false this.startSummaryTask(this.missDialogRefresh) }, restartTask() { this.startSummaryTask(true) }, closeTask() { this.taskVisible = false; this.stopTaskPolling(); this.loading = '' }, // 生成中收起弹窗:任务继续在后台执行,完成后仍会自动下载(与原来同步导出的「点完即下载」一致) @@ -643,7 +690,7 @@ const doExport = () => { if (items.length === 1) { // 汇总大表生成一次约 10~20 秒,走异步任务(进度弹窗),避免点击后长时间无响应 if (items[0].key === 'summaryWorkbook') { this.startSummaryTask(false); return } if (items[0].key === 'summaryWorkbook') { this.generateSummaryWorkbook(false); return } api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 }) .then(res => { this.downloadBlob(res, this.fileNameOf(items[0])); this.$message.success('报表导出成功'); this.loading = '' }) .catch(err => this.failWith(err)) @@ -794,6 +841,13 @@ .task-meta { margin: 2px 0; font-size: 12px; color:#909399; line-height: 1.6; } .task-error { margin: 8px 0 0; font-size: 13px; color:#F56C6C; word-break: break-all; } .task-done { margin: 8px 0 0; font-size: 13px; color:#67C23A; line-height: 1.6; } /* 导出前缺数清单弹窗 */ .miss-body { max-height: 62vh; overflow-y: auto; } .miss-lead { font-size: 13px; color: #303133; margin: 0 0 10px; line-height: 1.7; } .miss-sec { font-size: 13px; font-weight: 600; color: #F56C6C; margin: 10px 0 4px; } .miss-list { margin: 0; padding-left: 20px; font-size: 13px; color: #303133; line-height: 1.7; } .miss-list li { margin: 2px 0; } .miss-tip { font-size: 12px; color: #909399; margin: 10px 0 0; } </style> <style>