一、整体架构
小程序前端 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 的
gzipfeature 会自动发送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)。