十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Java的河南气象数据可视化系统:ETL、聚合与预警

基于Java的河南气象数据可视化系统:ETL、聚合与预警 简介面向计算机科学与技术等专业毕业设计阶段的学习者这份文档是一篇河南省气象数据可视化系统设计与实现的完整本科论文适合需要参考同类选题结构、技术选型与写作思路的毕业生。压缩包内仅含1个docx文件约2.09MB为排版完整的论文正文含中英文摘要、关键词与目录便于对照章节框架梳理写作脉络。内容围绕SpringBoot后台、MyBatis映射、Redis缓存、MySQL存储与Bootstrap前端布局展开并引入Hadoop与MapReduce完成气候数据的分布式统计与分析。功能层面覆盖前台的天气详情、报警预警、天气上报、监测个性化以及后台的天气管理、预警管理、分类管理、气候数据管理、异常监测与账户上报管理。论文完整记录了需求分析、模块划分、SSM框架搭建、数据库关系设计到编码实现的过程可借鉴开题与章节组织的写法。目前已有214人学习下载。1. 河南气象数据接进来那一刻才知道 Java 可视化系统的难点不在画图把「基于 Java 的气象数据可视化系统」当成画图题做通常第三天就会卡住。真正吃时间的是数据侧河南全省 18 个地市郑州、开封、洛阳、平顶山、安阳、鹤壁、新乡、焦作、濮阳、许昌、漯河、三门峡、南阳、商丘、信阳、周口、驻马店、济源的站点观测小时粒度一年就是几十万到上百万行字段里还夹着 -9999、999999 这类哨兵值直接扔给 ECharts 只会画出一堆穿模的折线。这个题目要解决的是三件事把多源气象观测规整成可查询的结构化数据、在后端用 Java 做聚合与阈值判断、再让前端按地市和时间窗把温度、降水、风速这些要素直观呈现出来。适合做课程设计、毕业设计的同学也适合想练一遍「采集—入库—聚合—可视化」完整链路的 Java 后端。数据可视化系统这四个字里可视化只是最后一公里的出口。2. 用 Spring Boot 搭河南气象数据可视化系统的后端骨架这套系统的技术栈不用花哨Spring Boot MyBatis MySQL ECharts 足够覆盖绝大多数场景真到数据量上去再考虑把观测事实表换成时序库。骨架阶段最容易翻车的是建模不是编码。2.1 气象数据模型设计站点、要素与时间粒度气象观测天然是星型结构站点维、要素维、时间维中间挂事实。站点维描述「在哪里测」要素维描述「测什么」时间维描述「什么时候测」事实表只存数值和状态标记。常见的错误是把温度、降水、风速拆成三张表查询时靠 union 拼一个地市的温度曲线就要扫三张表正确做法是一行一站点一时刻多个要素横向铺开虽然宽一点但聚合时一次扫描就够。时间粒度要先定死。小时观测、日统计、月统计三种粒度混在一张表里分组条件会写得非常难受。我一般只落小时粒度日值和月值靠 SQL 聚合出来需要极限性能再加物化表。另外提醒一点所有时间统一存北京时间、统一用DATETIME不要一半TIMESTAMP一半字符串时区问题后面会专门说。表名作用主键/唯一键预计量级t_station站点维表含经纬度与地市归属station_code百级t_element要素字典含单位与精度element_code十级t_obs_hour小时观测事实表station_code obs_time百万级t_alarm_rule阈值预警规则rule_id十级2.2 河南省站点观测表怎么定字段和索引表结构定完再改成本比想象中高所以字段类型要一次选对。气温用DECIMAL(5,2)能表达 -99.99 到 999.99别用FLOAT否则前端对不上数降水量同样用DECIMAL湿度、风向这种小整数用TINYINT UNSIGNED就够。CREATE TABLE t_obs_hour ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, station_code VARCHAR(12) NOT NULL COMMENT 区站号如 57083, city_code CHAR(4) NOT NULL COMMENT 地市行政区划码如 4101, obs_time DATETIME NOT NULL COMMENT 整点观测时间北京时间, temp DECIMAL(5,2) COMMENT 气温 ℃, prcp DECIMAL(6,2) COMMENT 小时降水量 mm, wind_speed DECIMAL(5,2) COMMENT 风速 m/s, humidity TINYINT UNSIGNED COMMENT 相对湿度 %, pressure DECIMAL(7,2) COMMENT 气压 hPa, data_flag TINYINT NOT NULL DEFAULT 0 COMMENT 0正常 1缺测 2可疑, PRIMARY KEY (id), UNIQUE KEY uk_station_time (station_code, obs_time), KEY idx_city_time (city_code, obs_time, data_flag) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_station_time的作用不只是防重它让「某站点某小时」的补数变成INSERT ... ON DUPLICATE KEY UPDATE一条语句。idx_city_time的列顺序必须是city_code在前因为绝大部分查询都是「郑州 某个时间范围」把obs_time放前面会退化成范围扫描加回表。data_flag放在索引末尾是为了让「只统计正常值」的过滤条件尽量留在索引里完成。数据量超过三千万行以后按obs_time做 RANGE 分区能明显减少历史查询的扫描量但分区键必须进主键PRIMARY KEY (id, obs_time)这种组合主键要在建表时就规划好中途改分区代价很大。2.3 聚合接口MyBatis 里写 SQL 还是 Service 里写 Stream「按地市统计某时间段平均气温、累计降水」这类需求用 SQL 聚合永远比捞回内存再算快。面试八股文里背的 Spring 循环依赖、Bean 生命周期落到这个系统上几乎用不到真正咬人的是事务边界和聚合位置选错。常见做法是 Mapper 出聚合结果Service 只做单位换算和结构转换。Mapper public interface ObsMapper { // 按地市聚合只统计正常值时间用左闭右开避免跨天重复计算 Select( SELECT city_code AS cityCode, AVG(temp) AS avgTemp, SUM(prcp) AS sumPrcp, AVG(wind_speed) AS avgWind, COUNT(*) AS sampleCnt FROM t_obs_hour WHERE obs_time #{start} AND obs_time #{end} AND data_flag 0 GROUP BY city_code ) ListCityAgg aggByCity(Param(start) LocalDateTime start, Param(end) LocalDateTime end); }start闭、end开是硬约定写成BETWEEN会让边界那一小时在两天的查询里各算一次。data_flag 0必须显式带上缺测值参与平均会把结果拉偏。返回的CityAgg建议用BigDecimal承接别用Double否则前端拿到的 23.400000000000002 会被用户截图吐槽。补一个转换细节地市列表需要固定 18 项聚合结果里可能只有 15 个地市有数据缺的要补零否则 ECharts 地图上会出现空白区。用 Stream 处理很顺手String[] allCities cityRepo.listAllCodes().stream() .map(City::getCityCode) .toArray(String[]::new); // 固定的 18 个地市编码 MapString, CityAgg aggMap mapper.aggByCity(start, end).stream() .collect(Collectors.toMap(CityAgg::getCityCode, a - a)); // 缺失地市补默认值保证地图着色完整2.4 定时采集与线程池的取舍采集任务不要每次调用都Executors.newFixedThreadPool线程池要作为 Spring 单例 Bean 注入核心线程数按「地市并发数 × 1.5」估一般 6 到 8 就够再大只会把下游接口压垮。任务用Scheduled(cron 0 5 * * * ?)每小时第 5 分钟触发给上游留出数据落地时间。任务本身要幂等靠前面那个唯一键兜底重复跑不会产生脏数据。3. 河南省气象数据 ETL从原始报文到可查询的观测表采集这层最容易被低估。原始数据可能来自文本报文、CSV 导出或接口 JSON字段名、单位、缺失标记各不相同能不能稳定入库决定了后面所有可视化的可信度。3.1 哨兵值与缺失值的统一处理气象数据里「缺测」不是空值而是特定数字。常见的对照关系是气温 -9999、降水 999999、风速 -9999这些值一旦算进平均某地市全年均温能直接从 15℃ 掉到 -300℃。处理策略是入库时统一转成NULL同时把data_flag置为 1。要素原始缺测标记入库处理data_flag气温-9999置NULL1降水999999 / -9999置NULL1风速-9999置NULL1气压99999置NULL1湿度超出 0~100保留原值2可疑湿度超出物理范围时不要丢标记成可疑更合理因为这类值往往意味着传感器漂移排查时需要看到原始痕迹。3.2 小时级增量拉取用 CountDownLatch 等所有地市跑完多个地市并行拉取主线程要等全部完成再写批次日志。这种「线程等待都完成」的场景用CountDownLatch最直接比Future.get()的循环写法更清爽。Scheduled(cron 0 5 * * * ?) // 每小时第 5 分钟执行 public void pullHourly() throws InterruptedException { ListString cities stationRepo.listCityCodes(); // 18 个地市 LocalDateTime slot LocalDateTime.now().withMinute(0).withSecond(0).withNano(0) .minusHours(1); // 上一整点 CountDownLatch latch new CountDownLatch(cities.size()); for (String city : cities) { collectPool.submit(() - { try { ListObsHour rows remoteClient.fetch(city, slot); obsService.saveBatch(rows); // 内部用 ON DUPLICATE KEY UPDATE } catch (Exception e) { log.warn(采集失败 city{} slot{}, city, slot, e); failCounter.increment(); // 失败不中断其他地市 } finally { latch.countDown(); // 必须放在 finally } }); } boolean finished latch.await(10, TimeUnit.MINUTES); if (!finished) { log.error(采集超时 slot{}已失败 {} 个地市, slot, failCounter.sum()); } }三个参数点值得留意latch.await一定要带超时否则某个地市接口假死会把调度线程挂住下一小时的触发被阻塞countDown()放finally异常路径也必须放行单个地市的异常只记录不抛出避免一个城市拖垮整批。3.3 批量写入参数一次 500 行还是 5000 行MyBatis 批量插入的性能拐点通常在 500 到 1000 行之间。低于 500 行网络往返占主导高于 5000 行SQL 包体过大、max_allowed_packet容易被打爆而且失败时整批回滚重试成本高。我会按 500 行切批并在 JDBC 连接串里加rewriteBatchedStatementstrue这条参数能让 MySQL 驱动把多条 INSERT 合并成一条多值语句实测差一个数量级。入库连接串参考jdbc:mysql://127.0.0.1:3306/weather?useUnicodetruecharacterEncodingutf8mb4\ serverTimezoneAsia/ShanghairewriteBatchedStatementstrueuseSSLfalseserverTimezone必须显式指定。不写的话驱动按 JVM 默认时区解释时间容器里 JVM 常常是 UTC入库时间会比北京时间少 8 小时而这个错误在前端只表现为「曲线整体左移了 8 小时」很容易被误判成前端时区处理问题。3.4 入库前的三条数据质量校验采集完不校验等于把排查成本推给用户。我会在saveBatch之前跑三条规则站点号必须能在t_station里找到找不到直接丢观测时间必须是整点且不晚于当前时间未来时间说明上游口径有问题同一批次内station_code obs_time不允许重复重复说明接口分页逻辑出错。三条规则都不通过就写告警日志不静默丢弃否则数据缺口永远查不出来。4. 用 ECharts 与 Java 聚合接口做河南省气象可视化后端把数据聚合对了前端才谈得上好看。这个系统的可视化通常就三类图地市着色地图、时间序列折线、要素对比柱状。4.1 聚合接口的参数设计与返回结构接口参数只留三个start、end、cities。时间范围一定要有上限校验超过 366 天直接返回参数错误否则一个手滑就能让数据库扫全表。参数类型必填说明startstring是yyyy-MM-dd HH:mm:ss闭区间endstring是同上开区间citiesstring否逗号分隔的地市编码空表示全部elementstring是temp/prcp/wind返回体固定为{code, msg, data}三段data里是地市编码到数值的映射前端不需要再做二次遍历。GetMapping(/city/summary) public RListCityAggVO citySummary( RequestParam DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss) LocalDateTime start, RequestParam DateTimeFormat(pattern yyyy-MM-dd HH:mm:ss) LocalDateTime end, RequestParam(required false) String cities) { if (Duration.between(start, end).toDays() 366) { return R.fail(时间跨度不能超过 366 天); // 防止全表扫描 } return R.ok(obsService.citySummary(start, end, split(cities))); }DateTimeFormat不能省不写的话 Spring 只会按 ISO 格式解析前端传2024-06-01 08:00:00会直接 400。4.2 河南地图注册与地市着色ECharts 本身不带河南边界需要先拿到行政区划码 410000 对应的 geoJSON用registerMap注册一次后续所有地图实例共用。import * as echarts from echarts; import henanJson from /assets/henan-410000.json; echarts.registerMap(henan, henanJson); // 全局注册一次即可 const option { tooltip: { trigger: item, formatter: {b}br/平均气温{c} ℃ }, visualMap: { min: -10, max: 40, calculable: true, inRange: { color: [#3b82f6, #93c5fd, #fde68a, #ef4444] } }, series: [{ type: map, map: henan, roam: true, label: { show: true, fontSize: 11 }, data: cityData // [{name:郑州, value: 27.3}, ...] }] };三个坑visualMap.min/max不要写死成固定值按当前时间窗的实际极值动态算否则冬天整张图一片蓝色地市name必须和 geoJSON 里的名称完全一致写「郑州市」和「郑州」是两套匹配roam: true打开后移动端要限制缩放层级不然用户很容易把地图拖出视野再也找不回来。4.3 温度、降水、风速三要素联动双 Y 轴是最常见的做法气温走左轴降水走右轴并以柱状呈现风速用第三轴或者干脆用另一张图。同一个站点的时间序列注意prcp是累计值temp是瞬时值两者的聚合方式不同降水要用SUM、气温用AVG混用会让结果完全失真。series: [ { name: 气温, type: line, smooth: true, yAxisIndex: 0, data: tempSeries }, { name: 降水量, type: bar, yAxisIndex: 1, data: prcpSeries, barMaxWidth: 14 }, // 柱宽上限避免小时点密集时糊成一片 { name: 风速, type: line, yAxisIndex: 0, lineStyle: { type: dashed }, data: windSeries } ]barMaxWidth是个实用参数。按小时查询一年就是 8760 个点不限制柱宽柱子会彼此压叠变成一块色块视觉上完全读不出信息。4.4 点数太多就降采样别让前端硬扛超过 2000 个数据点时折线图渲染会明显掉帧。降采样在后端做常用 LTTB最大三角形三桶算法保留趋势拐点或者退一步用固定间隔抽样。抽样间隔按Math.ceil(pointCount / 1500)计算简单、够用且不会像平均值抽样那样把极端降水峰值抹平。可视化系统里峰值恰恰是用户最关心的东西。5. 阈值预警与可视化系统的排错技巧预警是这个系统从「能看」到「有用」的分界线。做法上建议把规则抽成接口用策略模式实现多种组合新增一个「连续 3 小时降水超 30mm」的规则只加一个实现类不改主流程。public interface AlarmRule { boolean match(ListObsHour recent); // 输入最近若干小时观测 String level(); // 蓝/黄/橙/红 } Service public class Rain3hRule implements AlarmRule { public boolean match(ListObsHour recent) { return recent.size() 3 recent.stream() .limit(3) .mapToDouble(o - o.getPrcp() null ? 0 : o.getPrcp().doubleValue()) .sum() 30.0; // 3 小时累计降水阈值单位 mm } public String level() { return 橙色; } }规则执行放在采集完成之后避免和入库竞争连接。match只接收数据、不查库这样单元测试可以脱离数据库直接跑规则数量涨到几十条也不会互相污染。排错环节有三个高频故障。接口偶发超时先看是不是某个时间窗触发了全表扫描用EXPLAIN确认idx_city_time是否被命中曲线整体偏移 8 小时回头查 JDBC 的serverTimezone和容器内 JVM 时区地图整块空白多半是 geoJSON 没注册成功或者地市名称对不上在控制台打印echarts.getMap(henan)就能判断。另外部署时别忽略 JDKjava -version出来的可能不是你JAVA_HOME指的那个版本环境变量配置混乱导致的UnsupportedClassVersionError特别常见容器化部署时把基础镜像的 JDK 版本和本地对齐能省掉一整晚的排查。本文还有配套的精品资源点击获取
返回列表