2团
Published on 2026-07-13 / 0 Visits
0
0

和风天气接入与优化实践

一、整体架构

小程序前端                    Rust 后端                         和风天气
─────────────              ───────────────                  ─────────────
record.vue                 /weather handler
  自动获取天气        ──▶   1. 日期判断(今天/历史)
  (今天+有定位)             2. 查缓存(命中则直接返回)            ┌─ /v7/weather/now(实时)
                           3. 限流检查(缓存未命中才计数)   ──▶  ├─ /geo/v2/city/lookup(GeoAPI)
                           4. JWT 鉴权 + 调用和风 API            └─ /v7/historical/weather(时光机)
                           5. 写缓存 + 返回
WeatherIcon 组件
  根据 weather code
  渲染图标字体

天气数据最终作为字段(weather 图标代码、temperature 温度)随心情记录一起存储,回看时直接读记录字段,不再重复请求。


二、前端实现

2.1 图标字体方案

WeatherIcon.vue 接收和风图标代码(如 100 = 晴),生成 CSS 类名 qi-100,由全局 CSS 的伪元素注入字符:

<text class="weather-icon" :class="`qi-${code}`" :style="{ fontSize: `${size}rpx` }" />
/* 全局 qweather-icons.css,共 1007 个图标类 */
@font-face { font-family: 'qweather-icons'; src: url('qweather-icons.woff2') format('woff2'); }
.qi-100::before { content: '\f101'; }  /* 晴 */

相比 emoji 或图片,图标字体的优势:矢量清晰、体积小(woff2 约 54KB 覆盖全部图标)、随字体颜色主题变化。

一个对齐坑:qweather-icons.css 给 ::before 默认设了 vertical-align: -0.125em(为「图标与正文文字混排」做的下沉补偿)。但本组件用固定 1em 盒子承载字形,叠加后字形会落到盒子几何中线之下,在 flex 行(如首页「位置 · 天气」)里表现为图标偏下、没和同行文字对齐。修复是覆盖 ::before 的对齐:

// WeatherIcon.vue(scoped,特异性高于全局字体规则)
.weather-icon {
  display: inline-block;
  height: 1em;
  line-height: 1;
  &::before { vertical-align: baseline; }   // 抵消字体默认的 -0.125em 下沉
}

注意 .weather-icon 自身的 vertical-align 在 flex 父容器里会被忽略,必须覆盖 ::before 才生效。


三、后端实现

3.1 JWT 鉴权(EdDSA / Ed25519)

和风推荐用 JWT 而非 API Key 鉴权。私钥在服务启动时一次性解析,签发的 token 缓存在进程内:

// qweather.rs —— token 有效期 1 天,剩余 <5min 提前刷新
pub fn token(&self) -> Result<String, AppError> {
    // 快速路径:缓存未过期直接复用,避免每次请求都重新签名
    let guard = self.cache.lock();
    if let Some(cached) = guard.as_ref() {
        if cached.exp > unix_now() + TOKEN_REFRESH_MARGIN_SECS {
            return Ok(cached.token.clone());
        }
    }
    self.refresh()  // 慢路径:重新签发
}

请求时通过 Authorization: Bearer <token> 头发往项目专属 API Host(Host 本身也是身份认证的一部分,由和风控制台分配)。

3.2 三个上游接口

接口 用途 location 参数
/v7/weather/now 今天实时天气 经纬度 lng,lat
/geo/v2/city/lookup 经纬度→城市 ID 经纬度 lng,lat
/v7/historical/weather 近 10 天历史(时光机) 必须城市 ID

历史天气的难点:时光机接口的 location 只接受城市 ID,不接受经纬度。故查询历史前必须先调 GeoAPI 转换,LocationID 单独缓存(对坐标稳定不变,24h 复用):

// LocationID 与天气数据分开缓存——它永不变,可独立设更长 TTL
static LOCATION_ID_CACHE: LazyLock<Cache<String, String>> = LazyLock::new(|| {
    Cache::builder().max_capacity(1000)
        .time_to_live(Duration::from_secs(24 * 60 * 60))
        .build()
});

历史天气的温度算法:daily 只给最高/最低温,取均值 (tempMax + tempMin) / 2;天气图标从 24 条逐时数据中取正午时段作为当日代表(正午缺则取频次最高,最后兜底首条)。


四、缓存与请求优化

4.1 缓存分层策略(对照官方 cache 文档)

问题

原实现用单一缓存(30min TTL)覆盖所有天气数据。但历史天气数据永不变——30 分钟刷新一次纯属浪费调用额度。

方案:按时效性分层

// 实时天气:当天天气会变,30min 刷新(符合官方推荐)
static REALTIME_CACHE: LazyLock<Cache<String, WeatherInfo>> = ...
    .time_to_live(Duration::from_secs(30 * 60))   // 30min

// 历史天气:永不变,延长到 24h
static HISTORICAL_CACHE: LazyLock<Cache<String, WeatherInfo>> = ...
    .time_to_live(Duration::from_secs(24 * 60 * 60))  // 24h

handler 里按 is_today 分流到对应缓存:

let cache = if is_today { &REALTIME_CACHE } else { &HISTORICAL_CACHE };
if let Some(cached) = cache.get(&cache_key) {
    return Ok(/* 直接返回缓存 */);
}

效果

历史天气的重复查询从「每 30min 调一次和风」降到「24h 调一次」,对有限的历史日期(最近 10 天)和有限的区域(1km 精度聚合)而言,缓存命中率接近 100%。

官方文档强调:缓存时长应根据数据种类设置弹性策略,且必须支持清除。我们的缓存 key 含 {lat:.2},{lng:.2}:{date},坐标截断 2 位小数(~1km 精度,和风参数本就限 2 位),同区域多用户共享一次调用。

4.2 gzip 与请求优化(对照官方 gzip / optimize-requests 文档)

gzip 自动解压

和风 API 默认返回 gzip 压缩响应,若不解压会导致 .json() 解析失败。在全局 HTTP 客户端启用 gzip:

// main.rs —— 全局共享客户端
let http_client = reqwest::Client::builder()
    .timeout(Duration::from_secs(15))       // 全局总超时
    .connect_timeout(Duration::from_secs(5))
    .gzip(true)                             // 自动解压 gzip 响应
    .build()?;

官方文档:gzip 能极大减少网络流量、加快请求。reqwest 的 gzip feature 会自动发送 Accept-Encoding: gzip 头并透明解压,无需手动处理。

避免重试风暴

和风文档强调:错误时不要立即重试,否则可能被视为 DDoS 触发 429。我们的实现是单次请求、失败即返回(不做自动重试):

// 时光机 404(订阅未开通)→ 直接返回,而非重试
if resp.status() == reqwest::StatusCode::NOT_FOUND {
    return Ok(None);
}

这种「快速失败」策略符合官方的请求优化建议,避免错误请求堆积成重试风暴。

其他请求优化要点

  • 按需请求:前端仅在「记录今天心情 + 有定位」时自动获取一次,不预加载、不轮询
  • 超时分层:全局 15s + 单请求 10s,避免单个上游慢请求拖垮服务
  • LocationID 缓存:GeoAPI 结果缓存 24h,避免重复转换

五、配置与部署

和风相关配置通过环境变量注入(config.rs):

变量 说明 必需
QWEATHER_API_HOST 项目专属 API Host(控制台分配) 否(有默认值)
QWEATHER_PROJECT_ID Project ID(JWT sub)
QWEATHER_KEY_ID Credential ID(JWT kid)
QWEATHER_PRIVATE_KEY Ed25519 私钥 PEM(内嵌,优先) 二选一
QWEATHER_PRIVATE_KEY_FILE 私钥文件路径(默认 docker/hefeng/ed25519-private.pem 二选一

私钥文件通过 Docker volume 只读挂载进容器,权限需对齐容器运行用户(非 root 运行时需 chown 到容器 UID)。


Comment