Initial commit: WordLoop 单词学习应用

Vue 前端 + FastAPI 后端,含部署脚本与词典数据。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-04 14:30:53 -07:00
commit bd7635986a
66 changed files with 5495 additions and 0 deletions
+124
View File
@@ -0,0 +1,124 @@
# WordLoop 部署到 Cloudflare · w.tkmind.cn
本指南把 **WordLoop** 通过 **Cloudflare** 暴露为 `https://w.tkmind.cn`。应用为 Vue 前端 + FastAPI 后端 + MySQL,推荐 **Cloudflare Tunnel**(无需公网 IP、自动 HTTPS)。
---
## 一、注册 Cloudflare 并接入域名
1. 打开 [https://dash.cloudflare.com/sign-up](https://dash.cloudflare.com/sign-up) 注册账号(免费计划即可)。
2. **添加站点** → 输入根域名 **`tkmind.cn`**(子域 `w` 在根域下配置即可)。
3. Cloudflare 会给出两条 **NS 记录**,到 **购买/解析 tkmind.cn 的注册商**(阿里云、腾讯云、GoDaddy 等)把域名的 DNS 服务器改为 Cloudflare 提供的 NS。
4. 等待状态变为 **Active**(通常几分钟到 48 小时)。
> 若 `tkmind.cn` 已在 Cloudflare,跳过 2–3,直接进入第二节。
---
## 二、两种接入方式(二选一)
### 方式 ACloudflare Tunnel(推荐)
适合:家庭宽带、无固定公网 IP、或不想开放 80/443 端口。
| 步骤 | 操作 |
|------|------|
| 1 | 在**运行 WordLoop 的机器**安装 [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) |
| 2 | `cloudflared tunnel login`(浏览器授权) |
| 3 | `cloudflared tunnel create wordloop` |
| 4 | 复制 `deploy/cloudflared/config.yml.example``config.yml`,填入 `credentials-file` 实际路径 |
| 5 | `cloudflared tunnel route dns wordloop w.tkmind.cn` |
| 6 | 本机 Nginx 按 `deploy/nginx.conf` 监听 `127.0.0.1:8080`Tunnel 指向该地址 |
| 7 | `cloudflared tunnel --config deploy/cloudflared/config.yml run` 测试;稳定后用 `deploy/systemd/cloudflared-wordloop.service` |
**DNS 结果**`w.tkmind.cn` → CNAME → `xxxx.cfargotunnel.com`(由 `tunnel route dns` 自动创建)。
### 方式 B:DNS 代理到自有服务器
适合:已有云服务器与公网 IP。
在 Cloudflare **DNS****记录** 添加:
| 类型 | 名称 | 内容 | 代理 |
|------|------|------|------|
| `A` | `w` | 服务器公网 IP | 已代理(橙色云) |
服务器上:Nginx`deploy/nginx.conf`+ 后端 systemd`deploy/systemd/wordloop-backend.service`),**SSL 由 Cloudflare 边缘终止**(源站可只开 80 或由 Tunnel/内网访问)。
**SSL/TLS** 建议:**完全(严格)** 需在源站配置证书;起步可用 **灵活**(仅访客到 Cloudflare 为 HTTPS)。
---
## 三、服务器部署清单
```bash
# 1. 同步代码到例如 /var/www/wordloop
# 2. 后端
cd /var/www/wordloop/backend
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # 生产务必修改 MYSQL_* 与 WORDLOOP_SECRET_KEY
# 3. MySQL 建库(与 README 一致)
mysql -h ... -u ... -p -e "CREATE DATABASE IF NOT EXISTS wordloop ..."
# 4. 构建前端
cd /var/www/wordloop && chmod +x deploy/build.sh && ./deploy/build.sh
# 5. Nginx
sudo cp deploy/nginx.conf /etc/nginx/sites-available/wordloop
# 修改 root 路径为实际 dist 目录
sudo ln -sf /etc/nginx/sites-available/wordloop /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# 6. 后端服务
sudo cp deploy/systemd/wordloop-backend.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now wordloop-backend
```
---
## 四、生产环境变量
`backend/.env` 示例:
```env
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=wordloop
MYSQL_PASSWORD=<强密码>
MYSQL_DATABASE=wordloop
WORDLOOP_SECRET_KEY=<随机长字符串>
```
前端生产构建使用相对路径 `/api`,与 `deploy/nginx.conf` 反代一致,**无需改前端代码**。
---
## 五、验证
1. `curl -I https://w.tkmind.cn` 应返回 200。
2. 打开 `https://w.tkmind.cn` 能注册/登录。
3. API`https://w.tkmind.cn/api/`(或通过页面功能间接验证)。
4. 可选:`https://w.tkmind.cn/docs`FastAPI 文档,生产可关)。
---
## 六、常见问题
| 现象 | 处理 |
|------|------|
| DNS 未生效 | 确认 NS 已指向 Cloudflare`dig w.tkmind.cn` |
| 522 / 连接失败 | Tunnel 未运行或 Nginx/后端未监听 8080 / 18004 |
| API 401 / CORS | 同源访问应走 `/api`;勿把 API 指到另一域名 |
| 仅子域在 CF | 也可只把 `w` 用 CNAME 到 Tunnel,根域 NS 可仍在原注册商(需支持 CNAME 到 cfargotunnel.com |
---
## 本仓库相关文件
- `deploy/nginx.conf` — 静态站 + `/api` 反代
- `deploy/cloudflared/config.yml.example` — Tunnel 配置模板
- `deploy/systemd/*.service` — 后端与 Tunnel 开机自启
- `deploy/build.sh` — 前端生产构建
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# 在项目根目录执行:./deploy/build.sh
set -e
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
echo ">>> 构建前端..."
cd "$ROOT/frontend"
npm ci
npm run build
echo ">>> 完成。静态文件在 frontend/dist/"
echo " 生产部署请将 dist 同步到服务器,例如:"
echo " rsync -avz frontend/dist/ user@server:/var/www/wordloop/frontend/dist/"
+15
View File
@@ -0,0 +1,15 @@
# Cloudflare Tunnel 示例配置
# 1. 复制为 config.ymlcp config.yml.example config.yml
# 2. 运行 cloudflared tunnel login 后创建隧道:
# cloudflared tunnel create wordloop
# 3. 将 credentials-file 路径改为实际 JSON(通常在 ~/.cloudflared/<tunnel-id>.json
# 4. 在 Cloudflare 控制台为 w.tkmind.cn 添加 Public Hostname,或执行:
# cloudflared tunnel route dns wordloop w.tkmind.cn
tunnel: wordloop
credentials-file: /path/to/.cloudflared/<TUNNEL-UUID>.json
ingress:
- hostname: w.tkmind.cn
service: http://127.0.0.1:8080
- service: http_status:404
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# 在 105 服务器上执行(/root/wordloop
set -euo pipefail
ROOT="${WORDLOOP_ROOT:-/root/wordloop}"
cd "$ROOT"
MYSQL_HOST="${MYSQL_HOST:?MYSQL_HOST required}"
MYSQL_PORT="${MYSQL_PORT:-3306}"
MYSQL_USER="${MYSQL_USER:?MYSQL_USER required}"
MYSQL_PASSWORD="${MYSQL_PASSWORD:?MYSQL_PASSWORD required}"
MYSQL_DATABASE="${MYSQL_DATABASE:-wordloop}"
WORDLOOP_SECRET_KEY="${WORDLOOP_SECRET_KEY:-$(openssl rand -hex 32)}"
echo ">>> 写入 backend/.env"
cat > "$ROOT/backend/.env" <<EOF
MYSQL_HOST=${MYSQL_HOST}
MYSQL_PORT=${MYSQL_PORT}
MYSQL_USER=${MYSQL_USER}
MYSQL_PASSWORD=${MYSQL_PASSWORD}
MYSQL_DATABASE=${MYSQL_DATABASE}
WORDLOOP_SECRET_KEY=${WORDLOOP_SECRET_KEY}
EOF
chmod 600 "$ROOT/backend/.env"
echo ">>> Python 虚拟环境与依赖"
cd "$ROOT/backend"
if [ ! -d venv ]; then
python3 -m venv venv
fi
source venv/bin/activate
pip install -q -r requirements.txt
echo ">>> 创建数据库(若不存在)并建表"
python - <<'PY'
import pymysql
from config import get_settings
s = get_settings()
conn = pymysql.connect(
host=s.mysql_host,
port=s.mysql_port,
user=s.mysql_user,
password=s.mysql_password,
charset="utf8mb4",
)
try:
with conn.cursor() as cur:
cur.execute(
f"CREATE DATABASE IF NOT EXISTS `{s.mysql_database}` "
"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
)
conn.commit()
print(f"database `{s.mysql_database}` ready")
finally:
conn.close()
from database import engine, Base
from sqlalchemy import text
import models # noqa: F401
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
Base.metadata.create_all(bind=engine)
print("tables ready")
PY
echo ">>> 构建前端"
cd "$ROOT/frontend"
if ! command -v npm >/dev/null 2>&1; then
echo "请先安装 Node.js/npm"
exit 1
fi
npm ci --silent
npm run build
echo ">>> Nginx 可读静态目录"
chmod 755 /root /root/wordloop /root/wordloop/frontend /root/wordloop/frontend/dist 2>/dev/null || true
echo ">>> 配置 Nginx + HTTPS (w.tkmind.cn)"
if command -v nginx >/dev/null 2>&1; then
cp "$ROOT/deploy/wordloop-locations.inc" /etc/nginx/conf.d/wordloop-locations.inc
chmod +x "$ROOT/deploy/ssl-w.tkmind.cn.sh"
bash "$ROOT/deploy/ssl-w.tkmind.cn.sh"
systemctl enable nginx 2>/dev/null || true
else
echo "警告: 未安装 nginx,请手动安装后执行 deploy/ssl-w.tkmind.cn.sh"
fi
echo ">>> 配置 systemd 后端服务"
cp "$ROOT/deploy/systemd/wordloop-backend-root.service" /etc/systemd/system/wordloop-backend.service
systemctl daemon-reload
systemctl enable wordloop-backend
systemctl restart wordloop-backend
echo ">>> 完成"
systemctl --no-pager status wordloop-backend || true
curl -sf http://127.0.0.1:18004/ >/dev/null && echo "API: ok" || echo "API: 请检查 journalctl -u wordloop-backend"
+28
View File
@@ -0,0 +1,28 @@
# WordLoop 生产 — w.tkmind.cn80 + 443
server {
listen 80;
server_name w.tkmind.cn;
root /root/wordloop/frontend/dist;
index index.html;
client_max_body_size 10m;
include /etc/nginx/conf.d/wordloop-locations.inc;
}
server {
listen 443 ssl http2;
server_name w.tkmind.cn;
ssl_certificate /etc/letsencrypt/live/w.tkmind.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/w.tkmind.cn/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /root/wordloop/frontend/dist;
index index.html;
client_max_body_size 10m;
include /etc/nginx/conf.d/wordloop-locations.inc;
}
+37
View File
@@ -0,0 +1,37 @@
# WordLoop 生产环境 Nginx(监听本机 8080,由 cloudflared 或上游反代转发)
# 安装:sudo cp deploy/nginx.conf /etc/nginx/sites-available/wordloop
# sudo ln -sf /etc/nginx/sites-available/wordloop /etc/nginx/sites-enabled/
# sudo nginx -t && sudo systemctl reload nginx
server {
listen 127.0.0.1:8080;
server_name w.tkmind.cn;
root /var/www/wordloop/frontend/dist;
index index.html;
client_max_body_size 10m;
location /api/ {
proxy_pass http://127.0.0.1:18004;
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;
}
location /docs {
proxy_pass http://127.0.0.1:18004;
proxy_set_header Host $host;
}
location /openapi.json {
proxy_pass http://127.0.0.1:18004;
proxy_set_header Host $host;
}
location / {
try_files $uri $uri/ /index.html;
}
}
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# 从本机发布到 105 服务器 /root/wordloop
# 用法: ./deploy/publish.sh
# ./deploy/publish.sh root@120.26.184.105
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SSH_TARGET="${1:-${SSH_TARGET:-root@120.26.184.105}}"
# 可选:deploy/secrets.env 覆盖数据库等变量
if [ -f "$ROOT/deploy/secrets.env" ]; then
# shellcheck source=/dev/null
source "$ROOT/deploy/secrets.env"
fi
MYSQL_HOST="${MYSQL_HOST:-rm-uf6h1j53vtuxi78i90o.mysql.rds.aliyuncs.com}"
MYSQL_PORT="${MYSQL_PORT:-3306}"
MYSQL_USER="${MYSQL_USER:-boot}"
# %40 表示 @
MYSQL_PASSWORD="${MYSQL_PASSWORD:-@Abc888888}"
MYSQL_DATABASE="${MYSQL_DATABASE:-wordloop}"
echo ">>> 同步代码到 ${SSH_TARGET}:/root/wordloop/"
ssh -o ConnectTimeout=15 "$SSH_TARGET" "mkdir -p /root/wordloop"
rsync -avz --delete \
--exclude 'backend/venv' \
--exclude 'backend/__pycache__' \
--exclude 'backend/.env' \
--exclude 'backend/wordloop.db' \
--exclude 'backend/data/cache' \
--exclude 'frontend/node_modules' \
--exclude '.git' \
"$ROOT/" "${SSH_TARGET}:/root/wordloop/"
echo ">>> 远程安装与启动"
ssh "$SSH_TARGET" \
"MYSQL_HOST='${MYSQL_HOST}' \
MYSQL_PORT='${MYSQL_PORT}' \
MYSQL_USER='${MYSQL_USER}' \
MYSQL_PASSWORD='${MYSQL_PASSWORD}' \
MYSQL_DATABASE='${MYSQL_DATABASE}' \
bash /root/wordloop/deploy/install-production.sh"
echo ">>> 发布完成: http://120.26.184.105/ Cloudflare 请将 w 解析到该 IP"
+7
View File
@@ -0,0 +1,7 @@
# 复制为 secrets.env(已 gitignore),publish.sh 会自动加载
MYSQL_HOST=rm-uf6h1j53vtuxi78i90o.mysql.rds.aliyuncs.com
MYSQL_PORT=3306
MYSQL_USER=boot
MYSQL_PASSWORD=@Abc888888
MYSQL_DATABASE=wordloop
# SSH_TARGET=root@120.26.184.105
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# 交互式辅助:创建 Cloudflare Tunnel 并绑定 w.tkmind.cn
# 需已安装 cloudflared 且可访问 Cloudflare 账号
set -e
HOSTNAME="${WORDLOOP_HOSTNAME:-w.tkmind.cn}"
TUNNEL_NAME="${WORDLOOP_TUNNEL_NAME:-wordloop}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CF_DIR="$ROOT/deploy/cloudflared"
if ! command -v cloudflared >/dev/null 2>&1; then
echo "请先安装 cloudflared: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/"
exit 1
fi
mkdir -p "$CF_DIR"
echo ">>> 1/4 登录 Cloudflare(将打开浏览器)..."
cloudflared tunnel login
echo ">>> 2/4 创建隧道: $TUNNEL_NAME"
cloudflared tunnel create "$TUNNEL_NAME" || true
CRED=$(ls -1 "$HOME/.cloudflared"/*.json 2>/dev/null | head -1)
if [ -z "$CRED" ]; then
echo "未找到 credentials JSON,请检查 ~/.cloudflared/"
exit 1
fi
CONFIG="$CF_DIR/config.yml"
if [ ! -f "$CONFIG" ]; then
sed -e "s|/path/to/.cloudflared/<TUNNEL-UUID>.json|$CRED|" \
-e "s|hostname: w.tkmind.cn|hostname: $HOSTNAME|" \
"$CF_DIR/config.yml.example" > "$CONFIG"
echo "已生成 $CONFIG"
else
echo "保留已有 $CONFIG"
fi
echo ">>> 3/4 DNS: $HOSTNAME -> 隧道 $TUNNEL_NAME"
cloudflared tunnel route dns "$TUNNEL_NAME" "$HOSTNAME"
echo ">>> 4/4 请先确保 Nginx 在 127.0.0.1:8080 提供 WordLoop,然后运行:"
echo " cloudflared tunnel --config $CONFIG run"
echo ""
echo "生产环境可复制 systemd 单元:deploy/systemd/cloudflared-wordloop.service"
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# 在 105 服务器上为 w.tkmind.cn 签发 Let's Encrypt 并 reload nginx
set -euo pipefail
DOMAIN=w.tkmind.cn
WEBROOT=/root/wordloop/frontend/dist
EMAIL="${CERTBOT_EMAIL:-admin@tkmind.cn}"
# 先确保 80 可访问(用于 ACME)
cp /root/wordloop/deploy/wordloop-locations.inc /etc/nginx/conf.d/wordloop-locations.inc
cp /root/wordloop/deploy/nginx-production.conf /etc/nginx/conf.d/wordloop.conf
# 若尚无证书,临时注释 443 块会导致语法错误 — 仅用 certbot 独立签发的 80 配置
if [ ! -f "/etc/letsencrypt/live/${DOMAIN}/fullchain.pem" ]; then
cat > /etc/nginx/conf.d/wordloop.conf <<'NGINX80'
server {
listen 80;
server_name w.tkmind.cn;
root /root/wordloop/frontend/dist;
location /.well-known/acme-challenge/ { root /root/wordloop/frontend/dist; }
location / { try_files $uri $uri/ /index.html; }
}
NGINX80
nginx -t && systemctl reload nginx
certbot certonly --webroot -w "$WEBROOT" -d "$DOMAIN" \
--non-interactive --agree-tos -m "$EMAIL" \
--preferred-challenges http
fi
cp /root/wordloop/deploy/wordloop-locations.inc /etc/nginx/conf.d/wordloop-locations.inc
cp /root/wordloop/deploy/nginx-production.conf /etc/nginx/conf.d/wordloop.conf
nginx -t
systemctl reload nginx
echo "SSL ready for https://${DOMAIN}/"
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# 本地 MySQL -> 生产 RDS(经 105 服务器导入)
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SSH_TARGET="${SSH_TARGET:-root@120.26.184.105}"
REMOTE_SQL="/tmp/wordloop-sync.sql.gz"
LOCAL_HOST="${LOCAL_MYSQL_HOST:-localhost}"
LOCAL_PORT="${LOCAL_MYSQL_PORT:-3306}"
LOCAL_USER="${LOCAL_MYSQL_USER:-boot}"
LOCAL_PASS="${LOCAL_MYSQL_PASSWORD:-888888}"
LOCAL_DB="${LOCAL_MYSQL_DATABASE:-wordloop}"
RDS_HOST="${RDS_HOST:-rm-uf6h1j53vtuxi78i90o.mysql.rds.aliyuncs.com}"
RDS_PORT="${RDS_PORT:-3306}"
RDS_USER="${RDS_USER:-boot}"
RDS_PASS="${RDS_PASSWORD:-@Abc888888}"
RDS_DB="${RDS_DATABASE:-wordloop}"
TABLES="users user_settings words quiz_records dictionary_entries"
DUMP="$(mktemp /tmp/wordloop-sync.XXXXXX.sql.gz)"
cleanup() { rm -f "${DUMP%.gz}" "$DUMP" 2>/dev/null || true; }
trap cleanup EXIT
echo ">>> 1/3 导出本地库 (${LOCAL_DB})..."
mysqldump -h "$LOCAL_HOST" -P "$LOCAL_PORT" -u "$LOCAL_USER" -p"$LOCAL_PASS" \
--single-transaction --quick --extended-insert --add-drop-table \
--set-gtid-purged=OFF --no-tablespaces \
"$LOCAL_DB" $TABLES | gzip -c > "$DUMP"
ls -lh "$DUMP"
echo ">>> 2/3 上传到 ${SSH_TARGET}..."
scp "$DUMP" "${SSH_TARGET}:${REMOTE_SQL}"
echo ">>> 3/3 导入生产 RDS (${RDS_HOST})..."
ssh "$SSH_TARGET" "gunzip -c ${REMOTE_SQL} | mysql -h '${RDS_HOST}' -P '${RDS_PORT}' -u '${RDS_USER}' -p'${RDS_PASS}' '${RDS_DB}' && rm -f ${REMOTE_SQL}"
echo ">>> 校验生产库行数..."
ssh "$SSH_TARGET" "cd /root/wordloop/backend && source venv/bin/activate && python3 - <<'PY'
from sqlalchemy import text
from database import engine
with engine.connect() as c:
for t in ['users','words','dictionary_entries','user_settings','quiz_records']:
print(f\"{t}:\", c.execute(text(f'SELECT COUNT(*) FROM {t}')).scalar())
PY"
echo ">>> 同步完成"
@@ -0,0 +1,13 @@
[Unit]
Description=Cloudflare Tunnel for WordLoop (w.tkmind.cn)
After=network-online.target nginx.service
Wants=network-online.target
[Service]
Type=notify
ExecStart=/usr/local/bin/cloudflared tunnel --config /var/www/wordloop/deploy/cloudflared/config.yml run
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,15 @@
[Unit]
Description=WordLoop FastAPI backend
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/wordloop/backend
EnvironmentFile=/root/wordloop/backend/.env
ExecStart=/root/wordloop/backend/venv/bin/uvicorn main:app --host 127.0.0.1 --port 18004
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+15
View File
@@ -0,0 +1,15 @@
[Unit]
Description=WordLoop FastAPI backend
After=network.target mysql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/wordloop/backend
EnvironmentFile=/var/www/wordloop/backend/.env
ExecStart=/var/www/wordloop/backend/venv/bin/uvicorn main:app --host 127.0.0.1 --port 18004
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
+37
View File
@@ -0,0 +1,37 @@
# 由 install-production.sh 复制到 /etc/nginx/conf.d/wordloop-locations.inc
location /.well-known/acme-challenge/ {
try_files $uri =404;
}
location /api/ {
proxy_pass http://127.0.0.1:18004;
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;
}
location /docs {
proxy_pass http://127.0.0.1:18004;
proxy_set_header Host $host;
}
location /openapi.json {
proxy_pass http://127.0.0.1:18004;
proxy_set_header Host $host;
}
# 静态资源不存在时必须 404,不能回退 index.html(否则 JS 加载失败页面空白)
location ^~ /assets/ {
try_files $uri =404;
expires 7d;
add_header Cache-Control "public, immutable";
}
# Vue Router history/login 等路径回退 index.html
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-store, no-cache, must-revalidate";
}