PersonalWebApplication/docs/startpage-weather-location.md
cc f4342e7c82 docs: 天气定位失败时不兜底、不显示
线上不设 WEATHER_DEFAULT_LOCATION:高德定位失败即返回 failed,
不再回退默认城市、也不调和风,前端静默不显示。更新文档说明。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 20:35:54 +08:00

138 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 起始页:天气 + 定位(已实现)
导航起始页问候语下方展示「天气图标 · 城市 · 天气 · 气温」。
**城市来自高德 IP 定位,天气来自和风实时天气。** 密钥只在后端,前端不可见。
## 架构
```
浏览器 → GET /api/web/weather (后端)
├─ 高德 /v3/ip?ip=<访问者IP> → 城市名 + 城市中心坐标
└─ 和风 /v7/weather/now?location= → 天气文字 / 气温 / 图标码
```
- 后端:`Server/src/weather.rs`(服务 + 限流 + 缓存)、路由 `Server/src/routes/web.rs``/api/web/weather`
- 前端:
- `Client/src/api/weather.ts`、`Client/src/hooks/useWeather.ts`拉取30 分钟刷新)
- `Client/src/components/StartPage.tsx`:统一持有天气状态,派生**昼夜背景**与**天气动效**,并把天气传给 `Greeting`
- `Client/src/components/start/Greeting.tsx`:展示「天气图标 · 城市 · 天气 · 气温」
- `Client/src/components/start/WeatherIcon.tsx`:天气文字旁的静态图标
- `Client/src/components/start/WeatherFx.tsx`:雨/雪 canvas 动态层
## 限流与缓存(后端内置)
- **高德 300 次/天、和风 900 次/天**,各自独立计数,**北京时间 0 点归零**。
- 任一服务商额度用尽 → 接口返回 `{ ok:false, reason:"quota_exceeded", provider }`
前端显示「天气请求次数已用完,明日 0 点恢复」。
- **按访问者 IP 缓存 10 分钟**:前端每 30 分钟刷新一次,正常用量远低于额度。
- 计数只在“真的调用了上游”时增加(命中缓存不计数)。
## 环境变量
后端启动时读取(缺高德 Key 或和风 JWT 任一项时接口返回 `not_configured`,前端静默不展示):
| 变量 | 必填 | 说明 |
|------|------|------|
| `AMAP_KEY` | 是 | 高德 **Web服务(REST)** Key |
| `QWEATHER_HOST` | 是 | 和风**专属 API Host**(控制台给的,如 `https://xxxx.qweatherapi.com` |
| `QWEATHER_PROJECT_ID` | 是 | 和风**项目 ID** → JWT 的 `sub` |
| `QWEATHER_KEY_ID` | 是 | 和风**凭据 ID** → JWT 头的 `kid` |
| `QWEATHER_PRIVATE_KEY_PATH` | 二选一 | Ed25519 私钥(PKCS#8 PEM)**文件路径**(推荐,多行 PEM 用文件最省事) |
| `QWEATHER_PRIVATE_KEY` | 二选一 | 直接给私钥 PEM 内容(可用 `\n` 转义换行) |
| `AMAP_DAILY_LIMIT` | 否 | 默认 `300` |
| `QWEATHER_DAILY_LIMIT` | 否 | 默认 `900` |
| `WEATHER_CACHE_SECS` | 否 | 缓存秒数,默认 `600` |
| `WEATHER_DEFAULT_LOCATION` | 否 | 内网/本地 IP 定位失败时的兜底坐标 `经度,纬度`(如 `116.40,39.90` |
| `WEATHER_DEFAULT_CITY` | 否 | 兜底坐标对应的城市名 |
> 和风认证用 **JWT(EdDSA/Ed25519)**:后端用私钥签发 15 分钟有效的 token
> 以 `Authorization: Bearer` 调用。私钥**只放后端**,绝不进前端/仓库。
> 定位失败的处理:高德对**数据中心 / DNS / 部分移动·CGNAT / 境外 IP** 会返回空。
> 若**不设** `WEATHER_DEFAULT_LOCATION`,定位失败时接口直接返回 `failed` →
> **不再调和风**(省额度)→ 前端静默不显示天气。**线上有意不设兜底**,就是这个效果。
>
> 本地开发时访问者 IP 是 `127.0.0.1`,高德无法定位 → 想在本地看到天气,可临时设
> `WEATHER_DEFAULT_LOCATION` + `WEATHER_DEFAULT_CITY`(仅本地 `weather.env`,别带上线)。
## 本地运行
后端(在 `Server/`WSL 或 Windows 均可):
```bash
AMAP_KEY=xxx \
QWEATHER_HOST=https://xxxx.qweatherapi.com \
QWEATHER_PROJECT_ID=项目ID QWEATHER_KEY_ID=凭据ID \
QWEATHER_PRIVATE_KEY_PATH=./qweather-ed25519.pem \
WEATHER_DEFAULT_LOCATION=116.40,39.90 WEATHER_DEFAULT_CITY=北京 \
PORT=8080 cargo run
```
前端 `cd Client && npm run dev`,打开起始页即可看到天气行。
## 部署:改了后端时(重新编译)
改了 `Server/` 下代码时上线需要**重新编译 Rust 二进制**
1. WSL 里 `cd /mnt/f/Code/InternetProject/Server && cargo build --release`
2. scp 覆盖服务器 `/opt/internet-project/RustServer` + 前端 `dist/*`
3. 私钥与密钥建议写进服务器上的一个文件再统一加载,避免出现在命令历史里。
例如服务器放 `/opt/internet-project/qweather.pem``/opt/internet-project/weather.env`
```bash
# weather.env 内容(不进仓库)
AMAP_KEY=xxx
QWEATHER_HOST=https://xxxx.qweatherapi.com
QWEATHER_PROJECT_ID=项目ID
QWEATHER_KEY_ID=凭据ID
QWEATHER_PRIVATE_KEY_PATH=/opt/internet-project/qweather.pem
PORT=8548
```
启动:
```bash
ssh root@8.130.143.54 "pkill RustServer; cd /opt/internet-project && \
set -a && . ./weather.env && set +a && nohup ./RustServer > server.log 2>&1 &"
```
## 天气图标方案
未引入图标库/字体,而是把和风 `now.icon` 码归成 7 类
(晴日/晴夜/多云阴/雨/雷/雪/雾霾),各画一个极简线性 SVG`WeatherIcon.tsx`
`stroke=currentColor` 跟随文字颜色,矢量可缩放、离线可用,和玻璃风统一。
要更精细可换成和风官方图标集,但当前方案零依赖、足够清爽。
## 昼夜背景切换
背景图按昼夜在 `light.*` / `dark.*` 之间自动切换(图片放
`Client/src/assets/startpage/`,详见该目录 README
- **昼夜判断**`StartPage.tsx`):优先用 `now.icon`——`150/151/152/153/350/351/456/457`
判为夜、`100/101/102/103/300/301/400/401` 判为昼;其余无昼夜之分的天气码用
**本地时间兜底**18:0006:00 为夜)。每分钟一拍,跨时段会自动翻。
- **回退**:选中的图缺失 → 另一张 → 目录里排序第一张 → 中性渐变。
## 天气动态效果
`WeatherFx.tsx` 用一个轻量 canvas 粒子层按天气渲染(**克制**风格、低透明度、
尊重系统「减少动态效果」、`kind='none'` 不挂载):
| 天气(`now.icon` | 效果 |
|------|------|
| 雨类 `300399` | 斜向落雨;其中雷阵雨 `302/303/304` 叠**偶发闪电**一闪 |
| 雪类 `400499` 及冷 `901` | 缓慢飘落雪花 |
| 其余 | 无(静态) |
层级:背景图(-z-20) → 渐变 scrim + 半透明黑遮罩(-z-10) → **天气动效(z-0)** → 内容(z-10) → 齿轮(z-30)。
## 本地预览开关(仅 `npm run dev`
不开后端 / 不等真实天气也能预览昼夜背景与动效,加 URL 参数即可:
- `?bg=light` / `?bg=dark` —— 强制白天/夜晚背景
- `?fx=rain` / `?fx=snow` / `?fx=thunder` / `?fx=none` —— 强制动效
例:`http://localhost:5173/?bg=dark&fx=snow`。这些参数 `import.meta.env.DEV` 为真才生效,**线上无效**,线上完全由真实天气驱动。
## 再次部署的两种情况
- **只改前端**(动效、背景、样式等):前端单独重发即可——`npm run build` →
清空服务器 `static/` → 上传 `dist/*`。后端进程不用动(实时读磁盘静态文件)。
- **改了后端**`Server/` 下任何代码):需在 WSL `cargo build --release`
覆盖 `/opt/internet-project/RustServer` → 带环境变量重启(见上「部署:改了后端时」)。