feat: 汇总大表数据驱动 M1-货运——新增 FreightCalc 计算服务(B1 口径)与 7 月回归测试报告
2个文件已添加
266 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_M1-货运取数链路与7月回归_2026-09-08.md 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightCalc.java 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_M1-»õÔËÈ¡ÊýÁ´Â·Óë7Ô»عé_2026-09-08.md
New file
@@ -0,0 +1,41 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šM1-货运 å–数链路核对与 7 æœˆå›žå½’(2026-09-08)
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
汇总大表数据驱动改造 M1 ç¬¬ä¸€å—:把“货运口径”(规上/规下/合计 Ã— è´§è¿é‡/周转量 Ã— å½“月/累计)固化为可复用的 Java è®¡ç®—服务 `FreightCalc`(`traffic-audit-server/.../reportexport/calc/FreightCalc.java`),并验证:**仅凭数据库即可重算出 7 æœˆå·²ä¿®æ­£æ¯ç‰ˆä¸­è´§è¿é‡æŽ’名、货运周转量排名两张成品页的全部规上/规下数值(逐格 diff=0)**,为后续 SummaryFiller å†™å›žæ¯ç‰ˆæä¾›åŒæºçŸ©é˜µã€‚
## äºŒã€æµ‹è¯•环境
- åº“:`traffic_audit`(127.0.0.1:3308),数据 = 2026-07 å·²å¯¼å…¥çŽ°çŠ¶ï¼›
- æ¯ç‰ˆåŸºçº¿ï¼š`docs/生成汇总大表/2026å¹´7月道路运输量汇总表.xlsx`(2026-07 å·²ä¿®æ­£ç‰ˆï¼‰ï¼›
- å·¥å…·ï¼šmysql CLI å–æ•° + Python é‡ç®—比对;后端 `mvn -q compile` é€šè¿‡ï¼ˆexit=0)。
## ä¸‰ã€å–数口径(与既有 exportFreightRank/exportTurnoverRank åŠç¡®è®¤å›žæ‰§ B1 ä¸€è‡´ï¼‰
| æŒ‡æ ‡ | æ¥æº |
|---|---|
| è§„上货运量(当月/累计,万吨) | `h2032_enterprise_monthly.freight_total`(吨)按市州汇总 Ã·10000 |
| åˆè®¡è´§è¿é‡ï¼ˆä¸‡å¨ï¼‰ | `freight_turnover_import.freight_mMM`(当月)/ `freight_m01..mMM`(累计) |
| è§„下货运量 | åˆè®¡ âˆ’ è§„上(拆分表不存货运量列,不可直接取) |
| è§„上/合计周转量 | `scale_split_transport` MONTH / CUMULATIVE çš„ `above_scale_turnover` / `total_turnover` |
| è§„下周转量 | åˆè®¡ âˆ’ è§„上(与拆分表 below åˆ—核对一致) |
| æ¹–北省行 | 17 å¸‚州之和(与母版“全省行=Σ城市行”公式一致) |
## å››ã€ç”¨ä¾‹è¡¨
| # | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | è´§è¿é‡æŽ’名·累计(1-7月) | åº“ 2026-01~07 | ä¸Žæ¯ç‰ˆ B4:B21(规上)、G4:G21(规下)逐格一致 | diff=0 | é€šè¿‡ |
| 2 | è´§è¿é‡æŽ’名·当月(7月) | åº“ 2026-07 | ä¸Žæ¯ç‰ˆ B25:B42、G25:G42 é€æ ¼ä¸€è‡´ | diff=0 | é€šè¿‡ |
| 3 | è´§è¿å‘¨è½¬é‡æŽ’名·累计 | scale_split CUMULATIVE | ä¸Žæ¯ç‰ˆ B5:B22、G5:G22 ä¸€è‡´ | diff=0 | é€šè¿‡ |
| 4 | è´§è¿å‘¨è½¬é‡æŽ’名·当月 | scale_split MONTH | ä¸Žæ¯ç‰ˆ B27:B44、G27:G44 ä¸€è‡´ | diff=0 | é€šè¿‡ |
| 5 | å…¨çœè¡Œä¸Ž Î£ åŸŽå¸‚一致性 | 17 å¸‚州求和 | å…¨çœè§„上当月=3754.7823(母版) | è®¡=3754.7823 | é€šè¿‡ |
| 6 | Java ç¼–译 | `mvn -q -DskipTests compile` | æ— é”™è¯¯ | exit=0 | é€šè¿‡ |
## äº”、发现的问题与处置
1. H2032 æŒ‰ `region_code` æ±‡æ€»æ—¶å­˜åœ¨â€œä¸€å¸‚多区县代码”,初版脚本误用覆盖而非累加 â†’ å·²æ”¹ç´¯åŠ ï¼Œ0 å·®å¼‚。
2. ç´¯è®¡åˆè®¡é¦–版只加到 6 æœˆï¼ˆæ¼ m07)→ å·²æ”¹ 1..7 å…¨åŠ ï¼Œ0 å·®å¼‚。
3. `freight_turnover_import.region_name` å­˜åœ¨â€œæž—区/神农架林区/天门/天门市”等别名 â†’ ç»Ÿä¸€ç» RegionUtil åˆ«åå½’一后比对,0 å·®å¼‚。
4. å‘¨è½¬é‡ä¸¤é¡µåœ¨ä»»ä½•版本比对中均 0 å·®å¼‚(拆分表口径稳定)。
(上述均为回归脚本问题,非业务口径问题;已在 Python å›žå½’与 Java å®žçŽ°ä¸­åŒæ­¥ä¿®æ­£ã€‚ï¼‰
## å…­ã€ç»“论与下一步
- ç»“论:货运口径链路可从库完整复算 7 æœˆæ¯ç‰ˆï¼Œå£å¾„锁定;`FreightCalc` å·²å…¥åº“并通过编译。
- è¯´æ˜Žï¼šåŒæ¯”未在本服务计算——按 Q1 ç”±å†™å›žå±‚以母版缓存 2025 å‚照组为基准(去年同期 DB åˆ—为空,已核对)。
- ä¸‹ä¸€æ­¥ï¼šâ‘  M1-PaxCalc(公交/出租/轨道轮渡/网约/班线包车/城市客运口径,用城市客运三表 7 æœˆå›žå½’);② M1-MidCalc;③ M2 SummaryFiller å†™å›žå°è´¦+成品页并逐格 diff;④ 8 æœˆå®žå¼¹ã€‚
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/FreightCalc.java
New file
@@ -0,0 +1,225 @@
package com.trafficaudit.reportexport.calc;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.trafficaudit.common.util.RegionUtil;
import com.trafficaudit.dataimport.entity.FreightTurnoverImport;
import com.trafficaudit.dataimport.entity.H2032EnterpriseMonthly;
import com.trafficaudit.dataimport.entity.ScaleSplitTransport;
import com.trafficaudit.dataimport.mapper.FreightTurnoverImportMapper;
import com.trafficaudit.dataimport.mapper.H2032EnterpriseMonthlyMapper;
import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * è´§è¿å£å¾„计算服务(汇总大表数据驱动改造 M1-货运,2026-09-08)。
 *
 * å£å¾„(决策基线 docs/确认回执汇总…_2026-09-07.md B1;2026-07 ä¸Žå·²ä¿®æ­£æ¯ç‰ˆä¸¤å¼ æˆå“é¡µ
 * è§„上/规下 å½“月+累计 é€æ ¼å›žå½’ 0 å·®å¼‚):
 * 1. è§„上货运量 = h2032_enterprise_monthly.freight_total(吨)按 17 å¸‚州汇总 Ã·10000(万吨)。
 * 2. åˆè®¡è´§è¿é‡ = freight_turnover_import.freight_mMM(当月)/ freight_m01..mMM(累计)。
 * 3. è§„下货运量 = åˆè®¡ âˆ’ è§„上(拆分表不存货运量列,不能直接取规下)。
 * 4. è§„上/合计周转量 = scale_split_transport MONTH|CUMULATIVE çš„ above_scale_turnover / total_turnover
 *    ï¼ˆåˆè®¡ä¸Ž freight_turnover_import.turnover_mMM åŒæºï¼Œå·²æ ¸å¯¹ä¸€è‡´ï¼‰ï¼›è§„下周转 = åˆè®¡ âˆ’ è§„上。
 * 5. åŒæ¯”(去年同口径)不在本服务计算:按 Q1 ä»¥æ¯ç‰ˆç¼“å­˜ 2025 å‚照组为基准,由写回层填表时计算。
 *
 * è¾“出 key = RegionUtil è§„范市州名(武汉市…神农架林区)+ PROVINCE(湖北省,全省行 = 17 å¸‚州之和,
 * ä¸Žæ¯ç‰ˆâ€œå…¨çœè¡Œ=Σ城市行”公式一致)。
 */
@Service
public class FreightCalc {
    public static final String PROVINCE = "湖北省";
    /** å•个期间(当月或累计)的货运量/周转量三口径指标;无数据为 null */
    public static class FreightMetrics {
        private final Double aboveFreight;   // è§„上货运量(万吨)
        private final Double belowFreight;   // è§„下货运量(万吨)
        private final Double totalFreight;   // è§„上+规下货运量(万吨)
        private final Double aboveTurnover;  // è§„上货物周转量(万吨公里)
        private final Double belowTurnover;  // è§„下货物周转量(万吨公里)
        private final Double totalTurnover;  // è§„上+规下货物周转量(万吨公里)
        public FreightMetrics(Double aboveFreight, Double belowFreight, Double totalFreight,
                              Double aboveTurnover, Double belowTurnover, Double totalTurnover) {
            this.aboveFreight = aboveFreight;
            this.belowFreight = belowFreight;
            this.totalFreight = totalFreight;
            this.aboveTurnover = aboveTurnover;
            this.belowTurnover = belowTurnover;
            this.totalTurnover = totalTurnover;
        }
        public Double getAboveFreight() { return aboveFreight; }
        public Double getBelowFreight() { return belowFreight; }
        public Double getTotalFreight() { return totalFreight; }
        public Double getAboveTurnover() { return aboveTurnover; }
        public Double getBelowTurnover() { return belowTurnover; }
        public Double getTotalTurnover() { return totalTurnover; }
    }
    /** å…¨é‡è´§è¿çŸ©é˜µï¼šmonth=当月,cumulative=截至 period æœˆç´¯è®¡ */
    public static class FreightMatrix {
        private final String period;
        private final Map<String, FreightMetrics> month;
        private final Map<String, FreightMetrics> cumulative;
        public FreightMatrix(String period, Map<String, FreightMetrics> month, Map<String, FreightMetrics> cumulative) {
            this.period = period;
            this.month = month;
            this.cumulative = cumulative;
        }
        public String getPeriod() { return period; }
        public Map<String, FreightMetrics> getMonth() { return month; }
        public Map<String, FreightMetrics> getCumulative() { return cumulative; }
    }
    @Resource
    private H2032EnterpriseMonthlyMapper h2032Mapper;
    @Resource
    private FreightTurnoverImportMapper freightTurnoverMapper;
    @Resource
    private ScaleSplitTransportMapper scaleSplitMapper;
    /** è®¡ç®— period(如 2026-07)的当月与累计货运矩阵 */
    public FreightMatrix calc(String period) {
        int year = Integer.parseInt(period.substring(0, 4));
        int month = Integer.parseInt(period.substring(5, 7));
        // H2032 è§„上货运量(吨):市州 -> æœˆæ¡¶
        Map<String, Map<Integer, Double>> h2032TonByCityMonth = new HashMap<>();
        List<H2032EnterpriseMonthly> h2032List = h2032Mapper.selectList(
            new LambdaQueryWrapper<H2032EnterpriseMonthly>().likeRight(H2032EnterpriseMonthly::getReportPeriod, year));
        for (H2032EnterpriseMonthly rec : h2032List) {
            String rp = rec.getReportPeriod();
            if (rp == null || rp.compareTo(period) > 0) continue;
            String city = RegionUtil.cityByCode(rec.getRegionCode());
            if (city == null) continue;
            double freight = rec.getFreightTotal() == null ? 0.0 : rec.getFreightTotal();
            h2032TonByCityMonth.computeIfAbsent(city, k -> new HashMap<>())
                .merge(Integer.parseInt(rp.substring(5, 7)), freight, Double::sum);
        }
        // freight_turnover_import:合计货运量/周转量(仅存城市行;湖北省行由求和得出)
        Map<String, FreightTurnoverImport> ftMap = new HashMap<>();
        for (FreightTurnoverImport e : freightTurnoverMapper.selectList(
            new LambdaQueryWrapper<FreightTurnoverImport>()
                .eq(FreightTurnoverImport::getReportPeriod, period))) {
            String city = RegionUtil.normalizeCityName(e.getRegionName());
            if (city != null && !PROVINCE.equals(city)) ftMap.put(city, e);
        }
        // scale_split_transport:规上/合计周转量
        Map<String, ScaleSplitTransport> splitMonth = new HashMap<>();
        Map<String, ScaleSplitTransport> splitCumulative = new HashMap<>();
        for (ScaleSplitTransport rec : scaleSplitMapper.selectList(
            new LambdaQueryWrapper<ScaleSplitTransport>()
                .eq(ScaleSplitTransport::getReportPeriod, period))) {
            String city = RegionUtil.normalizeCityName(rec.getRegionName());
            if (city == null || PROVINCE.equals(city)) continue;
            if ("MONTH".equals(rec.getPeriodType())) splitMonth.put(city, rec);
            else if ("CUMULATIVE".equals(rec.getPeriodType())) splitCumulative.put(city, rec);
        }
        Map<String, FreightMetrics> monthMap = new LinkedHashMap<>();
        Map<String, FreightMetrics> cumulativeMap = new LinkedHashMap<>();
        for (String city : RegionUtil.cityList()) {
            monthMap.put(city, buildMetrics(city, month, h2032TonByCityMonth, ftMap, splitMonth, false));
            cumulativeMap.put(city, buildMetrics(city, month, h2032TonByCityMonth, ftMap, splitCumulative, true));
        }
        monthMap.put(PROVINCE, sumOf(monthMap));
        cumulativeMap.put(PROVINCE, sumOf(cumulativeMap));
        return new FreightMatrix(period, monthMap, cumulativeMap);
    }
    /** å•城市指标。cumulative=true æ—¶ H2032 å– 1..month å…¨æ¡¶ã€åˆè®¡å– m01..mMM ä¹‹å’Œï¼›false æ—¶åªå–当月。 */
    private FreightMetrics buildMetrics(String city, int month,
                                        Map<String, Map<Integer, Double>> h2032TonByCityMonth,
                                        Map<String, FreightTurnoverImport> ftMap,
                                        Map<String, ScaleSplitTransport> splitMap,
                                        boolean cumulative) {
        Double aboveFreight = null;
        Map<Integer, Double> h2032Buckets = h2032TonByCityMonth.get(city);
        if (h2032Buckets != null) {
            double ton;
            if (cumulative) {
                ton = 0.0;
                for (int m = 1; m <= month; m++) ton += h2032Buckets.getOrDefault(m, 0.0);
            } else {
                ton = h2032Buckets.getOrDefault(month, 0.0);
            }
            aboveFreight = ton / 10000.0;
        }
        FreightTurnoverImport ft = ftMap.get(city);
        Double totalFreight = null;
        Double totalTurnoverFromFt = null;
        if (ft != null) {
            if (cumulative) {
                double f = 0.0, t = 0.0;
                for (int m = 1; m <= month; m++) {
                    f += monthField(ft, m, true);
                    t += monthField(ft, m, false);
                }
                totalFreight = f;
                totalTurnoverFromFt = t;
            } else {
                totalFreight = monthField(ft, month, true);
                totalTurnoverFromFt = monthField(ft, month, false);
            }
        }
        ScaleSplitTransport split = splitMap.get(city);
        Double aboveTurnover = split == null ? null : split.getAboveScaleTurnover();
        Double totalTurnover = split != null && split.getTotalTurnover() != null
            ? split.getTotalTurnover() : totalTurnoverFromFt;
        Double belowFreight = totalFreight == null || aboveFreight == null ? null : totalFreight - aboveFreight;
        Double belowTurnover = totalTurnover == null ? null
            : totalTurnover - (aboveTurnover == null ? 0.0 : aboveTurnover);
        return new FreightMetrics(aboveFreight, belowFreight, totalFreight,
                                  aboveTurnover, belowTurnover, totalTurnover);
    }
    /** æ¹–北省行 = 17 å¸‚州之和(与母版全省行 Î£ å…¬å¼ä¸€è‡´ï¼›å…¨å¸‚州无数据时输出 0) */
    private FreightMetrics sumOf(Map<String, FreightMetrics> byCity) {
        double af = 0.0, bf = 0.0, tf = 0.0, at = 0.0, bt = 0.0, tt = 0.0;
        for (String city : RegionUtil.cityList()) {
            FreightMetrics m = byCity.get(city);
            if (m == null) continue;
            af += nz(m.getAboveFreight());
            bf += nz(m.getBelowFreight());
            tf += nz(m.getTotalFreight());
            at += nz(m.getAboveTurnover());
            bt += nz(m.getBelowTurnover());
            tt += nz(m.getTotalTurnover());
        }
        return new FreightMetrics(af, bf, tf, at, bt, tt);
    }
    private double monthField(FreightTurnoverImport e, int m, boolean freight) {
        switch (m) {
            case 1: return freight ? nz(e.getFreightM01()) : nz(e.getTurnoverM01());
            case 2: return freight ? nz(e.getFreightM02()) : nz(e.getTurnoverM02());
            case 3: return freight ? nz(e.getFreightM03()) : nz(e.getTurnoverM03());
            case 4: return freight ? nz(e.getFreightM04()) : nz(e.getTurnoverM04());
            case 5: return freight ? nz(e.getFreightM05()) : nz(e.getTurnoverM05());
            case 6: return freight ? nz(e.getFreightM06()) : nz(e.getTurnoverM06());
            case 7: return freight ? nz(e.getFreightM07()) : nz(e.getTurnoverM07());
            case 8: return freight ? nz(e.getFreightM08()) : nz(e.getTurnoverM08());
            case 9: return freight ? nz(e.getFreightM09()) : nz(e.getTurnoverM09());
            case 10: return freight ? nz(e.getFreightM10()) : nz(e.getTurnoverM10());
            case 11: return freight ? nz(e.getFreightM11()) : nz(e.getTurnoverM11());
            case 12: return freight ? nz(e.getFreightM12()) : nz(e.getTurnoverM12());
            default: return 0.0;
        }
    }
    private double nz(Double v) { return v == null ? 0.0 : v; }
}