fix(汇总大表): 《货运周转量排名》按「全省」行锚点对齐并加全省行自检(待验收)
3个文件已修改
97 ■■■■■ 已修改文件
HANDOFF.md 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/生成汇总大表/2026年9月道路运输量汇总表.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HANDOFF.md
@@ -4,6 +4,25 @@
> **每天结束时**:把当天进展、踩过的坑、新需求、未完成事项更新到本文件,然后可以放心开新对话。
> **协作约定(2026-08-31)**:说「提交」= æœ¬åœ° commit + æŽ¨é€åˆ°è¿œç«¯ git(origin main,Gitblit),不再只做本地 commit。
### 10-13(1):汇总大表《货运周转量排名》块错位修复(母版删多余标题行 + ä»£ç æŒ‰ã€Œå…¨çœã€è¡Œé”šç‚¹å¯¹é½ + å†™åŽè‡ªæ£€ï¼‰ã€å¾…验收】
- **现象(用户报告 + æ ¸å¯¹ï¼‰**:9 æœˆ â‘¡â‘¢ å…¥åº“后,生成件《货运周转量排名》整块错位一行——全省值落到**表头行**(覆盖「累计完成(万吨公里)」文字)、市州值与市州名**错开一行**(武汉行装黄石的数…林区行是母版旧值)、**武汉的值丢失**;`RANK/占比/Q/R` éšé”™ä½ä¸€å¹¶é”™ã€‚
- **根因(两层)**:① æ¯ç‰ˆè¯¥é¡µæ¯å—比源底稿 `docs/货运/生成_周转量排名.xlsx` **多 1 è¡Œ**(标题合并占两行:`A1:R2`、`A23:R24`;全省行在第 5/27 è¡Œï¼‰ï¼›â‘¡ `ReportExportService.copyRankValues` æŒ‰ã€Œå—标题行 + æºè¡Œåç§»ã€é€è¡Œç…§æ¬ï¼Œä¸” `:4334` é‡åˆ°ç›®æ ‡æ ¼æ˜¯å…¬å¼å°±è·³è¿‡ â†’ å½¢æˆ"值列保留 SUM、排名列被写成上一行市州"的半错位。该缺陷**一直存在**(7/8/9 æœˆæ¯ç‰ˆåŒç»“构),因这页此前走"整块缺数留空"分支(②③未导)而未暴露。
- **修复**:
  1. **母版**(用户 19:47 æ‰§è¡Œï¼‰ï¼š`docs/生成汇总大表/2026å¹´9月道路运输量汇总表.xlsx`《货运周转量排名》删掉两块多出的标题行 â†’ å…¨çœè¡Œå›žåˆ°ç¬¬ **4/25** è¡Œï¼Œä¸Žã€Šè´§è¿é‡æŽ’名》同构;Excel å·²è‡ªåŠ¨ä¿®å¥½åˆå¹¶åŒºä¸Ž `SUM(B5:B21)`/`SUM(B26:B42)` ç­‰å…¬å¼åŒºé—´ã€‚
  2. **代码**(`ReportExportService.copyRankValues` â†’ ä¼ å‚ `List<String> problems`):改按目标块「全省」行做锚点,`rowOff = ç›®æ ‡å…¨çœè¡Œ âˆ’ æºå…¨çœè¡Œ`;识别不到锚点回退标题行偏移并 WARN;新增**写后自检**(目标全省行与源侧不一致 â‰¥2 åˆ— â†’ WARN + å†™å…¥ã€Šæ•°æ®ç¼ºå¤±æ¸…单》)。三个排名页(货运量/周转量/中口径)统一生效。
- **验证(20:06 é‡æ–°ç”Ÿæˆ 2026-09,dev 8090/8080 å·²é‡å¯åŠ è½½æ–°ä»£ç ï¼‰**:《货运周转量排名》累计块/当月块 Ã— å…¨çœ + 17 å¸‚å·ž Ã— 16 åˆ—(规上/规下/合计值、名次、占比、同比、增速名次、Q/R)与库内 `scale_split_transport` 2026-09(CUMULATIVE/MONTH)**逐格一致,0 å¤„不一致**;缺数清单 4→2 æ¡ï¼ˆâ‘¡â‘¢å·²å…¥åº“,仅剩网约车 2 æ¡ï¼‰ï¼›é¡µç­¾å®šä½ 20 é¡µã€ç´¯è®¡é‡å»º 0 æ ¼ã€å…¬å¼é‡ç®— 39969 æ ¼æˆåŠŸã€‚
- **报告(本地、不入库)**:`docs/功能测试报告/功能测试报告_汇总大表周转量排名锚点对齐_2026-10-13.md`。产物:`_tmp/_fix_1013/生成_道路运输量汇总表_2026-09_验证.xlsx`(未入库)。
- **【待验收/待定夺/待处理】**:
  1. ã€å¾…验收】《货运周转量排名》母版删行 + ä»£ç é”šç‚¹å¯¹é½ä¸Žè‡ªæ£€ï¼ˆé¡µé¢é‡æ–°ç”Ÿæˆå³å¯å¤çŽ°éªŒè¯ï¼‰ã€‚
  2. ã€å¾…用户拍板】《货运量排名》累计块「规下增速 / å¢žé€ŸæŽ’名」**30 æ ¼**口径:生成件(全省 0.052525)vs æ¡Œé¢ã€Š2026å¹´9月客运量、货运量预测.xlsx》(全省 -0.224015),需定以哪边为准。
  3. ã€å¾…用户拍板】版式期望:块顺序(生成件=累计在上;预测件=当月在上)、市州名带不带"市"。
  4. ã€å¾…处理】7/8 æœˆæ¯ç‰ˆåŒä¸º"多 1 è¡Œ"结构(在 `归档/`)——锚点对齐后正确性已不受影响,下次取回出表时确认即可,暂不改归档件。
  5. ã€12 æœˆå‰ã€‘跨年母版需新开一本(扩列 `month>=12` æ˜Žç¡®æ‹’绝);建议做成半自动,规则待定(保留几年历史块 / æ–°ä¸€å¹´æœˆåº¦åˆ—布放 / 2027-01 æ¯ç‰ˆæ˜¯å¦ç•™æ¡£ï¼‰ã€‚
  6. ã€ç¼ºæ•°/缺行】网约车 9 æœˆï¼ˆè®¢å•、全省 pin、源文件);公交 9 æœˆ 141 è¡Œ vs 142 è¡Œï¼ˆæºæ–‡ä»¶ç¼º `随州市·广水市安顺公交有限公司`)。
- **环境**:dev å‰åŽç«¯ 20:00 é‡å¯ï¼ˆ`backend.log` è§ `Started TrafficAuditApplication`、`Tomcat started on port(s): 8090`);启动日志另有一条与本次无关的 `traffic_audit.holiday_calendar` è¡¨ä¸å­˜åœ¨å‘Šè­¦ã€‚
- **数据边界**:本轮只做生成与只读核对(库内查询只读),**未导入、未覆盖任何业务数据**。
### 10-12(14):汇总大表「缺数清单弹窗 + ç”Ÿæˆä»¶ã€Šæ•°æ®ç¼ºå¤±æ¸…单》页」+ ä½“检假阳性修复 + å…¬äº¤ 9月缺行核查【待验收】
- **动因**:用户要求把缺数摆到台面上(P0-3 å‰©ä½™ï¼‰ï¼Œå¹¶åœ¨ç”Ÿæˆä»¶é‡Œç»™ä¸€é¡µã€Šæ•°æ®ç¼ºå¤±æ¸…单》。
docs/Éú³É»ã×Ü´ó±í/2026Äê9ÔµÀ·ÔËÊäÁ¿»ã×ܱí.xlsx
Binary files differ
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3770,7 +3770,7 @@
            // 2026-10-13:累计列 / ç´¯è®¡åŒæ¯”分母按表头重建(根治扩列器早期“累计漏月”的历史欠账)
            int cumRebuilt = summaryWorkbookFiller.repairCumulativeFormulas(wb, year, effMonth);
            reportProgress(progress, 45, "刷新排名页与跨页合成页");
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month", dbProblems);
            int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // å››å¼ è·¨é¡µåˆæˆé¡µéšæŠ¥è¡¨æœŸåˆ·æ–°
            // 2026-10-12:停用「套用外部参照件《生成_道路运输量汇总表.xlsx》列宽」——版式一律以当期母版为准,
            // é¿å…æ¯ç‰ˆä¸Žå‚照件两处维护(母版已并入参照件的列宽/隐藏)。
@@ -4250,11 +4250,11 @@
     * çš„结果刷新数值,避免汇总表里的排名页停留在旧月份。
     * æ•°å€¼æ ¼æ¬å€¼ã€å…¬å¼æ ¼æŒ‰åç§»å¹³ç§»å…¬å¼åŽå†™å…¥ï¼›ç›®æ ‡æ ¼æœ¬èº«å·²æ˜¯å…¬å¼çš„(SUM/RANK/占比/补数)保留不动,打开时按新数据重算。
     */
    private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) throws Exception {
    private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode, List<String> problems) throws Exception {
        int n = 0;
        n += copyRankValues(wb, "货运量排名", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}});
        n += copyRankValues(wb, "货运周转量排名", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}});
        n += copyRankValues(wb, "中口径排名", exportPassengerMidRank(period, mode, wb), new int[][]{{0, 0}, {0, 10}});
        n += copyRankValues(wb, "货运量排名", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}}, problems);
        n += copyRankValues(wb, "货运周转量排名", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}}, problems);
        n += copyRankValues(wb, "中口径排名", exportPassengerMidRank(period, mode, wb), new int[][]{{0, 0}, {0, 10}}, problems);
        return n;
    }
@@ -4263,13 +4263,15 @@
     * srcBlocks[i] = æºå— i çš„èµ·å§‹ {行, åˆ—}(0 åŸºï¼‰ï¼›ç›®æ ‡å—按标题顺序(累计块在前、当月块在后)对应。
     * æ•°å€¼æ ¼ç›´æŽ¥æ¬å€¼ï¼›æºä¾§æ˜¯å…¬å¼çš„(排名/占比等)按目标块偏移整体平移后写入公式,打开时按汇总表自身数据重算。
     */
    private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) throws Exception {
    private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks,
                              List<String> problems) throws Exception {
        XSSFSheet dst = dstWb.getSheet(dstSheetName);
        if (dst == null) return 0;
        List<XSSFCell> dstTitles = rankTitleCells(dst);
        if (dstTitles.isEmpty()) return 0;
        int copied = 0;
        FormulaEvaluator dstEv = dstWb.getCreationHelper().createFormulaEvaluator();
        List<int[]> anchors = new ArrayList<>(); // {源全省行, ç›®æ ‡å…¨çœè¡Œ, å—首列, åˆ—偏移, å—宽}
        try (InputStream sin = new ByteArrayInputStream(srcBytes);
             XSSFWorkbook srcWb = new XSSFWorkbook(sin)) {
            XSSFSheet src = srcWb.getSheetAt(0);
@@ -4279,10 +4281,27 @@
                int sr0 = srcBlocks[b][0];
                int sc0 = srcBlocks[b][1];
                XSSFCell dstTitle = dstTitles.get(b);
                int rowOff = dstTitle.getRowIndex() - sr0;
                int colOff = dstTitle.getColumnIndex() - sc0;
                int rowOff = dstTitle.getRowIndex() - sr0;
                // 2026-10-13:《货运周转量排名》母版块标题曾占两行(合并),"标题行+固定偏移"会把源行
                // æ•´ä½“写错位一行(全省值落到表头行、市州值与市州名错开)。改为用「全省」行做锚点对齐:
                // rowOff = ç›®æ ‡å…¨çœè¡Œ - æºå…¨çœè¡Œï¼›è¯†åˆ«ä¸åˆ°é”šç‚¹å†å›žé€€æ ‡é¢˜è¡Œåç§»å¹¶å‘Šè­¦ã€‚
                Integer srcAnchor = rankAnchorRow(src, sr0, sc0);
                Integer dstAnchor = rankAnchorRow(dst, dstTitle.getRowIndex(), dstTitle.getColumnIndex());
                if (srcAnchor != null && dstAnchor != null) {
                    int anchored = dstAnchor - srcAnchor;
                    if (anchored != rowOff) {
                        log.info("排名页《{}》第 {} å—:标题行偏移 {} ä¸Žã€Œå…¨çœã€é”šç‚¹åç§» {} ä¸ä¸€è‡´ï¼ŒæŒ‰é”šç‚¹å¯¹é½ï¼ˆæºå…¨çœè¡Œ={},目标全省行={})",
                                dstSheetName, b + 1, rowOff, anchored, srcAnchor + 1, dstAnchor + 1);
                    }
                    rowOff = anchored;
                } else {
                    log.warn("排名页《{}》第 {} å—:未识别到「全省」锚点(源={},目标={}),回退按标题行偏移 {} å¯¹é½",
                            dstSheetName, b + 1, srcAnchor, dstAnchor, rowOff);
                }
                int h = rankBlockHeight(src, sr0, sc0);
                int w = rankBlockWidth(src, sr0, sc0);
                anchors.add(new int[]{srcAnchor == null ? -1 : srcAnchor, dstAnchor == null ? -1 : dstAnchor, sc0, colOff, w});
                // 2026-10-12:整块缺数(源侧一个数值都没有,如 9 æœˆç¼ºâ‘¡â‘¢æ—¶çš„周转量排名)→ ç›®æ ‡å—只留标题与标签,
                // æ•°æ®/排名/占比一律留空;否则母版的 RANK/占比公式会在空块上算出「全 1 + #DIV/0!」。
                boolean blockHasData = false;
@@ -4361,6 +4380,34 @@
                    }
                }
            }
        // 2026-10-13 è‡ªæ£€ï¼šç›®æ ‡ã€Œå…¨çœã€è¡Œåº”与源侧一致(偏移错位、汇总区间不对时报警并写进缺数清单)
        for (int[] an : anchors) {
            if (an[0] < 0 || an[1] < 0) continue;
            Row srr = src.getRow(an[0]);
            Row drr = dst.getRow(an[1]);
            if (srr == null || drr == null) continue;
            int badCols = 0;
            StringBuilder ex = new StringBuilder();
            for (int c = 0; c < an[4]; c++) {
                Cell sCell = srr.getCell(an[2] + c);
                Cell dCell = drr.getCell(an[2] + c + an[3]);
                Double sv = sCell == null ? null : rankNumeric(sCell, ev);
                Double dv = dCell == null ? null : rankNumeric(dCell, dstEv);
                if (sv == null && dv == null) continue;
                if (sv == null || dv == null || Math.abs(sv - dv) > Math.max(1.0, Math.abs(sv) * 1e-4)) {
                    badCols++;
                    if (badCols <= 3) {
                        if (ex.length() > 0) ex.append(";");
                        ex.append(colLetter(an[2] + c)).append(" æº=").append(sv).append(" ç›®æ ‡=").append(dv);
                    }
                }
            }
            if (badCols >= 2) {
                String msg = "排名页《" + dstSheetName + "》「全省」行与源侧不一致(" + badCols + " åˆ—):" + ex;
                log.warn(msg);
                if (problems != null) problems.add("生成校验:" + msg);
            }
        }
        }
        return copied;
    }
@@ -4427,6 +4474,23 @@
    }
    /** æŽ’名页里的“块标题”单元格(含“全省分市州”的说明文字),按行、列顺序返回 */
    /** åœ¨ [fromRow, fromRow+40) èŒƒå›´å†…、块起始两列里找整格文本为「全省」的行号(0 åŸºï¼‰ï¼›æ‰¾ä¸åˆ°è¿”回 null。
     *  2026-10-13:排名页块结构可能因母版版式不同而整体上/下移一行,用「全省」行做锚点可自动对齐。 */
    private Integer rankAnchorRow(Sheet sh, int fromRow, int col0) {
        int last = Math.min(sh.getLastRowNum(), fromRow + 40);
        for (int r = fromRow; r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = Math.max(0, col0); c <= col0 + 1; c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != CellType.STRING) continue;
                String v = cell.getStringCellValue();
                if (v != null && v.replace(" ", "").equals("全省")) return r;
            }
        }
        return null;
    }
    private List<XSSFCell> rankTitleCells(Sheet sh) {
        List<XSSFCell> out = new ArrayList<>();
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {