update at 2026-02-10 13:47:16

This commit is contained in:
douboer
2026-02-10 13:47:16 +08:00
parent 917f210dae
commit b6742cb13a
5 changed files with 412 additions and 3 deletions

124
PLAN.md
View File

@@ -1,5 +1,128 @@
# 项目计划2026-02-08 # 项目计划2026-02-08
## 0. 手动切换路由方案2026-02-10
### 0.1 目标
- 支持在**不发布小程序新版本**的情况下,从服务器 A`fonts.biboer.cn`)切换到服务器 B`mac.biboer.cn`),或反向切换。
- 切换仅通过手动修改远端配置文件触发,不引入自动健康探测。
- 避免 A/B 来回抖动切换。
### 0.2 已确认约束
- 字体目录统一一份:`/fonts/`A、B 各自托管同结构字体文件)。
- 配置文件独立管理:
- Web`/fonts.json`(可选 `/default.json`
- 小程序:`/miniprogram/assets/fonts.json``/miniprogram/assets/default.json`
- 小程序合法域名已包含 A/B 两个域名。
- 不使用 `version` 字段。
### 0.3 路由配置文件route-config.json
- 建议路径:`/miniprogram/assets/route-config.json`A、B 都提供)
- 建议结构:
```json
{
"active": "A",
"cooldownMinutes": 10,
"servers": {
"A": { "baseUrl": "https://fonts.biboer.cn" },
"B": { "baseUrl": "https://mac.biboer.cn" }
}
}
```
字段说明:
- `active`: 当前希望启用的目标服务器(`A``B`)。
- `cooldownMinutes`: 最短驻留时间。
- `10` 表示 10 分钟内不允许再次切换。
- `0` 表示可立即切换。
### 0.4 双确认切换规则(核心)
当客户端当前连接在 A且读取到 A 配置 `active=B` 时:
1. 客户端继续请求 B 的 `route-config.json`
2. 只有 B 也返回 `active=B`,才允许切换到 B。
3. 若 A/B 不一致,则保持当前服务器不变。
4. 若读取 B 失败(超时、非 200、JSON 非法),保持当前服务器不变,并记录失败日志。
反向切换B -> A同理执行。
### 0.5 A/B 交互与同步机制
为满足“双确认”A/B 需要配置一致性机制(交互):
- 维护端本地保留单一配置源文件Git 仓库内)。
- 通过 `deploy.sh` 一次性下发到 A、B。
- 采用“临时文件 + 原子替换mv”发布避免客户端读取半文件。
推荐切换步骤A -> B
1. 同步配置到 B确保 B 返回 `active=B`
2. 再同步配置到 A改为 `active=B`
3. 客户端双确认通过后完成切换。
### 0.6 客户端状态与防抖
本地持久化字段:
- `activeServerKey`(当前服务器)
- `lastSwitchAt`(最后切换时间戳)
- `routeConfigCache`(最近一次配置)
- `lastRouteCheckAt`(最后一次读取 route-config 时间戳)
切换判定:
-`cooldownMinutes > 0``now - lastSwitchAt < cooldownMinutes * 60 * 1000`,拒绝切换。
-`cooldownMinutes = 0`,通过双确认即可立即切换。
### 0.6.1 route-config.json 读取时机
1. 启动读取P0
- 小程序冷启动时优先读取 `route-config.json`,再加载 `fonts.json/default.json` 与 API 请求。
2. 回前台读取P0
- `App.onShow` 触发时检查是否超过最小间隔(例如 60 秒),超过则读取。
3. 失败兜底读取P0
- 当 API 或配置拉取连续失败时,立即触发一次读取并执行双确认逻辑。
- 若目标服务器配置读取失败,则仅记录日志并维持当前服务器,不执行切换。
4. 手动刷新读取P1可选
- 提供调试入口(如“刷新配置”按钮)用于即时验证切换。
节流规则:
-`now - lastRouteCheckAt < 60s`,跳过非必要读取,避免频繁请求。
### 0.7 实施任务拆分
1. 小程序端
- 新增 `route-manager`(加载路由配置、执行双确认、应用 cooldown、持久化状态
- `render-api``font-loader` 改为读取 `route-manager` 当前 `baseUrl`
- 启动时先加载路由,再加载 `fonts.json/default.json`
2. 服务端与部署
- A/B 都部署 `route-config.json`
- 使用 `deploy.sh` 统一发布:一次性下发到两台服务器并做原子替换。
- `deploy.sh` 负责同步 `route-config.json`、字体配置文件,并执行可选巡检。
- 增加巡检命令:检查 A/B 当前 `active` 是否一致。
3. 文档
- 更新 `miniprogram/README.md`:增加无发版切换流程。
- 增加运维操作手册A->B、B->A、回滚流程。
### 0.8 验收标准
- 仅修改远端 `route-config.json`,小程序无需发版即可切换 A/B。
- `cooldownMinutes=10`10 分钟内不会再次切换。
- `cooldownMinutes=0` 时,双确认满足后可立即切换。
- A/B 配置不一致时不发生切换。
- 目标服务器配置读取失败(超时/非 200/JSON 非法)时不发生切换。
- 切换后 API、字体清单、默认配置均来自目标服务器。
### 0.9 风险与回滚
- 风险A/B 配置不同步导致无法切换(预期保护行为)。
- 风险CDN 缓存导致短时间读取旧配置(通过短缓存 + 原子发布缓解)。
- 回滚:将 A/B 两端 `active` 同步改回原服务器,并设置 `cooldownMinutes=0` 可快速恢复。
## 1. 当前状态 ## 1. 当前状态
### 1.1 已完成(保留能力) ### 1.1 已完成(保留能力)
@@ -1113,4 +1236,3 @@ wx.downloadFile({ url: fontPath })
**计划更新日期**2026-02-08 **计划更新日期**2026-02-08
**下次更新**M1 完成后,根据实际情况调整后续里程碑细节 **下次更新**M1 完成后,根据实际情况调整后续里程碑细节
**部署方案**Cloudflare CDN + 海外服务器fonts.biboer.cn免备案 **部署方案**Cloudflare CDN + 海外服务器fonts.biboer.cn免备案

View File

@@ -87,7 +87,7 @@ location /api/ {
sudo nginx -t && sudo systemctl reload nginx sudo nginx -t && sudo systemctl reload nginx
``` ```
## 4. systemd可选 ## 4. systemdLinux可选)
仓库内提供示例:`apiserver/font2svg-api.service.example`,可复制到: 仓库内提供示例:`apiserver/font2svg-api.service.example`,可复制到:
@@ -98,7 +98,38 @@ sudo systemctl enable --now font2svg-api
sudo systemctl status font2svg-api sudo systemctl status font2svg-api
``` ```
## 5. 约束 ## 5. launchdmacOS可选
仓库内提供示例:`apiserver/font2svg-api.launchd.plist.example`
1. 复制并按本机路径修改(重点改 Python 路径和项目路径):
```bash
cp apiserver/font2svg-api.launchd.plist.example ~/Library/LaunchAgents/cn.biboer.font2svg-api.plist
```
2. 加载并启动:
```bash
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/cn.biboer.font2svg-api.plist
launchctl enable gui/$(id -u)/cn.biboer.font2svg-api
launchctl kickstart -k gui/$(id -u)/cn.biboer.font2svg-api
```
3. 查看状态与日志:
```bash
launchctl print gui/$(id -u)/cn.biboer.font2svg-api
tail -f /tmp/font2svg-api.log
```
4. 停止并卸载:
```bash
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/cn.biboer.font2svg-api.plist
```
## 6. 约束
- 字体解析完全基于字体清单(默认 `miniprogram/assets/fonts.json`),字体文件统一从 `<static-root>/fonts/` 读取,`fontId` 必须存在。 - 字体解析完全基于字体清单(默认 `miniprogram/assets/fonts.json`),字体文件统一从 `<static-root>/fonts/` 读取,`fontId` 必须存在。
- 服务端启用 CORS允许小程序访问。 - 服务端启用 CORS允许小程序访问。

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>cn.biboer.font2svg-api</string>
<key>ProgramArguments</key>
<array>
<string>/Users/gavin/font2svg/.venv/bin/python</string>
<string>/Users/gavin/font2svg/apiserver/server.py</string>
<string>--host</string>
<string>127.0.0.1</string>
<string>--port</string>
<string>9300</string>
<string>--static-root</string>
<string>/Users/gavin/font2svg</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/gavin/font2svg</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/font2svg-api.log</string>
<key>StandardErrorPath</key>
<string>/tmp/font2svg-api.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>PYTHONUNBUFFERED</key>
<string>1</string>
</dict>
</dict>
</plist>

86
deploy.sh Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
# deploy.sh
#
# 用途
# - 一键执行本地推送 + 远端两台服务器拉取代码A/B
# - 默认部署分支main。
#
# 执行流程
# 1) 运行本地推送脚本(默认 ./gitrun.sh
# 2) 服务器 A 执行 git fetch + checkout + pull --ff-only
# 3) 服务器 B 执行 git fetch + checkout + pull --ff-only
#
# 前置条件
# - 本机已配置 SSH 免密登录到 A/B 两台服务器。
# - 远端目录已存在 Git 仓库(默认 ~/font2svg
# - 本地 GITRUN_SCRIPT 可执行。
#
# 可覆盖环境变量
# - GITRUN_SCRIPT 本地推送脚本路径(默认 ./gitrun.sh
# - DEPLOY_BRANCH 部署分支(默认 main
# - REMOTE_WORKDIR 远端项目目录(默认 ~/font2svg
# - SERVER_A 服务器 A默认 gavin@mac.biboer.cn
# - SERVER_A_PORT 服务器 A SSH 端口(默认 22
# - SERVER_B 服务器 B默认 gavin@biboer.cn
# - SERVER_B_PORT 服务器 B SSH 端口(默认 21174
#
# 示例
# - 使用默认配置:
# bash deploy.sh
# - 指定分支:
# DEPLOY_BRANCH=release bash deploy.sh
# - 指定远端目录:
# REMOTE_WORKDIR=~/apps/font2svg bash deploy.sh
#
# 失败策略
# - set -euo pipefail任一步失败立即退出避免部分成功导致状态不一致。
# -----------------------------------------------------------------------------
set -euo pipefail
# 部署配置(可通过环境变量覆盖)
GITRUN_SCRIPT="${GITRUN_SCRIPT:-./gitrun.sh}"
DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}"
REMOTE_WORKDIR="${REMOTE_WORKDIR:-~/font2svg}"
SERVER_A="${SERVER_A:-gavin@mac.biboer.cn}"
SERVER_A_PORT="${SERVER_A_PORT:-22}"
SERVER_B="${SERVER_B:-gavin@biboer.cn}"
SERVER_B_PORT="${SERVER_B_PORT:-21174}"
SSH_OPTS=(
-o BatchMode=yes
-o ConnectTimeout=8
-o StrictHostKeyChecking=accept-new
)
log_info() {
echo "[INFO] $1"
}
run_local_push() {
if [[ ! -x "$GITRUN_SCRIPT" ]]; then
echo "[ERROR] git 推送脚本不可执行: $GITRUN_SCRIPT"
echo "请先执行: chmod +x $GITRUN_SCRIPT"
exit 1
fi
log_info "执行本地推送脚本: $GITRUN_SCRIPT"
"$GITRUN_SCRIPT"
}
run_remote_pull() {
local host="$1"
local port="$2"
log_info "远程拉取: $host:$port ($DEPLOY_BRANCH)"
ssh "${SSH_OPTS[@]}" -p "$port" "$host" \
"cd $REMOTE_WORKDIR && git fetch origin && git checkout $DEPLOY_BRANCH && git pull --ff-only origin $DEPLOY_BRANCH"
}
main() {
run_local_push
run_remote_pull "$SERVER_A" "$SERVER_A_PORT"
run_remote_pull "$SERVER_B" "$SERVER_B_PORT"
log_info "部署完成"
}
main "$@"

129
mac.conf Normal file
View File

@@ -0,0 +1,129 @@
# Font2SVG - Nginx 配置mac.biboer.cn
# 用途:为微信小程序提供静态字体资源 + 远端 SVG 渲染 API
server {
listen 80;
listen [::]:80;
server_name mac.biboer.cn;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name mac.biboer.cn;
# SSL 证书
ssl_certificate /Users/gavin/mac.biboer.cn_ecc/fullchain.cer;
ssl_certificate_key /Users/gavin/mac.biboer.cn_ecc/mac.biboer.cn.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 静态资源根目录(包含 fonts/、fonts.json、miniprogram/assets/*
root /Users/gavin/font2svg;
index fonts.json;
access_log /opt/homebrew/var/log/nginx/access.log;
error_log /opt/homebrew/var/log/nginx/error.log;
server_tokens off;
# 小程序跨域访问
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET,HEAD,POST,OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin,Range,Accept,Content-Type,Authorization" always;
add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always;
# MIME
types {
application/json json;
font/ttf ttf;
font/otf otf;
font/woff woff;
font/woff2 woff2;
application/vnd.ms-fontobject eot;
}
# SVG 渲染 API独立 Python 服务systemd 监听 127.0.0.1:9300
location ^~ /api/ {
# 预检请求:直接返回 204CORS 头由 server 级 add_header 提供)
if ($request_method = OPTIONS) {
return 204;
}
proxy_pass http://127.0.0.1:9300;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 健康检查(可选)
location = /healthz {
proxy_pass http://127.0.0.1:9300/healthz;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# fonts.json短缓存便于更新
location = /fonts.json {
expires 1h;
add_header Cache-Control "public, must-revalidate" always;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET,HEAD,POST,OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin,Range,Accept,Content-Type,Authorization" always;
add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always;
try_files $uri =404;
}
# 小程序配置:短缓存,便于切换
location = /miniprogram/assets/fonts.json {
expires 1h;
add_header Cache-Control "public, must-revalidate" always;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET,HEAD,POST,OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin,Range,Accept,Content-Type,Authorization" always;
add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always;
try_files $uri =404;
}
location = /miniprogram/assets/default.json {
expires 1h;
add_header Cache-Control "public, must-revalidate" always;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET,HEAD,POST,OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin,Range,Accept,Content-Type,Authorization" always;
add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always;
try_files $uri =404;
}
# 字体文件:长缓存
location ~* \.(ttf|otf|woff|woff2|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable" always;
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET,HEAD,POST,OPTIONS" always;
add_header Access-Control-Allow-Headers "Origin,Range,Accept,Content-Type,Authorization" always;
add_header Access-Control-Expose-Headers "Content-Length,Content-Range" always;
try_files $uri =404;
}
# 默认仅提供静态文件
location / {
try_files $uri =404;
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
}