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
252
253
254
255
256
package com.trafficaudit.reportexport.calc;
 
import com.trafficaudit.common.util.RegionUtil;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
 
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
 
/**
 * 汇总整本“数据驱动回填”引擎(M2 台账层,2026-09-09 接入)。
 *
 * 目标:生成任意月份《道路运输量汇总表(整本)》时不再依赖当月人工母版;
 * 母版只当版式底稿,当月值按已验证口径从库现算写入(班线包车 2026-07 与人工母版
 * 350 格 0 差异、公交/出租/轨道轮渡同批 0 差异)。
 *
 * 本版覆盖 4 张台账页:班线包车、公交、出租、轨道轮渡;
 * 网约车依赖《网约车订单及全省总量》拆分输入、货运页在 M3 接入,后续版本补充。
 *
 * 写入规则:
 * 1) 只写“市州明细行”的 2026 年 m 月当月值列(m 月值列 = 第 2+2*(m-1) 列,0 基,即 C 起);
 * 2) 全省行(=17 市州求和公式)与跨页合成页均为公式,不动,Excel 打开自动重算;
 * 3) 2025 同期参照列保留母版缓存值(同比公式自动引用);
 * 4) 源数据整月缺失时收集提示、不回填;市州当月全为 0 时保持母版空单元格。
 */
@Service
public class SummaryWorkbookFiller {
 
    @Resource
    private MidCalc midCalc;
    @Resource
    private PaxCalc paxCalc;
 
    /** 2026 年 1 月当月值列(0 基,即 C 列) */
    private static final int FIRST_MONTH_COL = 2;
 
    /** 回填 1..keepMonths 月台账页,返回写入格数;缺数提示写入 problems */
    public int fillMonthlyLedger(XSSFWorkbook wb, int year, int keepMonths, List<String> problems) {
        if (wb == null || keepMonths <= 0) return 0;
        if (problems == null) problems = new ArrayList<>();
        String yearPrefix = year + "-";
        int filled = 0;
        filled += fillCityRows(wb, "班线包车", midCalc.banxianMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillCityRows(wb, "公交", paxCalc.busMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillCityRows(wb, "出租", paxCalc.taxiMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillTrackFerry(wb, paxCalc.railFerryMonthly(yearPrefix, keepMonths), year, keepMonths, problems);
        return filled;
    }
 
    /** 通用台账页(班线包车/公交/出租)回填:按“列 A 市州名 + 列 B 指标文案”定位行,不写死行号 */
    private int fillCityRows(XSSFWorkbook wb, String sheetName,
                             Map<Integer, Map<String, double[]>> monthly,
                             int year, int keepMonths, List<String> problems, boolean wyc) {
        Sheet sh = wb.getSheet(sheetName);
        if (sh == null) {
            problems.add(sheetName + "页不存在(母版缺失该页),跳过自动回填");
            return 0;
        }
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) {
            problems.add(sheetName + "页未识别到 2026 年累计列,跳过自动回填");
            return 0;
        }
        // 1) 市州起始行(列 A 为规范市州名)
        Map<String, Integer> cityStart = new LinkedHashMap<>();
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = cityOf(row.getCell(0));
            if (city != null && !cityStart.containsKey(city)) cityStart.put(city, r);
        }
        if (cityStart.isEmpty()) {
            problems.add(sheetName + "页未识别到市州行,跳过自动回填");
            return 0;
        }
        List<Integer> starts = new ArrayList<>(cityStart.values());
        int filled = 0;
        List<String> missingMonths = new ArrayList<>();
        for (int m = 1; m <= mMax; m++) {
            if (!monthly.containsKey(m)) missingMonths.add(m + "月");
        }
        if (!missingMonths.isEmpty()) {
            problems.add(sheetName + "缺 " + year + " 年 " + String.join("、", missingMonths) + " 源数据(未回填)");
        }
        for (int i = 0; i < starts.size(); i++) {
            String city = findCityByStart(sh, starts.get(i));
            if (city == null) continue;
            int end = (i + 1 < starts.size()) ? starts.get(i + 1) - 1 : sh.getLastRowNum();
            // 2) 行内各指标所在行(B 列文案 → 指标位 0..3)
            int[] metricRow = new int[]{-1, -1, -1, -1};
            for (int r = starts.get(i); r <= end; r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                int idx = metricIndexOf(text(row.getCell(1)));
                if (idx >= 0 && metricRow[idx] < 0) metricRow[idx] = r;
            }
            if (metricRow[0] < 0 && metricRow[1] < 0) continue;
            for (int m = 1; m <= mMax; m++) {
                Map<String, double[]> mm = monthly.get(m);
                if (mm == null) continue;
                double[] arr = mm.get(city);
                if (arr == null || (arr[0] == 0.0 && arr[1] == 0.0)) continue;
                int col = FIRST_MONTH_COL + 2 * (m - 1);
                for (int idx = 0; idx < 4; idx++) {
                    if (metricRow[idx] < 0) continue;
                    double val = arr[idx];
                    if (val == 0.0) continue;
                    filled += setNumeric(sh, metricRow[idx], col, val);
                }
            }
        }
        return filled;
    }
 
    /** 轨道轮渡页(轨道=武汉/黄石;轮渡仅武汉)专用回填 */
    private int fillTrackFerry(XSSFWorkbook wb, Map<Integer, Map<String, double[]>> monthly,
                               int year, int keepMonths, List<String> problems) {
        String sheetName = "轨道、轮渡";
        Sheet sh = wb.getSheet(sheetName);
        if (sh == null) return 0;
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) return 0;
        // 轨道块与轮渡块标题行(列 A 文案)
        int trackTitle = -1, ferryTitle = -1;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            String a = text(sh.getRow(r) == null ? null : sh.getRow(r).getCell(0));
            if (a == null) continue;
            if (trackTitle < 0 && a.startsWith("轨道客运量")) trackTitle = r;
            else if (ferryTitle < 0 && a.startsWith("轮渡客运量")) ferryTitle = r;
        }
        if (trackTitle < 0 || ferryTitle < 0) {
            problems.add(sheetName + "页未识别到轨道/轮渡块标题,跳过自动回填");
            return 0;
        }
        int filled = 0;
        filled += fillFerryBlock(sh, monthly, trackTitle + 3, ferryTitle - 1, 0, mMax, problems, sheetName);
        filled += fillFerryBlock(sh, monthly, ferryTitle + 3, sh.getLastRowNum(), 2, mMax, problems, sheetName);
        return filled;
    }
 
    /** 填充一段“标题行后数据区”:base 为该块指标位基(轨道 0 / 轮渡 2);block 内
     *  每市州 2 行:客运量行(列 A=市州名)、旅客周转量行(A 空,B 含“周转量”) */
    private int fillFerryBlock(Sheet sh, Map<Integer, Map<String, double[]>> monthly,
                               int start, int end, int base, int mMax,
                               List<String> problems, String sheetName) {
        int filled = 0;
        // 收集市州客运量行
        Map<String, Integer> paxRow = new LinkedHashMap<>();
        for (int r = start; r <= end; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = cityOf(row.getCell(0));
            if (city != null) paxRow.put(city, r);
        }
        for (Map.Entry<String, Integer> e : paxRow.entrySet()) {
            String city = e.getKey();
            int rp = e.getValue();
            int rt = findTurnoverRow(sh, rp + 1, Math.min(end, rp + 6));
            if (rt < 0) continue;
            for (int m = 1; m <= mMax; m++) {
                Map<String, double[]> mm = monthly.get(m);
                if (mm == null) continue;
                double[] arr = mm.get(city);
                if (arr == null) continue;
                int col = FIRST_MONTH_COL + 2 * (m - 1);
                double pax = arr[base];
                double turn = arr[base + 1];
                if (pax != 0.0) filled += setNumeric(sh, rp, col, pax);
                if (turn != 0.0) filled += setNumeric(sh, rt, col, turn);
            }
        }
        return filled;
    }
 
    private int findTurnoverRow(Sheet sh, int from, int to) {
        for (int r = from; r <= to; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = text(row.getCell(0));
            if (a != null && !a.trim().isEmpty()) return -1; // 进入下一市州块
            String b = text(row.getCell(1));
            if (b != null && b.contains("周转量")) return r;
        }
        return -1;
    }
 
    /** 写数值:格不存在则建格并复制同行 1 月列样式;返回 1 */
    private int setNumeric(Sheet sh, int r, int col, double val) {
        Row row = sh.getRow(r);
        if (row == null) row = sh.createRow(r);
        Cell c = row.getCell(col);
        if (c == null) {
            c = row.createCell(col);
            Cell ref = row.getCell(FIRST_MONTH_COL);
            if (ref != null && ref.getCellStyle() != null) c.setCellStyle(ref.getCellStyle());
        }
        c.setCellValue(val);
        return 1;
    }
 
    /** 行 B 列文案 → 指标位:0 客运量 / 1 旅客周转量 / 2 其中城市内客运量或其中个体客运量 / 3 对应周转量 */
    private int metricIndexOf(String b) {
        if (b == null) return -1;
        if (b.contains("其中个体旅客周转量") || b.contains("其中城市内旅客周转量")) return 3;
        if (b.contains("其中个体客运量") || b.contains("其中城市内客运量")) return 2;
        if (b.contains("旅客周转量")) return 1;
        if (b.contains("客运量")) return 0;
        return -1;
    }
 
    /** 列 A 文案 → 规范市州名(湖北省/全省、非市州行返回 null) */
    private String cityOf(Cell a) {
        String t = text(a);
        if (t == null || t.trim().isEmpty()) return null;
        String norm = RegionUtil.normalizeCityName(t.trim());
        return RegionUtil.cityList().contains(norm) ? norm : null;
    }
 
    private String findCityByStart(Sheet sh, int startRow) {
        Row row = sh.getRow(startRow);
        return row == null ? null : cityOf(row.getCell(0));
    }
 
    /** 月份容量:在表头(前 5 行)找“2026…累计”列,容量=(累计列-1 月列)/2 */
    private int monthColumnCapacity(Sheet sh) {
        for (int r = 0; r <= 4 && r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (Cell c : row) {
                if (c == null) continue;
                String t = text(c);
                if (t != null && t.contains("2026") && t.contains("累计") && c.getColumnIndex() > FIRST_MONTH_COL) {
                    return (c.getColumnIndex() - FIRST_MONTH_COL) / 2;
                }
            }
        }
        return 7;
    }
 
    private String text(Cell c) {
        if (c == null) return null;
        switch (c.getCellType()) {
            case STRING: return c.getStringCellValue();
            case NUMERIC: return Double.toString(c.getNumericCellValue());
            default: return null;
        }
    }
}