PersonalWebApplication/docs/startpage-weather-location.md
cc d7d520058c docs: 补登录/入口门走 MySQL 的说明,修起始页过期引用
- 新增 docs/auth-login-gate.md:入口门 + 登录的 MySQL 后端、表结构、
  建库步骤、DATABASE_URL、无库兜底、安全待办
- startpage 文档:层级图删掉已移除的「齿轮」;weather.env 示例补 DATABASE_URL

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

172 lines
9.2 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.

# 起始页:天气 + 定位(已实现)
导航起始页问候语下方展示「天气图标 · 城市 · 天气 · 气温」。
**定位优先用前端高德 `AMap.Geolocation`(能 GPS 就 GPS失败退回后端高德 IP 定位;
天气始终来自和风实时天气(私钥只在后端)。**
## 架构
```
浏览器
├─ AMap.Geolocation 前端定位JS API
│ ├─ 浏览器原生定位GPS/WiFi精度高**需 HTTPS**)→ 坐标 + 城市
│ └─ 失败/被拒 → 高德纯 IP 城市定位getCityInfo
│ ↓ 带 lon,lat,city
└─ GET /api/web/weather[?lon=&lat=&city=] (后端)
├─ 带了坐标 → 直接用,**不调高德**(省高德额度)
├─ 没带坐标 → 高德 /v3/ip?ip=<访问者IP> 兜底 → 城市名 + 城市中心坐标
└─ 和风 /v7/weather/now?location= → 天气文字 / 气温 / 图标码
```
> **为什么前端定位 + 后端兜底两套并存:** 前端原生定位GPS能到街道级、还能绕开
> 「数据中心 / CGNAT / 境外 IP 高德返回空」的痛点,但**浏览器原生定位在非 localhost 的
> HTTP 页面下被禁用**。当前线上是裸 HTTP所以现阶段前端只会走它的 IP 退路(精度等同后端
> IP 定位);**等站点上了 HTTPSGPS 会自动开始生效,无需改代码**。前端定位彻底失败时,
> 后端再用 REST Key 做一次 IP 定位作双保险。
- 后端:`Server/src/weather.rs`(服务 + 限流 + 缓存,`get()` 接收可选前端坐标)、
路由 `Server/src/routes/web.rs``/api/web/weather`(解析可选 `lon/lat/city`)。
- 前端:
- `Client/src/lib/amap.ts`:加载高德 JSAPI + `AMap.Geolocation`,封装 `locate()`
- `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 点恢复」。
- **缓存 10 分钟**:前端带坐标时按 `坐标` 聚合缓存,未带时按访问者 `IP` 缓存;
前端每 30 分钟刷新一次,正常用量远低于额度。
- 计数只在“真的调用了上游”时增加(命中缓存不计数)。
**前端已定位(带了坐标)时不调高德、也不消耗高德额度**,只消耗和风额度。
## 环境变量
### 前端Vite`Client/.env.local`**不进仓库**
高德 **Web端(JS API)** 密钥,给 `AMap.Geolocation` 前端定位用。与后端 `AMAP_KEY` 是**两套**。
不配则前端不定位、直接由后端 IP 兜底旧行为不会报错。Vite 在 dev 与 build 时都会加载
`.env.local`,故本地构建出的 `dist` 会内置此 KeyJS Key 本就暴露在前端,靠高德控制台
**「安全域名」白名单**约束,不是机密)。
| 变量 | 必填 | 说明 |
|------|------|------|
| `VITE_AMAP_JS_KEY` | 否 | 高德 **Web端(JS API)** Key |
| `VITE_AMAP_JS_CODE` | 否 | 高德 **安全密钥(securityJsCode)**JSAPI 2.0 起与 Key 成对必需,缺它定位报 `INVALID_USER_SCODE` |
### 后端
后端启动时读取(缺和风 JWT 时接口返回 `not_configured`,前端静默不展示;
`AMAP_KEY` 仅 IP 兜底路径需要,前端总能定位时可不配):
| 变量 | 必填 | 说明 |
|------|------|------|
| `AMAP_KEY` | 兜底必填 | 高德 **Web服务(REST)** Key仅前端定位失败的 IP 兜底路径用 |
| `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
# 登录 / 入口门走 MySQL见 docs/auth-login-gate.md
DATABASE_URL=mysql://KarCharon:******@127.0.0.1:3306/InternetProject
```
启动:
```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)。
## 本地预览开关(仅 `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` → 带环境变量重启(见上「部署:改了后端时」)。