| | |
| | | > **每天结束时**:把当天进展、踩过的坑、新需求、未完成事项更新到本文件,然后可以放心开新对话。 |
| | | > **协作约定(2026-08-31)**:说「提交」= 本地 commit + 推送到远端 git(origin main,Gitblit),不再只做本地 commit。 |
| | | |
| | | ## 最新(2026-10-07 上午,新开会话交接) |
| | | |
| | | ### 10-07:走势图横轴和累计范围改为按年度配置(待 10-07 数据齐全后最终验收) |
| | | |
| | | - **发现方式**:应要求对已验收的 10月6日走势图 `(12).docx` 做数值复核。15 张图与源表、数据库和现场重新生成图一致,但折线图横轴仍出现 `10/8`。核对 `holiday_calendar` 后确认 2026 年十一只配置 `10-01` 至 `10-07` 共 7 天,代码却固定遍历 8 天。 |
| | | - **影响**:折线图会多一个 `10/8` 空日期位;2025 年多出的 10/8 还可能进入最终 7 天版的累计和公路出行结构对比,导致 2025 自驾由 `10,406` 变为 `9,365`、道路由 `648` 变为 `571`。 |
| | | - **修复**:`HolidayTrendService` 按所选年份的 `holiday_calendar` 生成日期标签和天数,折线数据、累计、每日构成、观测站数据均按配置天数截取;`HolidayTrendChartService.lineChart` 新增显式日期标签参数,旧调用保留默认行为。 |
| | | - **验证**:`mvn -q -f traffic-audit-server\pom.xml -Dtest=HolidayTrendServiceTest,HolidayTrendChartServiceTest test` 通过;后端全量 `mvn -q test -f traffic-audit-server\pom.xml` 通过;真实库只读生成 `_tmp_holiday_verify\server_trend_7day.docx`,15 张图完整;原图 + 3 倍放大 OCR 合并核对,2026/2025/2024 折线、每日构成、饼图、公路结构和观测站标签均与独立重算一致,横轴无 `10/8`。唯一 OCR 粘连是 `1.3701.31`,包含预期的 `1.37` 和 `1.31`。 |
| | | - **验收副本**:`C:\Users\jcxiong\Downloads\2026年十一假期全省综合运输客流量走势图(7天版-待验收).docx`。 |
| | | - **测试报告**:`docs\功能测试报告\功能测试报告_走势图横轴按年度配置_2026-10-07.md`(本地留存,不入库)。 |
| | | - **数据边界**:未导入、覆盖或重导任何业务数据。 |
| | | - **后续**:10-07 数据导入后重新生成最终 7 天版,确认横轴包含 10/7 且 2026-10-07 数值进入各图;随后再做一次只读核验。 |
| | | |
| | | ### 10-07:每日客流构成图右侧标签布局修复(验收通过) |
| | | |
| | | - **问题**:`image13`「每日客流构成变化」右侧外置百分比标签在相邻日期之间重叠 4 处;改为两列后引线交错,个别引线压住绿色块内数值。 |
| | | - **最终规则**:只有色块内放不下的标签进入右侧外置区;外置标签按原始图例序号排序,即道路→铁路→民航→水路;最多 2 列,按“先行后列、每行从左到右”填充;1 个标签整体居中,2 个标签两列各居中,3 个标签下排单标签跨两列居中,4 个标签为 2×2 格居中;两列布局不画引线,颜色对应色块。 |
| | | - **实现**:`HolidayTrendChartService` 将右侧标签带从 74 加宽到 140;`HorizontalStackCallout` 记录原始 `seriesIndex` 并按其排序;仅对每日客流构成图启用两列居中布局;公路出行结构仍走原单列带引线逻辑。 |
| | | - **验证**:真实库只读生成 2026-10-07 走势图;与原版相比 15 张图中仅 `image13` 变化,其余 14 张 SHA-256 完全一致;右侧标签区 OCR 重叠从 4 降为 0;无引线。后端全量 `mvn -q test -f traffic-audit-server\pom.xml` 通过。 |
| | | - **验收**:用户 2026-10-07 验收 `(12).docx` 通过。 |
| | | - **后续**:10-07 数据齐全后,按系统配置的假期天数(7 天)生成最终版走势图,用于分析报告;本轮不调整假日天数来源。 |
| | | - **数据边界**:未导入、覆盖或重导任何业务数据。 |
| | | |
| | | ### 10-07:普通国省道 2025-10-05 调整值更正(已核对) |
| | | |
| | | - **背景**:10-05 剔除 3 个环比降幅超过 50% 的站点(郑店、樊家庄、上津站)和 3 个 10-05 缺失站点(龙泉、华鲁交调站、大典河)。 |
| | | - **站点编号**:郑店 `S101L110420115`、樊家庄 `S120L105420702`、上津站 `G242L104420322`、龙泉 `S216L103420506`、华鲁交调站 `S223L117421024`、大典河 `S464L105422828`。 |
| | | - **各日剔除合计(中小客流量,辆次)**:2026-10-04 `16,484`;2026-10-05 `4,398`;2025-10-05 `14,239`;2024-10-05 `11,907`。调整时缺失站点按 0 处理。 |
| | | - **系统更正**:`holiday_vehicle_adjustment` 的 2025-10-05 调整值已由 `322.7801` 改为 `332.7801`(更新时间 2026-10-07 08:45:52);2024-10-05 `289.5557`、2025-10-04 `337.5009`、2024-10-04 `322.5277` 不变。 |
| | | - **工作簿更正**:`C:\Users\jcxiong\Downloads\2026年十一数据 (4).xlsx`(修改时间 08:46:52)中 `普通国省道!F7=332.7801`。联动结果已正确重算:`普通国省道!G7=-4.8%`;`人员流动量汇总` 的 `E7=1460.97`、`F7=-0.3%`、`K7=1372.85`、`Q7=1290.04`、`R7=-1.0%`、`V7=-4.28%`。 |
| | | - **公式更正已核实**:`普通国省道!E8=(D8-D7)/D7`、`G8=D8/F8-1`、`I8=D8/H8-1`、`E9=(D9-D8)/D8`。 |
| | | - **10-06 边界(重要)**:工作簿目前只有铁路 `C9=96.2`、水路 `C8=5.4932`、高速公路 `C8=206.1206`;民航 `C9`、道路 `C8`、普通国省道 `D8` 仍为空。因此 `人员流动量汇总!C8=596.38264` 只是部分合计,`D8=-59.06%` 以及各 `-100%/#DIV/0!` 都不可用于快报;补齐三项后重算。 |
| | | - **数据边界**:本轮只读核对,未导入、覆盖或重导任何业务数据。 |
| | | |
| | | ### 10-07:Codex 输入卡顿排查与交接建议 |
| | | |
| | | - 当前线程从 10-05 连续使用至今,rollout 约 `32.17 MB`,含 `3,751` 条 `response_item`、`3,469` 条 `event_msg`、`10` 次 compaction;用户已决定新开对话继续。 |
| | | - 已排除 `codex-plus-plus-manager --show-update`(未运行);重试风暴 guard 已安装,`dropped=859` 且连续 10 秒不增长,当前没有持续重试风暴。 |
| | | - 观测到 Codex 主进程累计约 `4.34 h` CPU、渲染进程约 `1.28 h`;短时峰值主进程约 `116%` 单核、渲染约 `44%`,属于间歇高负载。建议新对话继续;若仍卡,完全退出并重启 Codex。 |
| | | - **新对话接手**:先读根目录 `AGENTS.md` + 本文件。当前 git `HEAD` 与 `origin/main` 均为 `9b56330`,已跟踪文件无改动;dev 前端 `8080`、后端 `8090` 正在运行。 |
| | | |
| | | ## 历史(2026-10-07 早间关机快照) |
| | | |
| | | ### 10-07 关机快照 |
| | | |
| | | - **当前状态**:本轮仅做关机交接,无代码、测试口径或业务数据变更。上一任务“折线图纵向间距加高 20%”已于 10-06 验收,提交为 `54e10ae fix(holiday): 加大折线图纵向间距并同步 Word 尺寸`。 |
| | | - **Git**:`main` 的 `HEAD` 与 `origin/main` 均为 `54e10aee11aa4a10926df02782ca07d41eeb9b98`;无未提交的已跟踪文件,工作区仅有按约定不入库的 `_tmp_*` 临时目录/文件、`_tmp_pack_smoke_trend.docx` 和一份观测站原始 XLS。 |
| | | - **运行状态**:关机前本机 dev 前后端仍可访问(前端 8080、后端 8090 均返回 200);随计算机关机会自动停止,重启后按 `AGENTS.md` 用 `start-dev.ps1` 启动。 |
| | | - **本地留存**:折线图纵向间距的测试报告和工作日结已写入 `docs/功能测试报告/`、`docs/工作日结/`,均按约定不入库。 |
| | | - **下次接手**:先读根目录 `AGENTS.md` + 本文件;仓库级待办仍见第七节,本轮无新增待办。 |
| | | - **数据边界**:未导入、覆盖或重导任何业务数据。 |
| | | |
| | | ## 最新(2026-10-06) |
| | | |
| | | ### 10-06:折线图纵向间距加高(验收通过) |
| | |
| | | private static final Color AXIS_COLOR = Color.BLACK; |
| | | private static final Color MUTED_TEXT_COLOR = new Color(102, 102, 102); |
| | | private static final double UNIT_LABEL_BAND = 58.0; |
| | | private static final double RIGHT_LABEL_BAND = 74.0; |
| | | private static final double RIGHT_LABEL_BAND = 140.0; |
| | | private static final double ITEM_LABEL_GAP = 10.0; |
| | | private static final float HALO_OUTLINE_WIDTH = 4.0f; |
| | | private static final int PERCENT_ITEM_LABEL_DECIMALS = 1; |
| | |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis) { |
| | | return lineChart(yAxisLabel, series, percentAxis, 1100, 590); |
| | | return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, 2, 1100, 590); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | int width, int height) { |
| | | return lineChart(yAxisLabel, series, percentAxis, 2, width, height); |
| | | return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, 2, width, height); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | int valueDecimals, int width, int height) { |
| | | return lineChart(yAxisLabel, series, percentAxis, DAY_LABELS, valueDecimals, width, height); |
| | | } |
| | | |
| | | public byte[] lineChart(String yAxisLabel, List<ChartSeries> series, boolean percentAxis, |
| | | List<String> dayLabels, int valueDecimals, int width, int height) { |
| | | XYSeriesCollection dataset = new XYSeriesCollection(); |
| | | for (int seriesIndex = 0; seriesIndex < series.size(); seriesIndex++) { |
| | | ChartSeries item = series.get(seriesIndex); |
| | |
| | | plot.setRangeGridlinesVisible(false); |
| | | plot.setDomainTickBandPaint(null); |
| | | plot.setRangeTickBandPaint(null); |
| | | SymbolAxis domainAxis = new SymbolAxis(null, DAY_LABELS.toArray(new String[0])); |
| | | List<String> axisLabels = dayLabels == null || dayLabels.isEmpty() ? DAY_LABELS : dayLabels; |
| | | SymbolAxis domainAxis = new SymbolAxis(null, axisLabels.toArray(new String[0])); |
| | | domainAxis.setGridBandPaint(new Color(0, 0, 0, 0)); |
| | | domainAxis.setGridBandAlternatePaint(new Color(0, 0, 0, 0)); |
| | | domainAxis.setLowerMargin(0.04); |
| | |
| | | occupied.add(new Rectangle2D.Double(textX, baseline - metrics.getAscent(), |
| | | width, metrics.getHeight())); |
| | | } else { |
| | | horizontalCallouts.add(new HorizontalStackCallout(Math.min(startX, endX), |
| | | horizontalCallouts.add(new HorizontalStackCallout(row, Math.min(startX, endX), |
| | | Math.max(startX, endX), centerY, text, seriesColor(row))); |
| | | } |
| | | } else { |
| | |
| | | } |
| | | } |
| | | if (horizontal) { |
| | | drawHorizontalStackCallouts(graphics, horizontalCallouts, dataArea, metrics, occupied); |
| | | drawHorizontalStackCallouts(graphics, horizontalCallouts, dataArea, metrics, occupied, |
| | | rows >= 3); |
| | | } else { |
| | | for (int column = 0; column < columns; column++) { |
| | | drawStackCalloutColumn(graphics, image, callouts.get(column), column, columns, |
| | |
| | | private void drawHorizontalStackCallouts(Graphics2D graphics, |
| | | List<HorizontalStackCallout> items, |
| | | Rectangle2D dataArea, FontMetrics metrics, |
| | | List<Rectangle2D> occupied) { |
| | | List<Rectangle2D> occupied, |
| | | boolean packIntoColumns) { |
| | | if (items.isEmpty()) return; |
| | | Map<Long, List<HorizontalStackCallout>> rows = new LinkedHashMap<>(); |
| | | for (HorizontalStackCallout item : items) { |
| | |
| | | Collections.sort(row, new Comparator<HorizontalStackCallout>() { |
| | | @Override |
| | | public int compare(HorizontalStackCallout first, HorizontalStackCallout second) { |
| | | return Double.compare(first.getStartX(), second.getStartX()); |
| | | return Integer.compare(first.getSeriesIndex(), second.getSeriesIndex()); |
| | | } |
| | | }); |
| | | int widest = 0; |
| | | double columnX = 0.0; |
| | | for (HorizontalStackCallout item : row) { |
| | | widest = Math.max(widest, metrics.stringWidth(item.getText())); |
| | | columnX = Math.max(columnX, item.getEndX() + 8.0); |
| | | if (!packIntoColumns) { |
| | | int widest = 0; |
| | | double columnX = 0.0; |
| | | for (HorizontalStackCallout item : row) { |
| | | widest = Math.max(widest, metrics.stringWidth(item.getText())); |
| | | columnX = Math.max(columnX, item.getEndX() + 8.0); |
| | | } |
| | | // Keep the label column inside the reserved right margin. |
| | | columnX = Math.min(columnX, dataArea.getMaxX() + RIGHT_LABEL_BAND - widest); |
| | | int height = metrics.getHeight(); |
| | | double step = height + 3.0; |
| | | double centerY = row.get(0).getY(); |
| | | double top = centerY - (step * row.size()) / 2.0 + (step - height) / 2.0; |
| | | for (int i = 0; i < row.size(); i++) { |
| | | HorizontalStackCallout item = row.get(i); |
| | | int width = metrics.stringWidth(item.getText()); |
| | | double x = columnX; |
| | | double y = top + i * step; |
| | | graphics.setColor(item.getColor()); |
| | | graphics.setStroke(new BasicStroke(1.4f, BasicStroke.CAP_ROUND, |
| | | BasicStroke.JOIN_ROUND)); |
| | | graphics.drawLine((int) Math.round(item.getEndX()), (int) Math.round(item.getY()), |
| | | (int) Math.round(x - 3.0), (int) Math.round(y + height / 2.0)); |
| | | drawLabelWithHalo(graphics, item.getText(), |
| | | new Rectangle2D.Double(x, y, width, height), metrics, item.getColor()); |
| | | occupied.add(new Rectangle2D.Double(x - 2.0, y - 2.0, |
| | | width + 4.0, height + 4.0)); |
| | | } |
| | | continue; |
| | | } |
| | | // Keep the label column inside the reserved right margin. |
| | | columnX = Math.min(columnX, dataArea.getMaxX() + RIGHT_LABEL_BAND - widest); |
| | | // Small slices are labelled outside the bar. Packing those labels |
| | | // into two columns keeps each day's callouts inside its own row. |
| | | int columnCount = row.size() <= 1 ? 1 : 2; |
| | | int rowCount = (int) Math.ceil(row.size() / (double) columnCount); |
| | | double[] columnWidths = new double[columnCount]; |
| | | for (int i = 0; i < row.size(); i++) { |
| | | int column = columnCount == 1 ? 0 : i % columnCount; |
| | | columnWidths[column] = Math.max(columnWidths[column], |
| | | metrics.stringWidth(row.get(i).getText())); |
| | | } |
| | | double columnGap = 10.0; |
| | | double blockWidth = columnGap * (columnCount - 1); |
| | | for (double columnWidth : columnWidths) blockWidth += columnWidth; |
| | | double blockLeft = dataArea.getMaxX() + 8.0; |
| | | double blockRight = dataArea.getMaxX() + RIGHT_LABEL_BAND; |
| | | blockLeft += Math.max(0.0, (blockRight - blockLeft - blockWidth) / 2.0); |
| | | double[] columnCenters = new double[columnCount]; |
| | | double cursor = blockLeft; |
| | | for (int column = 0; column < columnCount; column++) { |
| | | columnCenters[column] = cursor + columnWidths[column] / 2.0; |
| | | cursor += columnWidths[column] + columnGap; |
| | | } |
| | | int height = metrics.getHeight(); |
| | | double step = height + 3.0; |
| | | double centerY = row.get(0).getY(); |
| | | double top = centerY - (step * row.size()) / 2.0 + (step - height) / 2.0; |
| | | double rowStep = height + 3.0; |
| | | double totalHeight = rowCount * height + (rowCount - 1) * (rowStep - height); |
| | | double top = row.get(0).getY() - totalHeight / 2.0; |
| | | for (int i = 0; i < row.size(); i++) { |
| | | HorizontalStackCallout item = row.get(i); |
| | | int column = columnCount == 1 ? 0 : i % columnCount; |
| | | int rowIndex = columnCount == 1 ? i : i / columnCount; |
| | | int itemsInRow = Math.min(columnCount, row.size() - rowIndex * columnCount); |
| | | int width = metrics.stringWidth(item.getText()); |
| | | double x = columnX; |
| | | double y = top + i * step; |
| | | graphics.setColor(item.getColor()); |
| | | graphics.setStroke(new BasicStroke(1.4f, BasicStroke.CAP_ROUND, |
| | | BasicStroke.JOIN_ROUND)); |
| | | graphics.drawLine((int) Math.round(item.getEndX()), (int) Math.round(item.getY()), |
| | | (int) Math.round(x - 3.0), (int) Math.round(y + height / 2.0)); |
| | | double centerX = itemsInRow == 1 && columnCount > 1 |
| | | ? blockLeft + blockWidth / 2.0 |
| | | : columnCenters[column]; |
| | | double x = centerX - width / 2.0; |
| | | double y = top + rowIndex * rowStep; |
| | | drawLabelWithHalo(graphics, item.getText(), |
| | | new Rectangle2D.Double(x, y, width, height), metrics, item.getColor()); |
| | | occupied.add(new Rectangle2D.Double(x - 2.0, y - 2.0, |
| | | width + 4.0, height + 4.0)); |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | private static class HorizontalStackCallout { |
| | | private final int seriesIndex; |
| | | private final double startX; |
| | | private final double endX; |
| | | private final double y; |
| | | private final String text; |
| | | private final Color color; |
| | | |
| | | private HorizontalStackCallout(double startX, double endX, double y, |
| | | private HorizontalStackCallout(int seriesIndex, double startX, double endX, double y, |
| | | String text, Color color) { |
| | | this.seriesIndex = seriesIndex; |
| | | this.startX = startX; |
| | | this.endX = endX; |
| | | this.y = y; |
| | |
| | | this.color = color; |
| | | } |
| | | |
| | | private int getSeriesIndex() { return seriesIndex; } |
| | | private double getStartX() { return startX; } |
| | | private double getEndX() { return endX; } |
| | | private double getY() { return y; } |
| | |
| | | if (holidayLabel == null) { |
| | | throw new IllegalArgumentException("不支持的节假日类型"); |
| | | } |
| | | List<HolidayCalendar> holidayDays = configService.findHolidayDays(year, holidayType); |
| | | if (holidayDays.isEmpty()) { |
| | | throw new IllegalArgumentException("所选年份和节假日尚未配置日期,请先在系统配置中维护节假日"); |
| | | } |
| | | List<String> dayLabels = dayLabels(holidayDays); |
| | | int dayCount = holidayDays.size(); |
| | | List<Integer> years = Arrays.asList(year, year - 1, year - 2); |
| | | Map<Integer, YearData> yearData = new LinkedHashMap<>(); |
| | | for (Integer item : years) { |
| | | yearData.put(item, buildYearData(item, holidayType)); |
| | | yearData.put(item, buildYearData(item, holidayType, dayCount)); |
| | | } |
| | | YearData selected = yearData.get(year); |
| | | if (selected == null || !selected.hasAnyData()) { |
| | |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "全省跨区域人员流动量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.totalDaily), false, 1100, 715), |
| | | series(years, yearData, item -> item.totalDaily), false, dayLabels, 2, 1100, 715), |
| | | lineImageSize(5268595L, 3424555L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期累计跨区域人员流动量走势", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.totalCumulative), false, 1100, 590), |
| | | series(years, yearData, item -> item.totalCumulative), false, dayLabels, 2, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期各交通运输方式客运量占比情况", |
| | |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期自驾出行量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.selfDrivingPersonFlow), false, 1100, 522), |
| | | series(years, yearData, item -> item.selfDrivingPersonFlow), false, dayLabels, 2, 1100, 522), |
| | | lineImageSize(5232400L, 2484120L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期铁路客运量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.rail), false, 1100, 445), |
| | | series(years, yearData, item -> item.rail), false, dayLabels, 2, 1100, 445), |
| | | lineImageSize(5264785L, 2129155L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期水路客运量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.water), false, 1100, 433), |
| | | series(years, yearData, item -> item.water), false, dayLabels, 2, 1100, 433), |
| | | lineImageSize(5274310L, 2075815L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期民航客运量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.civilAviation), false, 1100, 521), |
| | | series(years, yearData, item -> item.civilAviation), false, dayLabels, 2, 1100, 521), |
| | | lineImageSize(5271135L, 2496185L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期道路客运量情况", |
| | | chartService.lineChart("万人次", |
| | | series(years, yearData, item -> item.road), false, 1100, 590), |
| | | series(years, yearData, item -> item.road), false, dayLabels, 2, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("三、车流量走势")); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期高速公路车流量情况", |
| | | chartService.lineChart("万辆次", |
| | | series(years, yearData, item -> item.expressway), false, 1100, 590), |
| | | series(years, yearData, item -> item.expressway), false, dayLabels, 2, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期普通国省道车流量情况", |
| | | chartService.lineChart("万辆次", |
| | | series(years, yearData, item -> item.nationalProvincial), false, 1100, 590), |
| | | series(years, yearData, item -> item.nationalProvincial), false, dayLabels, 2, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期全省车流总量情况", |
| | | chartService.lineChart("万辆次", |
| | | series(years, yearData, item -> item.totalVehicleFlow), false, 1100, 590), |
| | | series(years, yearData, item -> item.totalVehicleFlow), false, dayLabels, 2, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期公路出行结构", |
| | |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | "“" + holidayLabel + "”假期每日客流构成变化", |
| | | chartService.stackedBarChart("占比", |
| | | HolidayTrendChartService.DAY_LABELS, |
| | | dailyShareSeries(selected), true, 1000, 607), true, |
| | | dayLabels, |
| | | dailyShareSeries(selected, dayCount), true, 1000, 607), true, |
| | | new HolidayTrendWordService.ImageSize(5106670L, 3097530L))); |
| | | |
| | | blocks.add(new HolidayTrendWordService.SectionBlock("附录二 普通国省道观测站路网运行")); |
| | | ObservationData observation = observationData(year, holidayType); |
| | | ObservationData observation = observationData(holidayDays); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "普通国省道观测站平均机动车速度", |
| | | chartService.lineChart("公里/小时", |
| | | Collections.singletonList(series("平均速度", observation.speed)), false, 1100, 590), |
| | | Collections.singletonList(series("平均速度", observation.speed)), false, dayLabels, 2, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | blocks.add(new HolidayTrendWordService.ChartBlock( |
| | | prefix + "普通国省道观测站平均拥挤度", |
| | | chartService.lineChart("拥挤度", |
| | | Collections.singletonList(series("平均拥挤度", observation.congestion)), false, 1100, 590), |
| | | Collections.singletonList(series("平均拥挤度", observation.congestion)), false, dayLabels, 2, 1100, 590), |
| | | lineImageSize(5268595L, 2821305L))); |
| | | |
| | | File template = resolveTemplateFile(); |
| | |
| | | } |
| | | } |
| | | |
| | | private YearData buildYearData(int year, String holidayType) { |
| | | private YearData buildYearData(int year, String holidayType, int dayCount) { |
| | | List<DailyMetrics> metrics = new ArrayList<>(); |
| | | for (int day = 1; day <= HolidayTrendChartService.DAY_LABELS.size(); day++) { |
| | | for (int day = 1; day <= dayCount; day++) { |
| | | metrics.add(statService.dailyMetrics(year, holidayType, day)); |
| | | } |
| | | YearData data = new YearData(); |
| | |
| | | return result; |
| | | } |
| | | |
| | | private List<HolidayTrendChartService.ChartSeries> dailyShareSeries(YearData data) { |
| | | private List<HolidayTrendChartService.ChartSeries> dailyShareSeries(YearData data, int dayCount) { |
| | | List<HolidayTrendChartService.ChartSeries> result = new ArrayList<>(); |
| | | result.add(series("自驾", share(data.selfDrivingPersonFlow, data))); |
| | | result.add(series("道路", share(data.road, data))); |
| | | result.add(series("铁路", share(data.rail, data))); |
| | | result.add(series("民航", share(data.civilAviation, data))); |
| | | result.add(series("水路", share(data.water, data))); |
| | | result.add(series("自驾", share(data.selfDrivingPersonFlow, data, dayCount))); |
| | | result.add(series("道路", share(data.road, data, dayCount))); |
| | | result.add(series("铁路", share(data.rail, data, dayCount))); |
| | | result.add(series("民航", share(data.civilAviation, data, dayCount))); |
| | | result.add(series("水路", share(data.water, data, dayCount))); |
| | | return result; |
| | | } |
| | | |
| | | private List<BigDecimal> share(List<BigDecimal> values, YearData data) { |
| | | private List<BigDecimal> share(List<BigDecimal> values, YearData data, int dayCount) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | for (int i = 0; i < dayCount; i++) { |
| | | BigDecimal total = dayTotal(data, i); |
| | | BigDecimal value = valueAt(values, i); |
| | | result.add(total == null || total.signum() == 0 |
| | |
| | | series("道路客运量", road)); |
| | | } |
| | | |
| | | private ObservationData observationData(int year, String holidayType) { |
| | | List<HolidayCalendar> days = configService.findHolidayDays(year, holidayType); |
| | | private ObservationData observationData(List<HolidayCalendar> days) { |
| | | if (days.isEmpty()) { |
| | | return new ObservationData(nulls(), nulls()); |
| | | return new ObservationData(nulls(0), nulls(0)); |
| | | } |
| | | LocalDate start = days.get(0).getHolidayDate(); |
| | | LocalDate end = start.plusDays(HolidayTrendChartService.DAY_LABELS.size() - 1L); |
| | | LocalDate end = days.get(days.size() - 1).getHolidayDate(); |
| | | List<ObservationDailyOperation> rows = observationStationFlowMapper.selectDailyOperations(start, end); |
| | | Map<LocalDate, ObservationDailyOperation> byDate = new LinkedHashMap<>(); |
| | | for (ObservationDailyOperation row : rows) { |
| | |
| | | } |
| | | List<BigDecimal> speed = new ArrayList<>(); |
| | | List<BigDecimal> congestion = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | ObservationDailyOperation row = byDate.get(start.plusDays(i)); |
| | | for (HolidayCalendar day : days) { |
| | | ObservationDailyOperation row = byDate.get(day.getHolidayDate()); |
| | | speed.add(row == null ? null : row.getAverageSpeed()); |
| | | congestion.add(row == null ? null : row.getAverageCongestion()); |
| | | } |
| | |
| | | |
| | | private List<BigDecimal> percentageChanges(List<BigDecimal> current, List<BigDecimal> previous) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) { |
| | | int size = Math.max(current == null ? 0 : current.size(), previous == null ? 0 : previous.size()); |
| | | for (int i = 0; i < size; i++) { |
| | | BigDecimal currentValue = valueAt(current, i); |
| | | BigDecimal previousValue = valueAt(previous, i); |
| | | BigDecimal ratio = HolidayNumberUtils.divide(currentValue, previousValue); |
| | |
| | | } |
| | | |
| | | private List<BigDecimal> nulls() { |
| | | return nulls(HolidayTrendChartService.DAY_LABELS.size()); |
| | | } |
| | | |
| | | private List<BigDecimal> nulls(int size) { |
| | | List<BigDecimal> result = new ArrayList<>(); |
| | | for (int i = 0; i < HolidayTrendChartService.DAY_LABELS.size(); i++) result.add(null); |
| | | for (int i = 0; i < size; i++) result.add(null); |
| | | return result; |
| | | } |
| | | |
| | | static List<String> dayLabels(List<HolidayCalendar> days) { |
| | | List<String> labels = new ArrayList<>(); |
| | | if (days == null) return labels; |
| | | for (HolidayCalendar day : days) { |
| | | if (day == null || day.getHolidayDate() == null) continue; |
| | | labels.add(day.getHolidayDate().getMonthValue() + "月" |
| | | + day.getHolidayDate().getDayOfMonth() + "日"); |
| | | } |
| | | return labels; |
| | | } |
| | | |
| | | private HolidayTrendWordService.ImageSize lineImageSize(long width, long height) { |
| | | return new HolidayTrendWordService.ImageSize( |
| | | width, HolidayTrendChartService.scaleLineChartHeight(height)); |
| New file |
| | |
| | | package com.trafficaudit.holiday.service; |
| | | |
| | | import com.trafficaudit.holiday.entity.HolidayCalendar; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.List; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.assertEquals; |
| | | |
| | | class HolidayTrendServiceTest { |
| | | |
| | | @Test |
| | | void buildsTrendAxisFromConfiguredHolidayDays() { |
| | | List<HolidayCalendar> days = new ArrayList<>(); |
| | | for (int index = 1; index <= 7; index++) { |
| | | HolidayCalendar day = new HolidayCalendar(); |
| | | day.setYear(2026); |
| | | day.setHolidayType("NATIONAL_DAY"); |
| | | day.setDayIndex(index); |
| | | day.setHolidayDate(LocalDate.of(2026, 10, index)); |
| | | days.add(day); |
| | | } |
| | | |
| | | assertEquals(Arrays.asList( |
| | | "10月1日", "10月2日", "10月3日", "10月4日", |
| | | "10月5日", "10月6日", "10月7日"), |
| | | HolidayTrendService.dayLabels(days)); |
| | | } |
| | | } |