| HANDOFF.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| docs/database.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
HANDOFF.md
@@ -4,6 +4,26 @@ > **每天结束时**:把当天进展、踩过的坑、新需求、未完成事项更新到本文件,然后可以放心开新对话。 > **协作约定(2026-08-31)**:说「提交」= 本地 commit + 推送到远端 git(origin main,Gitblit),不再只做本地 commit。 ## 最新(2026-10-06) ### 10-06:普通国省道「环比基期」改为人工备用值(不再参与计算) - **背景**:10-04 鄂州樊家庄站中小客流量断崖下跌(35419 -> 8856),领导要求剔除。手工稿快报的做法是:先用剔站后的数值算出 10-04 环比,把 10-03 国省道值挪到「环比基期」C 列备用,再把 10-03 恢复原值以免影响累计。用户明确口径:**「环比基期」只用于数据备用,不参与任何计算**。 - **旧行为(错)**:`HolidayStatService.nationalProvincialRingRatio` 在 2026 年只要 `comparison_base` 有值就按「当日 / 环比基期 - 1」算国省道环比;导出时 E 列写 `=(D{行}-C{行})/C{行}`。按手工稿的 C 列摆法(基期放在被比日那行),这样算还会错位(10-03 环比被算成 +1.11%)。 - **新行为(本次已改)**:国省道环比固定「当日 / 前一日 - 1」,2024/2025/2026 一致;`comparison_base` 只在导入/导出/页面展示时原样保存与回写,不参与环比、折算、累计。导出 E 列改为 `=(D{行}-D{上一日行})/D{上一日行}`,C 列照常回写备用值。 - **改动**:`HolidayStatService.java`(`nationalProvincialRingRatio` 去掉 year/comparisonBase 形参)、`HolidayWorkbookService.java`(新增 `ringRatioFormula`,导出按上一行同年 D 列)、`docs/database.md`(`comparison_base` 与计算口径两处)。 - **验证**:新增 `HolidayComparisonBaseTest`:① 库内存 `comparison_base=320.2418` 时,10-04 国省道环比仍按前一日 323.7837 计算,且不等于按基期算出的值;② 导出公式 = `=(D6-D5)/D5`。`mvn -o test` 全绿(4 个测试类)。 ### 10-06:待确认——10-04 国省道剔站(鄂州樊家庄)怎么进系统 - **手工口径已逐项核对,与库内 `observation_station_flow` 完全对得上**: - 10-04 国省道值(2026/2025/2024)= 原值 − 樊家庄当日值 -> 322.7695 / 337.5009 / 322.5277 万辆次(樊家庄 8856 / 4492 / 2328)。 - 10-04 环比分母 = 2026-10-03 原值 − 樊家庄 10-03 值 = 323.7837 − 3.5419 = **320.2418** 万辆次 -> 环比 = 322.7695/320.2418 − 1 = **+0.79%**。 - 10-03 显示值与累计仍用原值 323.7837,被剔的 320.2418 只落到 C 列备用。 - **趋势结论(需先跟领导对齐)**:只剔樊家庄,10-04 国省道环比只有 **+0.79%**;同日高速小客车 **+9.78%**、铁路 +9.94%、民航 +11.69%、道路 +4.99%、水路 +4.33%。**剔一站填不平约 9 个百分点的趋势差**,与「让国省道和高速趋势一致」的预期并不相符,建议同时请鄂州核实原始记录。 - **待决策的代码方案**:因 `comparison_base` 已定为纯备用,系统无法从「当日值」反推剔站基期,需要另设机制。推荐新增「异常站剔除」配置(站点编号 + 生效日期 + 年份),算环比分母时用 `observation_station_flow` 的「该日站点合计 − 剔站值」作基期;显示值与累计仍用原值。 - **决策点**:① 做成可配置的通用「异常站剔除」(推荐,可复用于其他节假日/站点),还是只按本次 10-04 一次性处理;② 剔站只影响国省道环比分母,还是也要下沉到非营业性小客车/公路/全社会的环比(手工稿实际把 4.17% / 4.55% 也一起改了)。 ## 最新(2026-10-05) ### 10-05 上午:关机快照(当前状态) docs/database.md
@@ -526,7 +526,7 @@ | road_category | varchar(32) | 高速公路 EXPRESSWAY / 普通国省道 NATIONAL_PROVINCIAL | | vehicle_type | varchar(32) | 小客车 SMALL_PASSENGER / 中小客车 MEDIUM_SMALL_PASSENGER | | vehicle_count | decimal(30,15) | 车流量(万辆次) | | comparison_base | decimal(30,15) | 普通国省道环比基期(万辆次,仅当年使用) | | comparison_base | decimal(30,15) | 普通国省道环比基期(万辆次,人工备用值,仅 2026 年启用、不参与计算) | | created_at / updated_at | datetime | 创建/更新时间 | 唯一键:`(year, flow_date, road_category, vehicle_type)`。 @@ -544,7 +544,7 @@ ### 计算与导入导出口径 - 非营业性人员流动量 =(高速车流 + 普通国省道车流)× 当年节假日客运系数。 - 2026 年普通国省道如填写环比基期,则只影响普通国省道车流量自身环比:填写时按“当日 / 环比基期 - 1”,未填写时按“当日 / 前一日 - 1”;折算人员流动量、总人员流动量等继续按 Excel 原公式计算。2024、2025 历史数据不使用该字段。 - 普通国省道环比固定按“当日 / 前一日 - 1”计算;2024/2025/2026 各年一致。`comparison_base`(Excel 普通国省道表 C 列“环比基期”)自 2026-10-05 起仅作人工备用:导入导出原样保存/回写,不参与环比、折算或累计;导出时 C 列照常写出,E 列“环比”公式始终引用上一行同年 D 列。 - 公路人员流动量 = 道路营业性客运量 + 非营业性人员流动量。 - 总人员流动量 = 公路 + 铁路 + 民航 + 水路。 - 导入按“年份 + 节假日分类”读取 `holiday_calendar` 中 `day_index>=1` 的日期,将工作簿“第N日”行覆盖写入当年客流/车流;同时读取当前年份铁路、民航的“前一日”行,写入 `day_index=0` 对应的日期。 traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayStatService.java
@@ -102,7 +102,7 @@ BigDecimal nationalProvincial = vehicle.get(HolidayConstants.ROAD_NATIONAL_PROVINCIAL); BigDecimal comparisonBase = comparisonBaseByDate.get(day.getHolidayDate()); BigDecimal nationalProvincialRingRatio = nationalProvincialRingRatio( year, nationalProvincial, previousNationalProvincial, comparisonBase); nationalProvincial, previousNationalProvincial); BigDecimal nonBusinessPersonFlow = null; if (coefficient != null && (expressway != null || nationalProvincial != null)) { nonBusinessPersonFlow = HolidayNumberUtils.multiply( @@ -252,12 +252,11 @@ return total; } private BigDecimal nationalProvincialRingRatio(int year, BigDecimal current, BigDecimal previous, BigDecimal comparisonBase) { // The national/provincial ring ratio always compares the current day with the previous day of // the same year. The stored comparison base (Excel column C) is kept as an operator backup // value only and never feeds this calculation. private BigDecimal nationalProvincialRingRatio(BigDecimal current, BigDecimal previous) { if (current == null) return null; if (HolidayConstants.supportsComparisonBase(year) && comparisonBase != null) { return ratio(current, comparisonBase); } return previous == null ? null : ratio(current, previous); } traffic-audit-server/src/main/java/com/trafficaudit/holiday/service/HolidayWorkbookService.java
@@ -568,6 +568,9 @@ setNumeric(sheet, row.getRowNum(), yearColumn, values.get(day.getValue())); } if (comparisonBaseColumn != null && HolidayConstants.supportsComparisonBase(year)) { // Column C (comparison base) is written as an operator backup value only. The ring // ratio in column E always compares the current day with the previous day row of // the same year, so the base never changes the calculated percentage. Map<LocalDate, BigDecimal> bases = loadComparisonBaseValues(year); for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { if (!bases.containsKey(day.getValue())) continue; @@ -575,16 +578,27 @@ if (row == null) continue; setNumeric(sheet, row.getRowNum(), comparisonBaseColumn, bases.get(day.getValue())); } Integer previousRowIndex = null; for (Map.Entry<Integer, LocalDate> day : dayDates.entrySet()) { if (!bases.containsKey(day.getValue())) continue; Row row = findDayRow(sheet, evaluator, day.getKey()); if (row == null) continue; int rowIndex = row.getRowNum(); int excelRow = rowIndex + 1; setFormula(sheet, rowIndex, 4, "=(D" + excelRow + "-C" + excelRow + ")/C" + excelRow); if (row == null) { previousRowIndex = null; continue; } if (previousRowIndex != null) { setFormula(sheet, row.getRowNum(), 4, ringRatioFormula(row.getRowNum() + 1, previousRowIndex + 1)); } previousRowIndex = row.getRowNum(); } } } } // Ring ratio = (current day value - previous day value) / previous day value, always taken // from the active year column (D). This intentionally ignores the comparison base column. static String ringRatioFormula(int excelRow, int previousExcelRow) { return "=(D" + excelRow + "-D" + previousExcelRow + ")/D" + previousExcelRow; } private void fillCoefficients(XSSFWorkbook workbook, FormulaEvaluator evaluator, int selectedYear, String holidayType) { @@ -1013,7 +1027,9 @@ if (row == null) row = sheet.createRow(rowIndex); Cell cell = row.getCell(columnIndex); if (cell == null) cell = row.createCell(columnIndex); cell.setCellFormula(formula); // POI's setCellFormula rejects a leading '='; the helper accepts either form. String normalized = formula.startsWith("=") ? formula.substring(1) : formula; cell.setCellFormula(normalized); } private Map<String, Object> toMap(HolidayCalendar calendar) { traffic-audit-server/src/test/java/com/trafficaudit/holiday/service/HolidayComparisonBaseTest.java
New file @@ -0,0 +1,99 @@ package com.trafficaudit.holiday.service; import com.trafficaudit.holiday.HolidayConstants; import com.trafficaudit.holiday.HolidayNumberUtils; import com.trafficaudit.holiday.entity.HolidayCalendar; import com.trafficaudit.holiday.entity.HolidayPassengerFlow; import com.trafficaudit.holiday.entity.HolidayVehicleFlow; import com.trafficaudit.holiday.mapper.HolidayPassengerFlowMapper; import com.trafficaudit.holiday.mapper.HolidayVehicleFlowMapper; import org.junit.jupiter.api.Test; import java.lang.reflect.Field; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * The Excel comparison base column (C) is an operator backup value only: it must never change the * national/provincial ring ratio. This locks in that behaviour. */ class HolidayComparisonBaseTest { @Test void ringRatioUsesPreviousDayEvenWhenComparisonBaseIsStored() throws Exception { HolidayConfigService configService = mock(HolidayConfigService.class); HolidayPassengerFlowMapper passengerMapper = mock(HolidayPassengerFlowMapper.class); HolidayVehicleFlowMapper vehicleMapper = mock(HolidayVehicleFlowMapper.class); when(configService.coefficientOf(2026, HolidayConstants.CATEGORY_HOLIDAY)).thenReturn(new BigDecimal("2.4")); when(passengerMapper.selectList(any())).thenReturn(new ArrayList<HolidayPassengerFlow>()); when(vehicleMapper.selectList(any())).thenReturn(vehicleRows()); HolidayStatService service = new HolidayStatService(); inject(service, "configService", configService); inject(service, "passengerFlowMapper", passengerMapper); inject(service, "vehicleFlowMapper", vehicleMapper); List<Map<String, Object>> rows = service.buildOverviewRows(2026, HolidayConstants.TYPE_NATIONAL_DAY, calendar()); BigDecimal ringRatio = (BigDecimal) rows.get(3).get("nationalProvincialRingRatio"); assertEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("323.7837")), ringRatio); assertNotEquals(ratio(new BigDecimal("322.7695"), new BigDecimal("320.2418")), ringRatio); assertEquals(new BigDecimal("320.2418"), rows.get(2).get("comparisonBase")); } @Test void exportFormulaComparesPreviousRowInsteadOfComparisonBaseColumn() { assertEquals("=(D6-D5)/D5", HolidayWorkbookService.ringRatioFormula(6, 5)); } private static BigDecimal ratio(BigDecimal current, BigDecimal base) { return HolidayNumberUtils.normalize(HolidayNumberUtils.divide(current, base).subtract(BigDecimal.ONE)); } private static List<HolidayVehicleFlow> vehicleRows() { List<HolidayVehicleFlow> rows = new ArrayList<>(); rows.add(vehicle("2026-10-03", "323.7837", "320.2418")); rows.add(vehicle("2026-10-04", "322.7695", null)); return rows; } private static HolidayVehicleFlow vehicle(String date, String value, String base) { HolidayVehicleFlow row = new HolidayVehicleFlow(); row.setYear(2026); row.setFlowDate(LocalDate.parse(date)); row.setRoadCategory(HolidayConstants.ROAD_NATIONAL_PROVINCIAL); row.setVehicleType(HolidayConstants.VEHICLE_MEDIUM_SMALL_PASSENGER); row.setVehicleCount(new BigDecimal(value)); row.setComparisonBase(base == null ? null : new BigDecimal(base)); return row; } private static List<HolidayCalendar> calendar() { List<HolidayCalendar> days = new ArrayList<>(); for (int i = 1; i <= 4; i++) { HolidayCalendar day = new HolidayCalendar(); day.setYear(2026); day.setHolidayType(HolidayConstants.TYPE_NATIONAL_DAY); day.setDayIndex(i); day.setHolidayDate(LocalDate.of(2026, 10, i)); days.add(day); } return days; } private static void inject(Object target, String field, Object value) throws Exception { Field declared = target.getClass().getDeclaredField(field); declared.setAccessible(true); declared.set(target, value); } }