广东水文项目问题排查实录:2026年3月19日技术挑战
2026年3月19日,对广东水文项目来说是一个充满挑战的工作日。在这一天的日常开发和维护中,我们接连遇到了三个不同层面的技术问题:数据采集层的数据缺失、业务层的数据表归属判断、以及数据展示层的时区和数据质量问题。本文将详细记录这三个问题的排查过程、根因分析以及修复方案,希望能为从事类似水文信息化项目的开发者提供一些参考和借鉴。
前言
广东水文项目是一个综合性较强的水利信息化系统,涵盖了水位监测、雨量监测、河道水情、水库水情等多种水文数据的采集、存储、查询和展示功能。系统采用典型的微服务架构,后端基于 Spring Boot 框架构建,数据存储使用 MySQL 数据库,业务数据按照不同的测站类型和数据特征分表存储。
项目中有几个核心的数据实体需要理解:
- 测站(Station):水文监测的基本单元,每个测站有一个唯一的测站编码(STCD),并且有一个测站类型(STTP)来标识其功能类型,如 ZZ 表示闸站、ZQ 表示河道站、RR 表示水库站、WD 表示水位站、YD 表示雨量站等。
- 实时数据(Realtime Data):最新的监测数据,通常以 5 分钟为采集间隔,包含水位、流量等关键指标。
- 历史数据(Historical Data):历史极值和历史记录,用于一站一档等分析展示功能。
在3月19日这一天,我们主要处理了与”一站一档”和”雨情实时信息”这两个业务模块相关的问题,下面逐一展开说明。
问题一:茜坑测站雨情实时数据缺失
问题现象
3月19日上午,收到用户反馈:茜坑测站(测站编码 81103420)在”雨情实时信息”页面无法查询到数据。然而奇怪的是,同一个测站在”一站一档”模块中能够正常搜索并显示数据。
用户描述的现象是:
- 进入”雨情实时信息”页面
- 搜索茜坑测站或直接输入测站编码 81103420
- 系统提示”没有数据”
但是在”一站一档”模块中,该测站能够正常显示其基本信息和历史数据。
排查过程
我首先检查了该测站的基础信息。通过查询 st_stbprp_b 测站基础表,确认茜坑测站存在且状态正常:
1 | SELECT STCD, STNM, STTP, USFL |
返回结果显示该测站类型为 YD(雨量站),使用标志为 1(启用)。
接下来,我分别查询了该测站在不同雨量数据表中的记录数量:
1 | -- 查询5分钟实时雨量表 |
查询结果如下:
| 数据表 | 数据类型 | 记录数 |
|---|---|---|
| ST_PPTN_R_MIN | 5分钟实时雨量 | 0条 |
| ST_PPTN_R | 小时雨量 | 860条 |
根因分析
经过排查,问题的根因已经非常清晰:
茜坑测站(81103420)只有小时雨量数据(ST_PPTN_R),没有 5 分钟实时数据(ST_PPTN_R_MIN)。
从技术角度来看,这是数据采集层面的问题,而不是代码 Bug。测站硬件或上报配置决定了它只上报小时级别的雨量数据,而不是 5 分钟级别的实时数据。
系统架构设计中,雨情实时信息模块默认查询的是 5 分钟实时数据表(ST_PPTN_R_MIN),而茜坑测站在该表中没有任何数据,因此返回空结果。这解释了为什么”雨情实时信息”页面无法显示数据。
而”一站一档”模块能够正常显示,是因为它查询的是小时雨量表(ST_PPTN_R),该表中存在茜坑测站的 860 条数据。
问题定性
经过分析,我们认为这是一个数据可用性问题,而非代码缺陷。系统按照设计正常运作,只是茜坑测站本身不具备 5 分钟实时雨量数据的采集条件。
可能的解决方案
针对这类问题,有几种潜在的解决方案可供考虑:
方案一:前端降级处理
当实时数据为空时,前端自动降级显示小时数据。这需要在 UI 层面增加逻辑判断,当检测到 5 分钟数据为空时,自动切换到小时数据视图,并给用户一个提示说明当前显示的是小时数据而非实时数据。
方案二:后端数据聚合
在后端处理层面,当查询 5 分钟数据为空时,自动从小时数据表中聚合数据返回。例如,可以将最近一小时的小时数据求和,作为实时数据的近似值返回。这种方案的优点是对前端透明,缺点是数据语义不够精确。
方案三:业务确认与配置调整
与水文部门确认茜坑测站的实际数据采集配置。如果该测站确实只需要上报小时数据,那么应该在系统配置中将其标记为”小时数据站”,前端据此显示不同的数据视图。
最终,我们与水文部门进行了沟通,确认茜坑测站由于硬件限制确实只支持小时数据采集,暂时采用方案一的前端降级处理作为短期解决方案,长期来看需要与设备部门协调升级采集设备。
问题二:江口水闸实时水情查询失败
问题现象
下午时段,接到另一个问题反馈:江口水闸(测站编码 81210300)在实时水情查询中无法找到数据。同样的,该测站在”一站一档”模块中能够正常显示。
具体表现:
- 在”实时水情”模块搜索江口水闸
- 系统提示没有数据
- 但在”一站一档”中能够看到该测站的基本信息和部分数据
系统排查
首先检查测站基础信息和数据分布情况。通过 SQL 查询发现:
1 | -- 检查测站基础信息 |
江口水闸的基础信息正常,测站类型为 ZZ(闸站),状态启用。
然后分别查询该测站在不同实时数据表中的情况:
1 | -- 检查河道实时表 |
查询结果:
| 数据表 | 数据类型 | 记录数 |
|---|---|---|
| st_river_r_min | 河道实时数据 | 0条 |
| st_rsvr_r_min | 水库实时数据 | 92条 |
这个结果非常有意思:江口水闸是一个 ZZ 类型(闸站)的测站,但其数据存储在水灾实时表中,而不是河道实时表中。
根因分析
经过深入分析,我们发现了问题的根本原因:
代码按测站类型区分查询数据表,但江口水闸的数据实际存储在与类型不匹配的表中。
系统的实时水情查询逻辑是:
1 | // 代码中的查询逻辑(修复前) |
江口水闸的测站类型是 ZZ,按照代码逻辑会查询河道实时表(st_river_r_min),但实际上该测站的数据存储在水库存实时表中(st_rsvr_r_min),所以查询结果为空。
这是一种数据录入或数据迁移过程中产生的不一致问题:测站的类型属性与其实际数据存储位置不匹配。
修复方案
为了解决这个问题,我们在 WaterRealtimeSummaryServiceImpl 中增加了 fallback 机制:当 ZZ 类型测站在河道表中没有数据时,自动尝试查询水库存实时表。
修改文件:WaterRealtimeSummaryServiceImpl(第 104-157 行附近)
核心修改思路:
- 新增
zzStations列表记录所有 ZZ 类型测站 - ZZ 类型优先查询河道表
- 新增 fallback 逻辑:如果河道表没有数据,则 fallback 查询水库存表
1 | // 2. 分离河道站和水库站 |
修复验证
修复后,我们通过手动指定时间同步接口进行了验证:
1 | POST /api/water/summary/sync?stcds=81210300&targetTime=2026-03-19 15:00:00 |
验证结果成功返回数据:
1 | { |
待修复问题:同步时间匹配
虽然上述修复解决了查询问题,但我们发现了一个关联的同步问题:
现状:同步任务使用精确时间匹配(TM = targetTime),例如查询 15:55 的数据。
问题:江口水闸数据的时间戳是 15:00、14:15 等非整 5 分钟时刻,这导致自动同步时使用精确匹配无法找到数据。
根本原因分析:
- 同步任务计算出”上一个 5 分钟”的时间点(如 15:55)
- SQL 查询使用
TM = #{tm}精确匹配 - 但江口水闸上报的数据时间戳是 15:00、14:15 等非整 5 分钟时刻
- 精确匹配查不到数据,所以汇总表始终没有这条数据
初步建议的解决方案:
方案一是修改 SQL 为范围查询,查询 targetTime 之前的最近一条数据:
1 | <!-- 查询最近的数据 --> |
方案二是在 Service 层处理,多次尝试向前查找直到找到数据或超过阈值。
这个问题需要进一步评估影响范围后再实施修复。
问题三:一站一档今日极值数据异常
问题描述
下午晚些时候,收到关于”一站一档”模块的数据异常反馈,涉及两个不同的字段问题:
问题 1:今日最低流量返回错误值 2.180,但实际数据库中存在更低的值 2.260
问题 2:历史最低水位返回 -9999(这是一个无效值占位符),实际应为 0.040
这两个问题发生在测站 81500150(水位站,类型 WD)上。
问题排查与分析
问题 1:今日最低流量错误
首先检查今日最低流量的计算逻辑。通过对比数据库中的实际数据,我们发现:
1 | -- 查询该测站今天(北京时间)的所有流量数据 |
实际查询结果显示最小的流量值确实是 2.260,但系统返回的今日最低流量却是 2.180。
经过仔细排查,我们发现问题的根因是时区不一致:
- 数据库
ST_RIVER_R表中存储的时间是 UTC 时间 - 代码使用
Calendar.getInstance()构建今日时间范围:2026-03-19 00:00:00 ~ 23:59:59 - MySQL 查询时把这个字符串当作 UTC 时间处理
- 导致实际查询范围变成了
2026-03-19 08:00 ~ 2026-03-20 07:59(北京时间) - 漏掉了北京时间 00:00~07:59 的数据,而真正的最低值 2.260 恰好在这个时间段内(UTC 19:00 = 北京时间 03:00)
问题 2:历史最低水位返回 -9999
历史最低水位的计算使用了 ST_RVEVS_R 表,通过 SQL 查询发现:
1 | -- 原 SQL |
问题在于 ST_RVEVS_R 表中存在 -9999 这种无效值占位符,原始 SQL 只过滤了 NULL,没有过滤负数,导致 -9999 被当作有效值返回。
修复方案
1. SQL 修改
修改 StRvevsRMapper.xml 中的 selectHistoricalMinLevel SQL,添加 AND LTZ > 0 过滤条件:
1 | -- 修改前 |
2. Java 修改:时区转换逻辑
修改 StationArchiveBiz.java 中的 fillTodayExtremes() 方法,使用 java.time 正确处理时区转换:
1 | // 北京时区 |
同样的时区转换逻辑也需要应用到水库站的今日极值计算方法 fillTodayReservoirExtremes() 中。
修复验证
修复后,两个问题都得到了正确的结果:
| 字段 | 修复前 | 修复后 | 状态 |
|---|---|---|---|
| historicalMinLevel | -9999.0 | 0.04 | 正常 |
| historicalMinLevelTime | 2013-02-21 | 2015-01-08 | 正常 |
| todayMinFlow | 2.180 | 2.260 | 正常 |
| todayMinFlowTime | 16:00(错误) | 03:00(正确) | 正常 |
正确的 UTC 查询范围应该是:
- 北京时间 2026-03-19 = UTC
2026-03-18 16:00:00~2026-03-19 15:59:59 - 该范围内最小流量为 2.260(UTC 19:00 = 北京时间 03:00)
影响范围评估
此次修复的影响范围仅限于以下接口的今日极值计算:
/basic/station/archive/monitoring/{stcd}?sttp=WD&subType=realtime(河道站)/basic/station/archive/reservoir/{stcd}(水库站)
不受影响的部分包括:
- 用户传入的 startTime/endTime 参数(用户自行处理时区)
- 历史极值(除 historicalMinLevel 外)
- 其他业务接口
经验总结与技术思考
通过这一天的问题排查和修复工作,我们总结出以下几点经验教训,希望能对类似项目的开发者提供一些参考。
1. 时区问题是分布式系统中常见的数据陷阱
在广东水文项目中,数据库服务器、应用服务器可能处于不同的时区配置下,而水文数据采集又涉及多个地区的数据汇总,时区问题很容易被忽视。
本次修复的时区问题是一个非常典型的案例:数据库存储 UTC 时间,代码使用本地时间构建查询条件,两者的不一致导致了数据查询范围的偏差,最终造成了极值计算错误。
建议:在系统设计阶段就应该明确规定时间的存储格式和查询方式,推荐做法是:
- 统一使用 UTC 时间存储
- 在数据访问层统一进行时区转换
- 在配置文件中显式声明时区设置
- 在接口文档中明确标注时间格式
2. 无效值占位符需要在数据入口处统一处理
水文数据中常见使用 -9999、-99、NULL 等作为无效值占位符。这种设计在数据采集和存储层面是合理的,但在数据展示和分析层面必须进行过滤。
本次修复中,历史最低水位查询就是因为没有过滤 -9999 这个无效值,导致返回了明显错误的结果。
建议:建立统一的数据清洗和过滤规范,在以下位置进行处理:
- 数据入库前的校验层
- 数据查询的 Mapper 层
- 数据展示的 Service 层
3. 测站类型与数据存储位置可能存在不一致
项目中经常会出现这样的情况:测站的类型属性(STTP)与实际数据存储位置不匹配。这可能是由于数据迁移、系统升级或人工录入错误导致的。
江口水闸的问题就是一个典型案例:测站类型为 ZZ(闸站),但数据存储在水库存实时表中而非河道实时表中。这种不一致性会导致按照类型判断数据表的代码逻辑失效。
建议:
- 在数据入口处增加校验,当测站类型与数据表不匹配时给出警告
- 在查询逻辑中增加 fallback 机制,提高系统的容错能力
- 定期进行数据一致性检查,发现并修复类型与数据不匹配的问题
4. 数据可用性问题需要业务层面配合解决
茜坑测站的问题本质上不是代码问题,而是数据采集层面的问题。测站硬件只支持小时数据采集,无法提供 5 分钟实时数据。对于这类问题,技术手段只能做到优雅降级或数据聚合,无法从根本上解决数据缺失的问题。
建议:建立测站能力档案,在系统中记录每个测站的数据采集能力和数据提供情况,让用户清楚地了解每个测站的数据范围和限制。
5. 代码审查和单元测试的重要性
这三个问题有一个共同点:它们都是由于代码逻辑在边界情况下处理不当导致的。如果有完善的单元测试覆盖这些边界场景,这些问题应该在开发阶段就被发现和修复。
建议:
- 加强对边界条件和异常情况的测试覆盖
- 定期进行代码审查,特别是涉及数据表切换、时区处理、数值边界等逻辑的代码
- 建立持续集成环境,在代码提交时自动运行相关测试
结语
2026年3月19日的这次问题排查经历,让我们深刻认识到水文信息化系统的复杂性和挑战性。从数据采集层的硬件限制,到数据存储层的数据表设计,再到数据展示层的时区和数据质量问题,每一个环节都可能成为问题的来源。
作为后端开发工程师,我们不仅要关注代码的功能实现,更要关注数据在各个环节的正确流转;不仅要处理正常的业务逻辑,更要考虑各种异常和边界情况;不仅要修复当前的问题,更要思考如何建立长效机制避免类似问题的再次发生。
水文信息化关乎国计民生,数据的准确性和系统的稳定性至关重要。希望本文的分享能为从事相关项目的开发者提供一些帮助,也欢迎大家交流探讨,共同进步。