fix(holiday): 快报累计值与道路同比按Excel显示精度处理
| | |
| | | ## 验证注意 |
| | | - 沙箱内 `Get-NetTCPConnection` 会因权限“拒绝访问”而静默失败,**不要**据此判定端口未监听;判断服务状态以 backend.log / frontend.log 为准。 |
| | | - 用户要求:临时脚本/文件未经同意不删;导入失败要在页面显示行号;AI 分析结果要给结构化排版 + 逐字输出动画。 |
| | | - **数据导入权限(2026-10-04 用户明确)**:除非用户明确要求助手导入,否则不要执行任何数据导入、覆盖或重导操作;用户自行导入的数据默认视为已完成,助手只做只读核对。 |
| | | |
| | | ## 重要节假日快报与分析报告口径(2026-10-04 起) |
| | | - 快报百分比必须先按导入的 `2026年十一数据.xlsx` 对应单元格的数字格式生成显示值,再按快报统一 1 位小数取整。当前源表不是全局同一精度:多数单元格是 `0.0%`;`人员流动量汇总` 的当日环比 D/P/J、累计同比 V/W/Y/Z/AB/AC,以及水路当日环比 D 是 `0.00%`。 |
| | | - 快报百分比必须先按导入的 `2026年十一数据.xlsx` 对应单元格的数字格式生成显示值,再按快报统一 1 位小数取整。当前源表不是全局同一精度:多数单元格是 `0.0%`;`人员流动量汇总` 的当日环比 D/P/J、累计同比 V/W/Y/Z/AB/AC,道路 `H`(同比2024年),以及水路当日环比 D 是 `0.00%`。 |
| | | - `人员流动量汇总` 的累计值 U/X/AA 存的是亿单位,单元格格式为 `0.0000000_`;快报必须在换算万人次前先按亿单位 7 位小数生成显示值,再乘 10000 并按快报 2 位输出。对应总计、公路合计、非营业性小客车三项。 |
| | | - 核对领导分析报告时,按该分析报告自身显示的小数位核对,不套用快报的 1 位小数口径;目标是复现人工在文档/Excel 上看到的值。 |
| | | |
| | | ## 工作规范(2026-09-02 起;2026-09-27 按同事提交方式调整) |
| | |
| | | > **协作约定(2026-08-31)**:说「提交」= 本地 commit + 推送到远端 git(origin main,Gitblit),不再只做本地 commit。 |
| | | |
| | | ## 最新(2026-10-04) |
| | | ### 10-04 上午:10 月 3 日数据导入并生成快报 |
| | | |
| | | - **源文件**:`D:\03_公司\02_项目\27_湖北省道路运输事业发展中心\10_业务\99_专项统计\01_重要节假日\2026\十一\10月3日\2026年十一数据10月3日.xlsx`(2026-10-04 10:11 更新);观测站原始数据为同目录 `观测站分日调查数据2026.10.3.xls`。 |
| | | - **导入过程(修正)**:用户先于 10:13:34 导入主数据;助手随后在 10:17:41 又用同一文件重复导入了一次。接口返回 `passengerRows=14, vehicleRows=6, skippedRows=24, successRows=20, failRows=0`,未检测到 10 月 1、2 日历史数据变化。唯一键命中后仅更新 `updated_at`,没有新增重复行。 |
| | | - **10 月 3 日入库值**:铁路 72.4、民航 5.7571、道路 73.12、水路 5.5668 万人次;高速 193.3634、普通国省道 323.7837 万辆次;合计人员流动量 1397.99694 万人次。 |
| | | - **观测站数据(修正)**:用户先导入;助手重复导入时按同日覆盖重插,当前 2026-10-03 为 691 行 / 691 个唯一键,无重复;中小客流量合计 3,237,837 辆次(323.7837 万辆次),与普通国省道入库值一致。 |
| | | - **快报**:已生成 `C:\Users\jcxiong\Downloads\湖北省国庆假期综合运输客流快报(10月3日).docx`。正文:当日 1398.00 万人次,环比下降 3.3%,同比 2025 下降 10.1%,同比 2024 下降 5.0%;累计 4486.63 万人次,同比 2025 下降 6.3%,同比 2024 下降 2.7%。 |
| | | - **人工稿比对(10-04 更新)**:手工稿与系统修正版提取文本逐行一致;系统原版仅有两处累计数差异,已随本次代码修正消除。今日走势图功能暂停。 |
| | | |
| | | ### 10-04 上午(2):快报累计数按 Excel 亿单位显示精度后再换算 |
| | | |
| | | - **问题**:系统原版与手工稿仅两处差异:非营业性小客车累计 `3961.76` 应为 `3961.77`;公路合计累计 `4189.31` 应为 `4189.32`。 |
| | | - **根因**:源表 `人员流动量汇总!X5/AA5`(总计对应 `U5`)存的是亿单位,单元格格式为 `0.0000000_`。人先看到 7 位小数的显示值,再换算成万人次并保留 2 位;旧代码直接拿原始累计值换算后取 2 位,漏掉了 Excel 显示这一步。 |
| | | - **修复**:`HolidayBulletinService` 新增 `displayedCumulativeWan(...)`,先按源单元格 7 位小数取整,再乘 10000,最后按快报 2 位输出;应用于总计、非营业性小客车、公路合计三处累计数。 |
| | | - **验证**:新增单测覆盖 `3961.76472 -> 3961.77`、`4189.31472 -> 4189.32`、`4486.62652 -> 4486.63`;`mvn test` 全通过。重启后基于现有库重新生成 10 月 3 日快报,与手工稿提取文本逐行比对结果为 `NO DIFFERENCES`。本次未执行任何数据导入。 |
| | | - **产物**:`_tmp_holiday_verify/bulletin_cumulative_display_2026-10-03.docx`。 |
| | | - **状态**:待用户验收/提交;走势图功能仍暂停。 |
| | | |
| | | ### 10-04 上午(3):快报其余 Excel 显示精度路径审计 |
| | | |
| | | - **审计范围**:逐项映射快报正文和表格的所有数值来源,核对源工作簿的实际单元格格式、Excel 显示值和代码传入的显示精度。 |
| | | - **结论**:除已修复的累计值 `U/X/AA` 外,没有发现其他未处理的快报取值路径。源表直接参与快报的 `0.00%` 单元格共 11 处:`人员流动量汇总` D/P/J、V/W/Y/Z/AB/AC,道路 H(同比2024年),水路 D(当日环比);代码均先按源格式取 2 位,再按快报 1 位处理。 |
| | | - **补修边界**:道路 H 原按 1 位源格式处理,10 月 3 日碰巧仍为 `6.6%`;改为先按 `0.00%` 得到 `6.62%` 后再取 1 位,边界值不会差 0.1 个百分点。 |
| | | - **未纳入直接快报路径的格式**:`普通国省道` E/N 虽为 `0.00%`,快报使用的是 `人员流动量汇总` 汇总后的 P/R/T,不使用该两列;高速公路/普通国省道内部 4 位小数人员流动量同理只作为源公式中间列,不直接输出。 |
| | | - **验证**:`mvn test` 全通过;重启后端后重新生成 10 月 3 日快报,与手工稿逐块、逐单元格比较无任何差异。10 月 2 日与手工稿的内容和表格文本一致,仅有 Word run 分段格式差异。`_tmp_holiday_verify/bulletin_after_road_format_2026-10-01/02/03.docx` 为本轮核对产物。 |
| | | - **数据操作**:只读核对,未导入、覆盖或重导任何数据。 |
| | | - **状态**:待用户验收/提交。 |
| | | |
| | | ### 10-04:快报百分比按 Excel 单元格显示精度后再取 1 位 |
| | | |
| | |
| | | private static final BigDecimal EPSILON = new BigDecimal("0.0000001"); |
| | | private static final int EXCEL_ONE_DECIMAL_PERCENT = 1; |
| | | private static final int EXCEL_TWO_DECIMAL_PERCENT = 2; |
| | | private static final int EXCEL_SEVEN_DECIMAL_YI = 7; |
| | | private static final BigDecimal TEN_THOUSAND = new BigDecimal("10000"); |
| | | |
| | | @Value("${holiday.bulletin-template:docs/专项统计/重要节假日/湖北省国庆假期综合运输客流快报模板.docx}") |
| | | private String bulletinTemplatePath; |
| | |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getNonBusinessPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getNonBusinessPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | number(currentCumulative.getNonBusinessPersonFlow()), |
| | | numberCumulative(currentCumulative.getNonBusinessPersonFlow()), |
| | | ratio(currentCumulative.getNonBusinessPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getNonBusinessPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | |
| | | number(current.getRoad()), |
| | | ratio(current.getRoad(), previousDay == null ? null : previousDay.getRoad(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoad(), previousYear == null ? null : previousYear.getRoad(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad(), EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoad(), twoYearsAgo == null ? null : twoYearsAgo.getRoad(), EXCEL_TWO_DECIMAL_PERCENT), |
| | | number(currentCumulative.getRoad()), |
| | | ratio(currentCumulative.getRoad(), previousYearCumulative == null ? null : previousYearCumulative.getRoad(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getRoadPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getRoadPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | number(currentCumulative.getRoadPersonFlow()), |
| | | numberCumulative(currentCumulative.getRoadPersonFlow()), |
| | | ratio(currentCumulative.getRoadPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getRoadPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | ratio(current.getTotalPersonFlow(), twoYearsAgo == null ? null : twoYearsAgo.getTotalPersonFlow(), |
| | | EXCEL_ONE_DECIMAL_PERCENT), |
| | | number(currentCumulative.getTotalPersonFlow()), |
| | | numberCumulative(currentCumulative.getTotalPersonFlow()), |
| | | ratio(currentCumulative.getTotalPersonFlow(), |
| | | previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | |
| | | DailyMetrics twoYearsAgoCumulative) { |
| | | BigDecimal total = currentCumulative == null ? null : currentCumulative.getTotalPersonFlow(); |
| | | return String.format("假期以来,全省全社会跨区域人员流动量累计%s万人次,比2025年同期%s,比2024年同期%s。", |
| | | formatNumber(total), |
| | | formatNumber(displayedCumulativeWan(total)), |
| | | ratioPhrase(total, previousYearCumulative == null ? null : previousYearCumulative.getTotalPersonFlow(), |
| | | EXCEL_TWO_DECIMAL_PERCENT), |
| | | ratioPhrase(total, twoYearsAgoCumulative == null ? null : twoYearsAgoCumulative.getTotalPersonFlow(), |
| | |
| | | return value == null ? "—" : formatNumber(value); |
| | | } |
| | | |
| | | private String numberCumulative(BigDecimal value) { |
| | | return value == null ? "—" : formatNumber(displayedCumulativeWan(value)); |
| | | } |
| | | |
| | | /** |
| | | * Source cumulative cells U5/X5/AA5 are stored in 100-million units and displayed with seven decimals. |
| | | * Mirror that displayed precision before the bulletin converts the value to 10-thousand units. |
| | | */ |
| | | private BigDecimal displayedCumulativeWan(BigDecimal cumulativeWan) { |
| | | if (cumulativeWan == null) return null; |
| | | BigDecimal yi = cumulativeWan.divide(TEN_THOUSAND, EXCEL_SEVEN_DECIMAL_YI, RoundingMode.HALF_UP); |
| | | return HolidayNumberUtils.normalize(yi.multiply(TEN_THOUSAND)); |
| | | } |
| | | |
| | | private String formatNumber(BigDecimal value) { |
| | | if (value == null) return ""; |
| | | return HolidayNumberUtils.format2(value); |
| | |
| | | assertEquals("-4.5%", ratio(service, "95.551398", "100", 2)); |
| | | assertEquals("-0.1%", ratio(service, "99.955", "100", 2)); |
| | | assertEquals("0%", ratio(service, "99.955", "100", 1)); |
| | | assertEquals("6.7%", ratio(service, "106.649", "100", 2)); |
| | | assertEquals("6.6%", ratio(service, "106.649", "100", 1)); |
| | | assertEquals("1.0%", ratio(service, "101", "100", 1)); |
| | | assertEquals("0%", ratio(service, "100", "100", 1)); |
| | | assertEquals("0%", ratio(service, "10004", "10000", 1)); |
| | |
| | | text); |
| | | } |
| | | |
| | | @Test |
| | | void cumulativeAmountsUseExcelYiDisplayBeforeWanRounding() throws Exception { |
| | | HolidayBulletinService service = new HolidayBulletinService(null); |
| | | |
| | | assertEquals("3961.77", cumulativeNumber(service, "3961.76472")); |
| | | assertEquals("4189.32", cumulativeNumber(service, "4189.31472")); |
| | | assertEquals("4486.63", cumulativeNumber(service, "4486.62652")); |
| | | } |
| | | |
| | | private String cumulativeNumber(HolidayBulletinService service, String value) throws Exception { |
| | | Method method = HolidayBulletinService.class.getDeclaredMethod("numberCumulative", BigDecimal.class); |
| | | method.setAccessible(true); |
| | | BigDecimal number = value == null ? null : new BigDecimal(value); |
| | | return (String) method.invoke(service, number); |
| | | } |
| | | |
| | | private Object comparableCumulative(HolidayBulletinService service, DailyMetrics current, |
| | | DailyMetrics comparisonYear, |
| | | Function<DailyMetrics, BigDecimal> selector) throws Exception { |