zhizhijie
5 天以前 7f958f5bea92039eaf6d4f2af3cc0c46a86b8c94
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
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
 
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
/**
 * 货运口径「导出前基数体检」(B4,2026-10-12 新增 6 条规则)。
 *
 * <p>背景:规下 = 全口径(②《货运量周转量表》)− 规上(①H203-2 企业月报÷10000)——是**倒减**,
 * 因此①漏报/缺报会让规下虚高,极端情况还会算出负数。本类把这些异常在导出前挑出来,
 * 供日志/弹窗/《数据缺失清单》页使用(同一套结果也供生成主链的 dbProblems 复用)。
 *
 * <p>规则:
 * <ol>
 *   <li>某市州规下为负(货量或周转量);</li>
 *   <li>②该市州有全口径、①无规上记录 → 规下等于全口径,明显虚高;</li>
 *   <li>②全口径小于①规上(逻辑不可能);</li>
 *   <li>Σ17 市州 ≠ ②的"湖北省"行(超容差 0.01);</li>
 *   <li>某市州全口径「本月与上月完全相同」(疑重复导入,两侧 ≥ 50 万吨/万吨公里);</li>
 *   <li>某市州全口径同比异常(|去年同月→本月| &gt; 50% 且两侧 ≥ 50 万吨/万吨公里);</li>
 *   <li>累计勾稽:①周转量 1..M 月求和 ÷10000 与③《拆分表》CUMULATIVE.above_scale_turnover 不一致(超容差 0.01)。</li>
 * </ol>
 */
@Slf4j
@Service
public class FreightReadinessCheck {
 
    private static final double TOL = 0.01;
    private static final double YOY_RATIO = 0.50;
    /** 环比检查的绝对门槛:两侧都要 ≥ 该值才判异常(避免神农架林区这类小基数噪声) */
    private static final double CHAIN_FLOOR = 50.0;
 
    @Resource
    private H2032EnterpriseMonthlyMapper h2032Mapper;
    @Resource
    private FreightTurnoverImportMapper freightTurnoverMapper;
    @Resource
    private ScaleSplitTransportMapper scaleSplitMapper;
 
    public List<String> check(String period) {
        List<String> issues = new ArrayList<>();
        if (period == null || period.length() < 7) return issues;
        int year = Integer.parseInt(period.substring(0, 4));
        int month = Integer.parseInt(period.substring(5, 7));
 
        // ① 规上(吨)按市州+月份
        Map<String, Map<Integer, Double>> aboveFreight = new HashMap<>();
        Map<String, Map<Integer, Double>> aboveTurnover = new HashMap<>();
        for (H2032EnterpriseMonthly rec : h2032Mapper.selectList(
                new LambdaQueryWrapper<H2032EnterpriseMonthly>()
                        .likeRight(H2032EnterpriseMonthly::getReportPeriod, year))) {
            String rp = rec.getReportPeriod();
            if (rp == null || rp.compareTo(period) > 0) continue;
            String city = RegionUtil.cityByCode(rec.getRegionCode());
            if (city == null) continue;
            int m = Integer.parseInt(rp.substring(5, 7));
            aboveFreight.computeIfAbsent(city, k -> new HashMap<>())
                    .merge(m, rec.getFreightTotal() == null ? 0.0 : rec.getFreightTotal(), Double::sum);
            aboveTurnover.computeIfAbsent(city, k -> new HashMap<>())
                    .merge(m, rec.getTurnoverTotal() == null ? 0.0 : rec.getTurnoverTotal(), Double::sum);
        }
 
        // ② 全口径:市州行 + 湖北省行(当期)
        Map<String, FreightTurnoverImport> ft = new HashMap<>();
        FreightTurnoverImport province = null;
        for (FreightTurnoverImport e : freightTurnoverMapper.selectList(
                new LambdaQueryWrapper<FreightTurnoverImport>()
                        .eq(FreightTurnoverImport::getReportPeriod, period))) {
            String city = RegionUtil.normalizeCityName(e.getRegionName());
            if (city == null) continue;
            if (FreightCalc.PROVINCE.equals(city)) province = e;
            else ft.put(city, e);
        }
        // 上月全口径(用于环比)
        String prevPeriod = prevPeriod(period);
        Map<String, FreightTurnoverImport> ftPrev = new HashMap<>();
        for (FreightTurnoverImport e : freightTurnoverMapper.selectList(
                new LambdaQueryWrapper<FreightTurnoverImport>()
                        .eq(FreightTurnoverImport::getReportPeriod, prevPeriod))) {
            String city = RegionUtil.normalizeCityName(e.getRegionName());
            if (city != null && !FreightCalc.PROVINCE.equals(city)) ftPrev.put(city, e);
        }
 
        // ③ 拆分表:当月/累计 规上与合计周转量
        Map<String, ScaleSplitTransport> splitCum = new HashMap<>();
        for (ScaleSplitTransport rec : scaleSplitMapper.selectList(
                new LambdaQueryWrapper<ScaleSplitTransport>()
                        .eq(ScaleSplitTransport::getReportPeriod, period))) {
            String city = RegionUtil.normalizeCityName(rec.getRegionName());
            if (city == null || FreightCalc.PROVINCE.equals(city)) continue;
            if ("CUMULATIVE".equals(rec.getPeriodType())) splitCum.put(city, rec);
        }
 
        double sumFreight = 0.0, sumTurnover = 0.0;
        for (String city : RegionUtil.cityList()) {
            FreightTurnoverImport row = ft.get(city);
            Double totalF = row == null ? null : monthField(row, month, true);
            Double totalT = row == null ? null : monthField(row, month, false);
            Double abF = aboveFreight.containsKey(city) ? aboveFreight.get(city).get(month) : null;
            abF = abF == null ? null : abF / 10000.0;
            Double abT = aboveTurnover.containsKey(city) && aboveTurnover.get(city).get(month) != null
                    ? aboveTurnover.get(city).get(month) / 10000.0 : null;
 
            if (totalF != null) sumFreight += totalF;
            if (totalT != null) sumTurnover += totalT;
 
            if (totalF == null && abF == null) continue;      // 两侧都没数 → 交给缺数清单(另算)
            if (totalF == null && abF != null) {
                issues.add(String.format("货运体检:%s %s 缺②全口径(规上 %.4f 在、全口径缺)→ 规下无法计算"
                        + "(需先导《货运量周转量表》当月档)", period, city, abF));
            }
            if (totalT == null && abT != null) {
                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));
            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));
 
            FreightTurnoverImport prev = ftPrev.get(city);
            if (prev != null) {
                int prevMonth = month == 1 ? 12 : month - 1;
                Double prevF = monthField(prev, prevMonth, true);
                Double prevT = monthField(prev, prevMonth, false);
                if (totalF != null && sameValue(prevF, totalF) && Math.abs(totalF) >= CHAIN_FLOOR) {
                    issues.add(String.format("货运体检:%s %s 全口径货运量本月与上月完全相同(%.4f),疑重复导入", period, city, totalF));
                }
                if (totalT != null && sameValue(prevT, totalT) && Math.abs(totalT) >= CHAIN_FLOOR) {
                    issues.add(String.format("货运体检:%s %s 全口径周转量本月与上月完全相同(%.4f),疑重复导入", period, city, totalT));
                }
            }
            if (row != null) {
                Double lastF = lastMonthField(row, month, true);
                Double lastT = lastMonthField(row, month, false);
                if (lastF != null && totalF != null && Math.abs(lastF) >= CHAIN_FLOOR && Math.abs(totalF) >= CHAIN_FLOOR) {
                    double ratio = totalF / lastF - 1.0;
                    if (Math.abs(ratio) > YOY_RATIO) {
                        issues.add(String.format("货运体检:%s %s 全口径货运量同比异常(去年同月 %.4f -> 本月 %.4f,%.1f%%)", period, city, lastF, totalF, ratio * 100));
                    }
                }
                if (lastT != null && totalT != null && Math.abs(lastT) >= CHAIN_FLOOR && Math.abs(totalT) >= CHAIN_FLOOR) {
                    double ratio = totalT / lastT - 1.0;
                    if (Math.abs(ratio) > YOY_RATIO) {
                        issues.add(String.format("货运体检:%s %s 全口径周转量同比异常(去年同月 %.4f -> 本月 %.4f,%.1f%%)", period, city, lastT, totalT, ratio * 100));
                    }
                }
            }
 
ScaleSplitTransport cum = splitCum.get(city);
            if (cum != null && cum.getAboveScaleTurnover() != null) {
                Map<Integer, Double> buckets = aboveTurnover.get(city);
                if (buckets != null) {
                    double sum = 0.0;
                    for (int m = 1; m <= month; m++) sum += buckets.getOrDefault(m, 0.0);
                    double fromH2032 = sum / 10000.0;
                    double fromSplit = cum.getAboveScaleTurnover();
                    if (Math.abs(fromH2032 - fromSplit) > TOL) {
                        issues.add(String.format("货运体检:%s %s 规上周转量累计勾稽不一致(①逐月求和 %.4f vs ③累计 %.4f)", period, city, fromH2032, fromSplit));
                    }
                }
            }
        }
        if (province != null) {
            Double pf = monthField(province, month, true);
            Double pt = monthField(province, month, false);
            if (pf != null && Math.abs(pf - sumFreight) > TOL) {
                issues.add(String.format("货运体检:%s Σ17市州货运量 %.4f ≠ ②表湖北省行 %.4f(差 %.4f)", period, sumFreight, pf, sumFreight - pf));
            }
            if (pt != null && Math.abs(pt - sumTurnover) > TOL) {
                issues.add(String.format("货运体检:%s Σ17市州周转量 %.4f ≠ ②表湖北省行 %.4f(差 %.4f)", period, sumTurnover, pt, sumTurnover - pt));
            }
        }
        if (!issues.isEmpty()) {
            log.warn("货运体检(B4):{} 命中 {} 条问题", period, issues.size());
        }
        return issues;
    }
 
    /** ②表按 月号 取当月货运量/周转量(与 FreightCalc 同一取法) */
    private Double monthField(FreightTurnoverImport e, int m, boolean freight) {
        switch (m) {
            case 1: return freight ? e.getFreightM01() : e.getTurnoverM01();
            case 2: return freight ? e.getFreightM02() : e.getTurnoverM02();
            case 3: return freight ? e.getFreightM03() : e.getTurnoverM03();
            case 4: return freight ? e.getFreightM04() : e.getTurnoverM04();
            case 5: return freight ? e.getFreightM05() : e.getTurnoverM05();
            case 6: return freight ? e.getFreightM06() : e.getTurnoverM06();
            case 7: return freight ? e.getFreightM07() : e.getTurnoverM07();
            case 8: return freight ? e.getFreightM08() : e.getTurnoverM08();
            case 9: return freight ? e.getFreightM09() : e.getTurnoverM09();
            case 10: return freight ? e.getFreightM10() : e.getTurnoverM10();
            case 11: return freight ? e.getFreightM11() : e.getTurnoverM11();
            case 12: return freight ? e.getFreightM12() : e.getTurnoverM12();
            default: return null;
        }
    }
 
    /** ②表去年同月值 */
    private Double lastMonthField(FreightTurnoverImport e, int m, boolean freight) {
        switch (m) {
            case 1: return freight ? e.getLastFreightM01() : e.getLastTurnoverM01();
            case 2: return freight ? e.getLastFreightM02() : e.getLastTurnoverM02();
            case 3: return freight ? e.getLastFreightM03() : e.getLastTurnoverM03();
            case 4: return freight ? e.getLastFreightM04() : e.getLastTurnoverM04();
            case 5: return freight ? e.getLastFreightM05() : e.getLastTurnoverM05();
            case 6: return freight ? e.getLastFreightM06() : e.getLastTurnoverM06();
            case 7: return freight ? e.getLastFreightM07() : e.getLastTurnoverM07();
            case 8: return freight ? e.getLastFreightM08() : e.getLastTurnoverM08();
            case 9: return freight ? e.getLastFreightM09() : e.getLastTurnoverM09();
            case 10: return freight ? e.getLastFreightM10() : e.getLastTurnoverM10();
            case 11: return freight ? e.getLastFreightM11() : e.getLastTurnoverM11();
            case 12: return freight ? e.getLastFreightM12() : e.getLastTurnoverM12();
            default: return null;
        }
    }
 
    private boolean sameValue(Double a, Double b) {
        if (a == null || b == null) return false;
        return Math.abs(a - b) < 1e-9;
    }
 
    private String prevPeriod(String period) {
        int y = Integer.parseInt(period.substring(0, 4));
        int m = Integer.parseInt(period.substring(5, 7));
        if (m == 1) return (y - 1) + "-12";
        return String.format("%04d-%02d", y, m - 1);
    }
 
    public String describe(String period) {
        List<String> issues = check(period);
        return issues.isEmpty() ? "货运体检:未发现问题" : String.join("\n", issues);
    }
}