从天气数据到行动提醒:一个降雨预警系统的设计与实现
文章目录
天气应用通常擅长告诉我们“今天有雨”,生活中的问题却更具体:雨大约什么时候到、是否来得及处理户外物品、网页没有打开时能否主动提醒?
我把这个需求做成了一个轻量的降雨预警网站:rain.actpi.com。它聚合分钟级降水、实况天气、逐小时预报、官方预警和雷达回波,再把原始数据转换为一条容易执行的建议。
本文不展开真实监测地点、坐标、通知地址和线上调度时段,只讨论可以复用的设计方法与工程经验。
先定义“要做什么决定”
如果只是展示温度和天气图标,调用一次天气接口就够了。但降雨提醒真正要回答的是:
- 未来一段时间内是否会开始降雨;
- 从收到提醒到完成处理还剩多少时间;
- 降水只是短暂飘雨,还是已经达到需要行动的程度;
- 附近雷达回波是否正在靠近;
- 页面无人查看时,后台能否持续监测且避免重复通知。
因此,系统的核心不是“天气数据看板”,而是一个持续更新的决策器。输入是多个时间尺度的气象数据,输出则应尽量简单:立即处理、继续观察或暂时安全。
这也带来一个重要原则:先定义行动窗口,再选择数据源和阈值。否则页面很容易堆满数据,却仍然无法帮助用户做决定。
系统架构:核心判断只保留一份
线上版本由静态页面、Netlify Functions、定时任务和状态存储组成:
浏览器
├─ 天气接口 ── 分钟级降水、实况、逐小时预报、官方预警
└─ 雷达接口 ── 雷达元数据与地图瓦片
定时监测任务
├─ 调用同一套风险分析逻辑
├─ 保存上一次提醒状态
└─ 通过已配置的消息通道发送通知
浏览器不直接保存第三方服务凭据,而是请求站点自己的 Serverless API。函数负责签名、超时控制、响应校验和字段整理,前端只接收展示所需的结构化结果。
风险判断被提取成共享模块,网页查询和后台监测都调用它。这样可以避免最麻烦的一类问题:页面认为风险很低,定时任务却因为另一套规则发送了紧急提醒。
地点精度与隐私边界
降雨具有明显的局地差异,使用城市或行政区级天气往往过于宽泛。系统允许用坐标指定监测点,再根据数据供应商支持的精度映射到相应天气网格。
这里需要区分两个概念:
- 用户选择的地点:用于地图定位和交互;
- 供应商实际查询网格:由接口支持的坐标精度决定。
页面会说明数据对应的是附近网格,而不是把结果包装成“某栋建筑上空的精确预报”。坐标在进入接口前还要检查数值范围,避免异常参数被转发给上游服务。
自定义地点只保存在浏览器本地存储中,不同步到公共服务端。仓库和文章同样不记录家庭、学校、单位等真实地点,也不公开通知密钥、Webhook 或生产环境变量值。
把降水量翻译成行动等级
“未来有雨”并不等于“现在必须行动”。为了减少无意义提醒,我把判断拆为三个层次:
- 观察窗口:在未来若干个分钟级时段中寻找最早降雨;
- 处置余量:用预计开始时间减去处理所需时间;
- 风险等级:综合峰值、累计量和持续时长判断影响。
一个简化模型可以写成:
若窗口内没有有效降水:暂时安全
若只有零星、短暂降水:继续观察
若单时段强度或连续累计超过阈值:建议尽快处理
阈值不是气象行业标准,而是针对具体使用场景的经验参数。户外物品的材质、遮挡、风向和容错程度不同,结果也会不同。因此页面应同时显示最早降雨时间、窗口累计量、峰值和剩余时间,让用户能够理解建议是如何得出的。
比“预测得绝对准确”更现实的目标,是让规则透明、参数可调整,并在每次获得新数据后重新计算。
雷达图是辅助信号,不是答案
分钟级预报适合做数值判断,雷达图则擅长回答另一个问题:附近哪里正在下雨,回波大致往哪个方向移动?
系统在 Leaflet 地图上叠加最近几帧雷达瓦片,并实现了一个保持简单的回波外推:
- 从连续雷达帧中抽样有效回波像素;
- 计算每帧回波的加权中心;
- 根据相邻帧位移估算移动方向和速度;
- 将最新回波按该速度向前平移;
- 判断平移后的回波是否接近监测点。
这个方法便于解释,也足够用来提示“回波正在靠近”。但它无法预测雨区的新生、增强、分裂或消散,更不是数值天气预报。因此雷达外推只作为辅助信号,不能覆盖分钟级降水和官方预警的结论。
让附加功能失败时,核心功能仍可用
天气站点依赖多条外部链路:实况、逐小时预报、预警、雷达元数据、地图底图和通知服务都可能独立失败。可靠性设计的关键,是让故障局部化。
系统将分钟级降水作为核心数据,其余请求并行执行并独立处理结果:
- 实况接口超时,不影响降雨风险卡片;
- 雷达服务不可用,仍保留天气判断和地图底图;
- 底图加载失败,页面给出明确提示,而不是无限显示“加载中”;
- 通知通道失败,会记录错误并允许其他已配置通道继续尝试。
对于可能返回代理错误页的接口,也不能直接假设响应一定是 JSON。更稳妥的处理顺序是:先读取文本,检查 HTTP 状态,再显式解析 JSON,最后验证关键字段。
const response = await fetch(url, { signal });
const body = await response.text();
if (!response.ok) {
throw new Error(`upstream request failed: ${response.status}`);
}
let data;
try {
data = JSON.parse(body);
} catch {
throw new Error('upstream did not return valid JSON');
}
缓存策略也按数据类型区分:分钟级降水保持较短缓存,变化较慢的实况和逐小时数据可以适当延长。接口超时时,如果已有一份仍具参考价值的缓存,页面会标注数据年龄,而不是直接清空所有内容。
消息通知本质上是一个状态机
定时任务可以频繁检查,但不能每次发现同一片雨区就发送一条消息。为此,状态存储至少要记录:
- 上一轮是否处于风险状态;
- 风险首次出现和最近一次提醒的时间;
- 上一轮风险摘要;
- 当前风险是否已经解除。
通知逻辑可以简化为:
安全 → 风险:发送提醒
风险 → 风险:仅在等级显著上升或冷却期结束后提醒
风险 → 安全:更新状态,不发送紧急消息
安全 → 安全:不操作
这种边沿触发比“每次轮询都发送”更符合人的使用习惯。多个通知通道可以并存,但访问令牌和 Webhook 只能放在 Netlify 的服务端环境变量中,绝不能写进前端脚本或提交到仓库。
Netlify 上的部署方式
项目采用静态前端加 Serverless Functions 的结构。核心目录大致如下:
web/ # 静态页面与交互逻辑
netlify/functions/ # 天气、雷达和定时监测入口
netlify/lib/ # 共享分析与通知模块
netlify.toml # 构建、路由和调度配置
前端通过站内路径访问天气和雷达接口,由 Netlify 重写到对应函数。生产环境再按配置的频率和时段运行监测任务,并使用持久化存储保存去重状态。
部署中最值得坚持的几条规则是:
- 仓库只提交环境变量名和示例值,不提交真实凭据;
- 预览部署默认不发送真实通知;
- 查询函数与定时任务共享判断模块;
- 修改环境变量后重新部署,并进行一次受控的通知测试;
- 对上游调用设置超时、缓存和清晰的降级路径。
本地调试时,应先使用模拟数据或测试通知完成规则验证,再执行单次真实查询;确认结果无误后,最后开启周期监测。这样既能节省接口额度,也能避免调试阶段打扰实际接收者。
开发中踩过的坑
地图初始化顺序
Leaflet 要先设置地图中心和缩放级别,再添加图层。否则可能出现 Set map center and zoom first。地图容器尺寸发生变化后,还需要重新计算布局,否则瓦片可能只渲染一部分。
把轮询频率误认为数据频率
客户端每分钟请求一次,并不代表上游每分钟都会生成新预报。轮询周期应参考数据源的实际更新时间,并结合缓存与接口额度设置。更频繁地获取同一批数据,只会增加请求量。
混淆查询时间与数据时间
“页面什么时候刷新”和“供应商什么时候更新数据”是两个不同时间。页面同时展示两者后,用户才能判断看到的是缓存、延迟数据还是最新结果。
让一个超时拖垮整个页面
早期实现把多个接口串联起来,只要云量或雷达请求超时,核心降雨判断也无法显示。改成并行查询、独立超时和模块化降级后,页面在外部服务波动时仍然可用。
仍然存在的边界
这个系统适合个人场景中的辅助决策,但它仍有清晰边界:
- 公共天气网格不能代替现场雨量计;
- 简单回波外推无法描述对流系统的快速生消;
- 第三方天气、地图和消息服务都可能限流或中断;
- 本地保存的地点不会自动同步到其他设备;
- 经验阈值需要根据实际误报和漏报持续调整。
涉及人身安全、灾害避险或生产调度时,应以气象主管部门发布的预警和专业系统为准,不能只依赖这个轻量工具。
结语
这个项目最有价值的部分,不是把更多天气字段搬到网页上,而是完成了从数据到行动的转换:
什么时候可能下雨?现在是否需要处理?如果我没打开页面,系统能否及时提醒?
当数据源、判断规则、通知状态和失败边界都被明确下来,一个看似简单的生活需求就变成了可解释、可维护的工程系统。天气仍然充满不确定性,但好的工具可以把这种不确定性压缩成更清晰的下一步。