Initial commit: WordLoop 单词学习应用
Vue 前端 + FastAPI 后端,含部署脚本与词典数据。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,直接进入第二节。
|
||||
|
||||
---
|
||||
|
||||
## 二、两种接入方式(二选一)
|
||||
|
||||
### 方式 A:Cloudflare 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` — 前端生产构建
|
||||
Executable
+13
@@ -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/"
|
||||
@@ -0,0 +1,15 @@
|
||||
# Cloudflare Tunnel 示例配置
|
||||
# 1. 复制为 config.yml:cp 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
|
||||
Executable
+98
@@ -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"
|
||||
@@ -0,0 +1,28 @@
|
||||
# WordLoop 生产 — w.tkmind.cn(80 + 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+45
@@ -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)"
|
||||
@@ -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
|
||||
Executable
+46
@@ -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"
|
||||
@@ -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}/"
|
||||
Executable
+49
@@ -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
|
||||
@@ -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
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user