2026-06-18 20:13:18 +08:00
|
|
|
|
# 起始页:天气 + 定位(已实现)
|
|
|
|
|
|
|
|
|
|
|
|
导航起始页问候语下方展示「天气图标 · 城市 · 天气 · 气温」。
|
2026-06-20 20:45:39 +08:00
|
|
|
|
**定位优先用前端高德 `AMap.Geolocation`(能 GPS 就 GPS),失败退回后端高德 IP 定位;
|
|
|
|
|
|
天气始终来自和风实时天气(私钥只在后端)。**
|
2026-06-18 20:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
## 架构
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-06-20 20:45:39 +08:00
|
|
|
|
浏览器
|
|
|
|
|
|
├─ AMap.Geolocation 前端定位(JS API)
|
|
|
|
|
|
│ ├─ 浏览器原生定位(GPS/WiFi,精度高,**需 HTTPS**)→ 坐标 + 城市
|
|
|
|
|
|
│ └─ 失败/被拒 → 高德纯 IP 城市定位(getCityInfo)
|
|
|
|
|
|
│ ↓ 带 lon,lat,city
|
|
|
|
|
|
└─ GET /api/web/weather[?lon=&lat=&city=] (后端)
|
|
|
|
|
|
├─ 带了坐标 → 直接用,**不调高德**(省高德额度)
|
|
|
|
|
|
├─ 没带坐标 → 高德 /v3/ip?ip=<访问者IP> 兜底 → 城市名 + 城市中心坐标
|
2026-06-18 20:13:18 +08:00
|
|
|
|
└─ 和风 /v7/weather/now?location= → 天气文字 / 气温 / 图标码
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-20 20:45:39 +08:00
|
|
|
|
> **为什么前端定位 + 后端兜底两套并存:** 前端原生定位(GPS)能到街道级、还能绕开
|
|
|
|
|
|
> 「数据中心 / CGNAT / 境外 IP 高德返回空」的痛点,但**浏览器原生定位在非 localhost 的
|
|
|
|
|
|
> HTTP 页面下被禁用**。当前线上是裸 HTTP,所以现阶段前端只会走它的 IP 退路(精度等同后端
|
|
|
|
|
|
> IP 定位);**等站点上了 HTTPS,GPS 会自动开始生效,无需改代码**。前端定位彻底失败时,
|
|
|
|
|
|
> 后端再用 REST Key 做一次 IP 定位作双保险。
|
|
|
|
|
|
|
|
|
|
|
|
- 后端:`Server/src/weather.rs`(服务 + 限流 + 缓存,`get()` 接收可选前端坐标)、
|
|
|
|
|
|
路由 `Server/src/routes/web.rs` 的 `/api/web/weather`(解析可选 `lon/lat/city`)。
|
2026-06-18 20:13:18 +08:00
|
|
|
|
- 前端:
|
2026-06-20 20:45:39 +08:00
|
|
|
|
- `Client/src/lib/amap.ts`:加载高德 JSAPI + `AMap.Geolocation`,封装 `locate()`
|
|
|
|
|
|
- `Client/src/api/weather.ts`、`Client/src/hooks/useWeather.ts`(先定位再拉取,30 分钟刷新)
|
2026-06-18 20:13:18 +08:00
|
|
|
|
- `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 点恢复」。
|
2026-06-20 20:45:39 +08:00
|
|
|
|
- **缓存 10 分钟**:前端带坐标时按 `坐标` 聚合缓存,未带时按访问者 `IP` 缓存;
|
|
|
|
|
|
前端每 30 分钟刷新一次,正常用量远低于额度。
|
2026-06-18 20:13:18 +08:00
|
|
|
|
- 计数只在“真的调用了上游”时增加(命中缓存不计数)。
|
2026-06-20 20:45:39 +08:00
|
|
|
|
**前端已定位(带了坐标)时不调高德、也不消耗高德额度**,只消耗和风额度。
|
2026-06-18 20:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
## 环境变量
|
|
|
|
|
|
|
2026-06-20 20:45:39 +08:00
|
|
|
|
### 前端(Vite,`Client/.env.local`,**不进仓库**)
|
|
|
|
|
|
|
|
|
|
|
|
高德 **Web端(JS API)** 密钥,给 `AMap.Geolocation` 前端定位用。与后端 `AMAP_KEY` 是**两套**。
|
|
|
|
|
|
不配则前端不定位、直接由后端 IP 兜底(旧行为,不会报错)。Vite 在 dev 与 build 时都会加载
|
|
|
|
|
|
`.env.local`,故本地构建出的 `dist` 会内置此 Key(JS 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 兜底路径需要,前端总能定位时可不配):
|
2026-06-18 20:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
| 变量 | 必填 | 说明 |
|
|
|
|
|
|
|------|------|------|
|
2026-06-20 20:45:39 +08:00
|
|
|
|
| `AMAP_KEY` | 兜底必填 | 高德 **Web服务(REST)** Key,仅前端定位失败的 IP 兜底路径用 |
|
2026-06-18 20:13:18 +08:00
|
|
|
|
| `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` 调用。私钥**只放后端**,绝不进前端/仓库。
|
|
|
|
|
|
|
2026-06-18 20:35:54 +08:00
|
|
|
|
> 定位失败的处理:高德对**数据中心 / DNS / 部分移动·CGNAT / 境外 IP** 会返回空。
|
|
|
|
|
|
> 若**不设** `WEATHER_DEFAULT_LOCATION`,定位失败时接口直接返回 `failed` →
|
|
|
|
|
|
> **不再调和风**(省额度)→ 前端静默不显示天气。**线上有意不设兜底**,就是这个效果。
|
|
|
|
|
|
>
|
|
|
|
|
|
> 本地开发时访问者 IP 是 `127.0.0.1`,高德无法定位 → 想在本地看到天气,可临时设
|
|
|
|
|
|
> `WEATHER_DEFAULT_LOCATION` + `WEATHER_DEFAULT_CITY`(仅本地 `weather.env`,别带上线)。
|
2026-06-18 20:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
## 本地运行
|
|
|
|
|
|
|
|
|
|
|
|
后端(在 `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 二进制**:
|
2026-06-21 21:23:12 +08:00
|
|
|
|
1. WSL 里 `cd /home/cc/InternetProject/Server && cargo build --release`
|
2026-06-18 20:13:18 +08:00
|
|
|
|
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
|
2026-06-20 20:49:04 +08:00
|
|
|
|
# 登录 / 入口门走 MySQL(见 docs/auth-login-gate.md)
|
|
|
|
|
|
DATABASE_URL=mysql://KarCharon:******@127.0.0.1:3306/InternetProject
|
2026-06-18 20:13:18 +08:00
|
|
|
|
```
|
|
|
|
|
|
启动:
|
|
|
|
|
|
```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:00–06:00 为夜)。每分钟一拍,跨时段会自动翻。
|
|
|
|
|
|
- **回退**:选中的图缺失 → 另一张 → 目录里排序第一张 → 中性渐变。
|
|
|
|
|
|
|
|
|
|
|
|
## 天气动态效果
|
|
|
|
|
|
|
|
|
|
|
|
`WeatherFx.tsx` 用一个轻量 canvas 粒子层按天气渲染(**克制**风格、低透明度、
|
|
|
|
|
|
尊重系统「减少动态效果」、`kind='none'` 不挂载):
|
|
|
|
|
|
|
|
|
|
|
|
| 天气(`now.icon`) | 效果 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| 雨类 `300–399` | 斜向落雨;其中雷阵雨 `302/303/304` 叠**偶发闪电**一闪 |
|
|
|
|
|
|
| 雪类 `400–499` 及冷 `901` | 缓慢飘落雪花 |
|
|
|
|
|
|
| 其余 | 无(静态) |
|
|
|
|
|
|
|
2026-06-20 20:49:04 +08:00
|
|
|
|
层级:背景图(-z-20) → 渐变 scrim + 半透明黑遮罩(-z-10) → **天气动效(z-0)** → 内容(z-10)。
|
2026-06-18 20:13:18 +08:00
|
|
|
|
|
|
|
|
|
|
## 本地预览开关(仅 `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` → 带环境变量重启(见上「部署:改了后端时」)。
|
2026-06-21 21:28:41 +08:00
|
|
|
|
|
|
|
|
|
|
## Git 远程与推送
|
|
|
|
|
|
|
|
|
|
|
|
仓库有两个远程,代码改完上线后两边都要同步:
|
|
|
|
|
|
|
|
|
|
|
|
| 远程 | 地址 | 协议 / 认证 | 在 WSL 里能否直接推 |
|
|
|
|
|
|
|------|------|-------------|---------------------|
|
|
|
|
|
|
| `gitea` | `ssh://gitea@8.130.143.54:2222/cc/PersonalWebApplication.git` | SSH(已配密钥) | ✅ 可直接 `git push gitea main` |
|
|
|
|
|
|
| `origin` | `https://github.com/KarCharon/MyOwnWebPage.git` | HTTPS(需账号 / Token) | ⚠️ 非交互环境推不了,要手动 |
|
|
|
|
|
|
|
|
|
|
|
|
- **gitea**:WSL 里 SSH 密钥已就绪,`git push gitea main` 直接成功。
|
|
|
|
|
|
- **GitHub(origin)**:HTTPS 地址在自动化 / 非交互环境里没有凭据会报
|
|
|
|
|
|
`could not read Username for 'https://github.com'`。手动推送时,密码要填
|
|
|
|
|
|
**Personal Access Token**(不是登录密码)。若想长期免输入,可改用 SSH(前提是
|
|
|
|
|
|
GitHub 配过本机公钥):
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
git remote set-url origin git@github.com:KarCharon/MyOwnWebPage.git
|
|
|
|
|
|
git push origin main
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
> 行尾:仓库历史里部分文件曾从 Windows 带入 CRLF。在 WSL(LF)下编辑会让这些文件
|
|
|
|
|
|
> 整片显示为「已改动」,其实只是行尾差异(`git diff --ignore-all-space` 为空)。
|
|
|
|
|
|
> 提交前注意别把这类行尾噪声混进功能提交;要彻底消除可加 `.gitattributes`(`* text=auto eol=lf`)。
|