Initial commit: MyBlog full stack blog

This commit is contained in:
2026-08-22 22:28:41 +08:00
commit c61193f2af
64 changed files with 7290 additions and 0 deletions
+476
View File
@@ -0,0 +1,476 @@
# MyBlog 部署指南(Ubuntu + Nginx + FastAPI
本指南覆盖:服务器初始化 → 上传项目 → 安装依赖 → 配置 Nginx / systemd → 开启 HTTPS → 验证上线。
假设你的阿里云服务器为 **Ubuntu 22.04/24.04**,已有公网 IP 与可登录的账号(root 或 sudo 用户)。
> 架构速览:Nginx 提供前端静态文件与 `/uploads/`,并把 `/api/*` 反代到
> `127.0.0.1:8080` 的 FastAPI(只监听本机,公网无法直连);后端使用 SQLite,数据在
> `/var/www/blog/backend/blog.db`。
---
## 一、本地准备:确认要上传的内容
项目根目录(`D:\MyBlog`)需要上传:
| 路径 | 说明 | 是否上传 |
| --- | --- | --- |
| `frontend/` | 前端全部文件(index.html / *.js / style.css | ✅ |
| `backend/` | 后端代码(不含 `blog.db``__pycache__` | ✅ |
| `uploads/` | 上传目录(可传已有文件,如头像 favicon.png) | ✅(可为空) |
| `requirements.txt` | Python 依赖清单 | ✅ |
| `.env.example` | 环境变量模板 | ✅ |
| `README.md` | 项目说明 | ✅ |
| `deploy/` | 部署文件(nginx.conf / myblog.service / 本指南) | ✅ |
| `.env` | 含密钥与邮箱授权码,**不要上传**,到服务器上手动创建 | ❌ |
| `backend/blog.db` | 本地测试数据库,**不要上传**,服务器上重新初始化 | ❌ |
### 1.1 Windows 打包并上传(自带 tar 与 scp)
```powershell
# 在项目目录 D:\MyBlog 下执行:打包(排除本地环境文件)
tar -czf myblog.tar.gz frontend backend uploads deploy requirements.txt .env.example README.md
# 上传到服务器 /tmp(把 root@IP 换成你的实际账号和公网 IP)
scp myblog.tar.gz root@你的公网IP:/tmp/
```
> 提示:Windows 10 1803+ 自带 `tar` / `scp`。也可用 WinSCP / Xftp 直接拖拽,
> 或使用 Git Bash / WSL 里的 `rsync -avz --exclude='.env' --exclude='backend/blog.db' ./ root@IP:/var/www/blog/`。
---
## 二、阿里云安全组开放端口
登录阿里云控制台 → ECS 实例 → 安全组 → 配置规则 → 入方向,**仅开放以下端口**:
| 端口 | 用途 | 建议 |
| --- | --- | --- |
| 22 | SSH 远程管理 | 建议“指定源”只放行你的家庭/办公 IP |
| 80 | HTTPNginx | 0.0.0.0/0 |
| 443 | HTTPSNginx | 0.0.0.0/0(配合 certbot |
| 25565 | Minecraft(独立服务,与博客无关) | 按需开放 |
⚠️ **8080 端口千万不要开放**FastAPI 只监听 `127.0.0.1:8080`,公网即使访问 8080 也会被拒绝;
如果安全组放行 8080 而 Nginx 代理配置有误,等于把后端裸奔在公网上。
---
## 三、服务器初始化
SSH 登录服务器后执行:
```bash
# 1. 系统更新 + 安装 Nginx / Python3 / venv / pip
sudo apt update && sudo apt upgrade -y
sudo apt install -y nginx python3 python3-venv python3-pip
# 2. 创建博客专用系统账号(禁止登录,只用于运行后端)
sudo useradd -r -m -s /usr/sbin/nologin blog
# 3. 创建部署目录
sudo mkdir -p /var/www/blog
sudo chown -R blog:blog /var/www/blog
# 4. 解压上传的项目包(由 /tmp 解压到 /var/www/blog
sudo mkdir -p /var/www/blog
cd /tmp && sudo tar -xzf myblog.tar.gz -C /var/www/blog/
sudo chown -R blog:blog /var/www/blog
# 5. 准备证书校验目录(Nginx 配置里用到)
sudo mkdir -p /var/www/blog/.well-known/acme-challenge
sudo chown -R blog:blog /var/www/blog/.well-known
# 6. 确认前端文件就位(页面引用绝对路径 /main.jsroot 必须指向 frontend
ls /var/www/blog/frontend/index.html
```
---
## 四、配置 .env(服务器上手动创建)
`.env` 含密钥与授权码,必须到服务器上创建,不要从本地直接上传:
```bash
cd /var/www/blog
sudo -u blog cp .env.example .env
sudo -u blog nano .env # 或 vi
```
必填项(`nano` 里按 `Ctrl+O` 保存,`Ctrl+X` 退出):
```ini
# 强随机密钥:在服务器上执行下面命令生成,然后粘贴进来
# python3 -c "import secrets; print(secrets.token_urlsafe(48))"
JWT_SECRET=这里粘贴生成的随机密钥
# 博主账号(首次执行 python -m backend.seed 时创建)
BLOGGER_USERNAME=孤竹居士
BLOGGER_EMAIL=guzhujushi2008@163.com
BLOGGER_PASSWORD=你的登录密码
# SMTP 邮箱验证码(163 邮箱 + 授权码)
SMTP_HOST=smtp.163.com
SMTP_PORT=465
SMTP_USER=guzhujushi2008@163.com
SMTP_AUTH_CODE=你的163授权码
EMAIL_FROM=guzhujushi2008@163.com
```
其余可选变量(限流、验证码、评论长度等)不填会使用默认值,说明见 `.env.example`
设置文件权限,防止他人读取密钥:
```bash
sudo chmod 600 /var/www/blog/.env
```
---
## 五、安装依赖 + 初始化数据库
```bash
# 1. 创建虚拟环境并安装依赖(建议先换成国内 pip 镜像,速度更快)
cd /var/www/blog
sudo -u blog python3 -m venv venv
sudo -u blog venv/bin/pip install -r requirements.txt
# 可选加速:-i https://pypi.tuna.tsinghua.edu.cn/simple
# 2. 初始化数据库表(自动创建 backend/blog.db
sudo -u blog venv/bin/python -m backend.database
# 3. 创建唯一博主账号(读取 .env 里的 BLOGGER_*,重复执行会跳过)
sudo -u blog venv/bin/python -m backend.seed
# 4. 检查上传目录可写
sudo -u blog mkdir -p uploads/avatar uploads/article uploads/project
sudo chown -R blog:blog /var/www/blog/uploads
```
---
## 六、部署 Nginx 配置
```bash
# 1. 复制配置文件(内容见 deploy/nginx.conf
sudo cp /var/www/blog/deploy/nginx.conf /etc/nginx/sites-available/myblog
# 2. 把 server_name 改成你的域名(或公网 IP)
sudo nano /etc/nginx/sites-available/myblog
# 3. 启用站点(删除默认站点,避免冲突)
sudo rm -f /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/myblog /etc/nginx/sites-enabled/
# 4. 校验配置并重载
sudo nginx -t
sudo systemctl reload nginx
```
---
## 七、部署 systemd 服务
```bash
# 1. 复制服务文件(内容见 deploy/myblog.service
sudo cp /var/www/blog/deploy/myblog.service /etc/systemd/system/
# 2. 加载并启动
sudo systemctl daemon-reload
sudo systemctl enable --now myblog
# 3. 确认状态:Active: active (running) 表示成功
sudo systemctl status myblog
```
**手动启动方式(调试用,生产请用 systemd)**
```bash
cd /var/www/blog
sudo -u blog /var/www/blog/venv/bin/uvicorn backend.main:app --host 127.0.0.1 --port 8080
```
---
## 八、开启 HTTPSLet's Encrypt,免费证书)
```bash
# 1. 安装 certbot 的 Nginx 插件
sudo apt install -y certbot python3-certbot-nginx
# 2. 一键签发并自动改写 Nginx 配置(会自动加 443 监听与自动续期)
sudo certbot --nginx -d your-domain.com
# 3. 证书自动续期(certbot 已内置定时任务,可验证)
sudo certbot renew --dry-run
```
HTTPS 配置完成后:
- 80 端口会 301 跳转到 443certbot 自动处理)
- 记得把安全组 443 放行(见第二节)
- 建议把 `.env` 里的 `EMAIL_FROM` 等保持不变即可
---
## 九、上线验证清单
```bash
# 1. 后端接口是否通(应返回 {"success":true,...}
curl http://127.0.0.1:8080/api/article/list
# 2. 前端首页是否可访问(应返回 index.html)
curl -I http://127.0.0.1:8080/ # 本机访问后端正常(证明服务活着)
curl -I http://你的公网IP/ # 公网走 Nginx 访问首页
# 公网直接访问 http://公网IP:8080 应超时/拒绝(8080 未放行且只监听本机)
# 3. SPA 刷新是否正常(直接访问子路由应回退到 index.html)
curl -I http://你的公网IP/partition/4
# 4. 上传目录是否可访问(favicon 应为 200
curl -I http://你的公网IP/uploads/avatar/favicon.png
# 5. 查看后端日志是否有报错
journalctl -u myblog -f
# 6. 浏览器验证全流程:
# 打开首页 → 注册/登录 → 文章列表 → 文章详情 → 评论/点赞 → 好友申请
# (博主)登录 → 发文管理 → 上传头像/封面/项目 → 审批好友申请
```
---
## 十、常见问题
| 现象 | 排查方向 |
| --- | --- |
| 页面能打开但接口报错 | `journalctl -u myblog` 看后端日志;确认 `.env``JWT_SECRET` 已配置 |
| 上传图片 404 | `uploads/` 目录权限:`sudo chown -R blog:blog /var/www/blog/uploads` |
| 刷新子页面 404 | Nginx `try_files $uri $uri/ /index.html;` 是否在 `location /` 中 |
| 博主登录失败多次被锁 | 安全组限流策略(默认 5 次/15 分钟),等窗口过期或调大 `.env` 阈值 |
| 验证码发不出去 | 检查 `.env` 的 SMTP 配置与 163 授权码;服务器 465 端口出方向是否被云安全组限制 |
| 修改代码后不生效 | 后端:`sudo systemctl restart myblog`;前端:用 `deploy/update.ps1` 部署(自动换版本号,浏览器强制拉新文件);若仍异常,Ctrl+F5 强刷一次 |
| 8080 被公网扫到 | 确认 uvicorn 只监听 `127.0.0.1`,且安全组未放行 8080 |
---
## 十一、日常运维
```bash
# 查看状态 / 重启 / 日志
sudo systemctl status myblog
sudo systemctl restart myblog
journalctl -u myblog -n 100
# 备份数据库与上传文件(SQLite 单文件 + 目录)
sudo tar -czf backup_$(date +%F).tar.gz -C /var/www/blog backend/blog.db uploads
# 升级代码流程:见下方「更新代码到服务器(增量同步 + 自动重启)」
```
### 更新代码到服务器(增量同步 + 自动重启)
修改完本地代码后,把改动同步到服务器并重启后端,**一条命令完成**:
```bash
# 在本地 Git Bash / WSL 的项目目录执行(root 登录时无需 sudo)
rsync -avz \
--exclude='.env' \
--exclude='backend/blog.db' \
--exclude='venv' \
--exclude='__pycache__' \
--exclude='uploads/' \
./ root@你的公网IP:/var/www/blog/ \
&& ssh root@你的公网IP 'systemctl restart myblog'
```
命令做了什么:
- `rsync` 只把**本地改动过的文件**增量同步到服务器,未变化的自动跳过;
`&&` 表示同步成功后继续通过 SSH 执行重启 → 改完代码跑这一条,就是一次完整更新。
- 只改前端(`frontend/`)时后端无需重启;重启约 1 秒、无副作用,想省心可每次都执行。
注意:
- **不要加 `--delete`**:会删除服务器上本地没有的文件(例如用户通过网页上传的图片)。
- 排除 `uploads/`:它是服务器上的运行时数据(网页上传的文件只存在于服务器)。
若本地新增了静态资源(如头像 favicon)需要上传,去掉 `--exclude='uploads/'` 再执行。
- 非 root 用户:把 `root@` 换成你的用户名,并先在服务器上配置该命令的免密 sudo:
```bash
# 服务器上执行(username 换成你的登录用户)
sudo visudo -f /etc/sudoers.d/blog-update
# 在文件里加入下面一行后保存:
# username ALL=(ALL) NOPASSWD: /bin/systemctl restart myblog
```
之后本地执行:
```bash
rsync -avz --exclude='.env' --exclude='backend/blog.db' --exclude='venv' --exclude='__pycache__' --exclude='uploads/' ./ username@你的公网IP:/var/www/blog/ && ssh username@你的公网IP 'sudo systemctl restart myblog'
```
验证更新是否生效:
```bash
ssh root@你的公网IP 'systemctl status myblog'
curl -s http://你的公网IP/api/article/list # 应返回 {"success":true,...}
```
### 一键更新脚本(推荐,Windows 上直接用)
项目自带两个脚本,不需要 rsync / Git BashWindows 打开 PowerShell 即可:
| 文件 | 作用 |
| --- | --- |
| `deploy/update.ps1` | 本地一键脚本:打包代码 -> 上传 -> 触发服务器更新 |
| `deploy/update_server.sh` | 服务器端脚本:备份 -> 解压 -> 数据库迁移 -> 重启 -> 验证 |
| `deploy/deploy.conf` | 配置服务器地址与域名(`SERVER=` / `DOMAIN=` |
用法:
```powershell
# 在项目根目录 D:\MyBlog 下执行
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1
# 或指定服务器(覆盖 deploy.conf
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1 -Server root@8.145.36.108
# 需要把本地 uploads/ 一并同步时(首次部署 / 新增静态资源)
powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1 -IncludeUploads
```
脚本会做的事(与上面 rsync 方案等价且更省心):
- 打包时自动排除 `.env``backend/blog.db``__pycache__``venv`,默认也排除 `uploads/`
- 每次更新前在服务器上自动备份到 `/root/blog-backups/`(含数据库与上传文件),可回滚
- 自动执行幂等数据库迁移(补齐 `articles.category` 等新列),并重启 `myblog` 服务
- 结束后自动验证后端接口与线上 HTTPS 是否正常
### 前端缓存自动清理(更新后无需手动 Ctrl+F5)
页面引用的 `style.css` / `main.js` 已带版本号 `?v=__VERSION__`
- 每次执行 `deploy/update.ps1` 部署时,服务器端会自动把 `__VERSION__` 替换成**当前时间戳**
- 版本号一变化,浏览器就会重新下载最新 JS/CSS,旧缓存自动失效,无需手动清理
- Nginx 同时对前端文件返回 `Cache-Control: no-cache`(每次重新校验),双保险
- 如需手动验证:`curl -s https://你的域名/ | grep main.js` 应能看到带版本号的引用
### SSH 白名单自动更新(家庭公网 IP 变化不锁死)
家庭宽带公网 IP 经常变化,若安全组 22 端口只放行固定 IP,换 IP 后 SSH 会被自己挡在门外。
本项目提供“家庭端定时上报 + 服务器端自动更新安全组”的完整方案,文件均在 `deploy/ipwatch/`
```
家庭电脑 report.ps1(每 30 分钟)---> 博客 /myip 获取当前公网 IP
| IP 有变化时
└--> POST /api/ipwatch/report(携带密钥)
服务器调用阿里云 ECS API:先新增新 IP 规则,成功后再删除旧规则(绝不锁死 SSH)
```
**服务器端配置(一次性):**
1. 在服务器 `.env` 中追加以下变量(AccessKey 只放服务器,不要发给家庭端):
```ini
# 生成随机上报密钥:python3 -c "import secrets; print(secrets.token_urlsafe(24))"
IPWATCH_SECRET=这里粘贴随机密钥
ALIYUN_AK_ID=你的RAM子账号AK
ALIYUN_AK_SECRET=你的RAM子账号SK
IPWATCH_SECURITY_GROUP_ID=sg-0jl65y8luej10xz8neli
IPWATCH_REGION=cn-wulanchabu
IPWATCH_PORT=22
```
2. RAM 子账号只授予该安全组的查询/增/删规则权限(`ecs:DescribeSecurityGroupAttribute`
`ecs:AuthorizeSecurityGroup``ecs:RevokeSecurityGroup`),不要使用主账号密钥。
3. 重新部署代码后重启服务:`sudo systemctl restart myblog`
4. 验证服务状态:`curl -s http://127.0.0.1:8080/api/ipwatch/status`,应返回 `configured: true`
**家庭端安装(Windows,一次性):**
```powershell
cd deploy\ipwatch
Copy-Item ipwatch.conf.example ipwatch.conf
# 用记事本编辑 ipwatch.confSECRET 填与服务器 IPWATCH_SECRET 相同的值
powershell -ExecutionPolicy Bypass -File .\install.ps1 # 注册每 30 分钟的计划任务
Get-Content .\ipwatch.log # 查看首次运行结果
```
**验证方法:**
- 手动模拟一次 IP 变化:调用 `POST /api/ipwatch/report` 上报一个测试 IP,再到阿里云控制台
`deploy/ipwatch` 检查安全组 22 端口规则,确认“先加后删”;随后再上报回真实 IP。
- 说明:测试期间新规则替换旧规则,SSH 会短暂不可用,测试后必须恢复真实 IP。
**注意事项:**
- 安全组中其它端口(80/443/25565 等)和 `0.0.0.0/0` 规则不会被脚本触碰,
它只操作“tcp 22/22 且来源为单个 IP(/32)”的规则。
- 上报接口按来源 IP 限流(默认 60 秒一次,`IPWATCH_REPORT_INTERVAL_SECONDS` 可调)。
- 若上报密钥泄露:改服务器 `.env``IPWATCH_SECRET` → 重启服务 → 同步改家庭端 `ipwatch.conf`
- 阿里云 AccessKey 建议定期在 RAM 控制台轮换,轮换后同步更新服务器 `.env`
### JS→WASM 预编译(把上传项目的 JS 编译为 WASM)
后端提供“把项目演示目录里的 .js 预编译为 .wasm”的功能(基于 Javy / QuickJS),
编译产物与报告存放在 `uploads/demos/{项目id}/wasm/`
**重要说明(先看这里):**
- 浏览器页面**仍然运行原始 JS**。Javy 产物是“QuickJS 解释器 + 字节码”,不能操作
DOM,速度也不如浏览器自带的 V8 JIT,所以它**不能替代**演示页里的 JS。
- 这个功能的真正价值是**服务端沙箱执行**:把不可信的 JS 关进 WASM 沙箱里运行,
隔离文件/网络/系统访问(符合“禁止直接运行用户上传代码”的安全规范)。
**安装编译工具(一次性,服务器上执行):**
```bash
cd /var/www/blog
bash deploy/install_javy.sh # 下载约 14MB,安装到 /usr/local/bin/javy
javy --version # 验证:应输出 javy 9.1.0
```
体积与性能分析:
- Javy 官方 Linux 工具包:约 14MB(下载)/ 约 40MB(解压后),磁盘足够即可。
- 编译产物大小:每个 .wasm 约 300KB1MB(内含 QuickJS 运行时快照 + 你的 JS 字节码)。
- 编译速度:普通几十 KB 的 JS 文件 1 秒内完成;总耗时与文件数成正比。
- 运行性能:QuickJS 是解释器,同类计算基准通常比 V8 JIT 慢数倍到十几倍;
用于沙箱隔离/短任务没问题,不适合做重计算。
**下载压缩包也会被一起打包为 wasm:**
- 每次预编译时,除了演示目录里的 .js,还会把项目的下载压缩包(uploads/project/{uuid}.zip
内的全部 JS 合并打包为单个 wasm,输出到同目录 `uploads/project/{uuid}.wasm`(可直接下载)。
- 说明:zip 本身不是 JS,无法直接编译;这里是把 zip 内的 .js 按文件名排序拼接成一个
临时 bundle 再交给 Javy,产物是“压缩包的 wasm 版本”(演示页仍运行原始 JS)。
- 总量上限默认 10MB`WASM_BUNDLE_MAX_SIZE_MB` 可调),超过会跳过打包并在报告中注明。
**使用方式(任选其一):**
```bash
# 方式一:服务器上手动触发(推荐先用这个验证)
bash deploy/precompile_wasm.sh 1
# 方式二:博主登录后调用接口
curl -X POST http://127.0.0.1:8080/api/project/1/precompile -H "Authorization: Bearer <token>"
# 查看最近一次编译报告(公开接口)
curl -s http://127.0.0.1:8080/api/project/1/wasm-report
```
**可选配置(追加到服务器 .env,重启生效):**
```ini
WASM_COMPILE_ENABLED=true # 总开关
JAVY_PATH=/usr/local/bin/javy # 编译工具路径
WASM_MAX_JS_SIZE_MB=3 # 单个 JS 超过 3MB 跳过
WASM_TIMEOUT_SECONDS=60 # 单文件编译超时
```
运行验证(可选):编译产物可用 wasmtime 执行(服务器已装 v47.0.3,路径 /usr/local/bin/wasmtime):
`ash
wasmtime run uploads/demos/1/wasm/data.wasm # 纯数据脚本可正常退出
`
注意:Javy 下载源是 GitHub Releases,服务器需能访问 GitHub;若失败可在本机下载后
`scp` 到服务器再解压安装(包名:javy-x86_64-linux-v9.1.0.gz)。
+5
View File
@@ -0,0 +1,5 @@
# MyBlog 部署配置(供 update.ps1 读取,可自行修改)
# 服务器地址:SSH 登录格式 user@公网IP(必须已配置 SSH 免密登录)
SERVER=root@8.145.36.108
# 域名:用于更新后验证 HTTPS 是否正常;没有域名可留空
DOMAIN=guzhujushi.cn
+42
View File
@@ -0,0 +1,42 @@
#!/bin/bash
# ============================================================
# 安装 Javy(JS→WASM 编译工具)到服务器
# ------------------------------------------------------------
# 用法:bash deploy/install_javy.sh
# 作用:从 GitHub 官方 Releases 下载 Javy(约 14MB 单文件),
# 校验 SHA256 后安装到 /usr/local/bin/javy
# 幂等:已安装且版本一致时直接跳过,可重复执行
# 体积说明:下载包约 14MB,解压后约 40MB,仅占磁盘少量空间
# ============================================================
set -eu
JAVY_VERSION="${JAVY_VERSION:-9.1.0}"
JAVY_BIN="/usr/local/bin/javy"
URL="https://github.com/bytecodealliance/javy/releases/download/v${JAVY_VERSION}/javy-x86_64-linux-v${JAVY_VERSION}.gz"
SHA_URL="${URL}.sha256"
# 已安装且版本匹配 -> 跳过(避免重复下载)
if [ -x "$JAVY_BIN" ] && "$JAVY_BIN" --version 2>/dev/null | grep -q "$JAVY_VERSION"; then
echo "Javy ${JAVY_VERSION} 已安装,跳过下载:$($JAVY_BIN --version)"
exit 0
fi
echo "== 下载 Javy ${JAVY_VERSION}(约 14MB=="
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
curl -fL -sS -o "$TMP/javy.gz" "$URL"
curl -fL -sS -o "$TMP/javy.gz.sha256" "$SHA_URL"
echo "== 校验 SHA256 =="
EXPECT=$(awk '{print $1}' "$TMP/javy.gz.sha256")
ACTUAL=$(sha256sum "$TMP/javy.gz" | awk '{print $1}')
if [ "$EXPECT" != "$ACTUAL" ]; then
echo "校验失败:期望 $EXPECT,实际 $ACTUAL" >&2
exit 1
fi
echo "校验通过"
gunzip -f "$TMP/javy.gz"
install -m 0755 "$TMP/javy" "$JAVY_BIN"
echo "安装完成:$("$JAVY_BIN" --version)"
echo "路径:$JAVY_BIN"
+39
View File
@@ -0,0 +1,39 @@
# MyBlog IP 白名单自动更新(家庭端)
## 这是什么
家庭宽带的公网 IP 会不定期变化。阿里云安全组 22 端口如果只放行固定 IP,
换 IP 后你就会被自己挡在门外。
这套方案让家庭电脑定时检测自己的公网 IP,一旦变化就通知服务器,
服务器自动调用阿里云 API 把安全组 22 端口白名单更新为最新 IP。
流程:
1. report.ps1 请求 博客/myip 获取当前公网 IP
2. 与上次记录比较,有变化时 POST /api/ipwatch/report
3. 服务器调用阿里云 API 更新安全组(先加新规则、后删旧规则)
## 家庭端配置步骤(Windows
1. 复制 ipwatch.conf.example 为 ipwatch.conf,填写:
- SERVER:博客地址(默认 https://guzhujushi.cn
- SECRET:与服务器 .env 中 IPWATCH_SECRET 相同
2. 运行安装脚本(注册每 30 分钟的计划任务并立即执行一次):
powershell -ExecutionPolicy Bypass -File .\install.ps1
3. 查看日志确认成功:
Get-Content .\ipwatch.log
正常会看到:白名单更新成功:你的公网IP(服务器:白名单更新成功)
或:公网 IP 未变化:xxx(表示无需更新,正常)。
## 手动运行 / 卸载
powershell -ExecutionPolicy Bypass -File .\report.ps1
powershell -ExecutionPolicy Bypass -File .\uninstall.ps1
## 安全说明
- 家庭端只保存 IPWATCH_SECRET(随机字符串);阿里云 AccessKey 只存在服务器
.env 中,不要复制到家庭端或提交到 git。
- 如果 IPWATCH_SECRET 泄露:登录服务器改 .env 里的值,再同步修改本目录
ipwatch.conf,最后重启后端服务即可。
- 服务器端每次上报限流 60 秒一次,且只增删 22 端口 /32 单 IP 规则。
+33
View File
@@ -0,0 +1,33 @@
# ============================================================
# 安装 MyBlog IP 白名单定时任务(家庭 Windows 电脑)
# 作用:注册 Windows 计划任务,每 30 分钟自动运行 report.ps1
# 前置:已复制 ipwatch.conf.example 为 ipwatch.conf 并填写
# 用法:powershell -ExecutionPolicy Bypass -File .\install.ps1
# 说明:计划任务在“当前登录用户”下运行;如需开机无登录也运行,
# 请右键任务 -> 属性勾选“不管用户是否登录都要运行”
# ============================================================
$ErrorActionPreference = "Stop"
$ScriptDir = $PSScriptRoot
$Report = Join-Path $ScriptDir "report.ps1"
$ConfFile = Join-Path $ScriptDir "ipwatch.conf"
if (-not (Test-Path -LiteralPath $ConfFile)) {
Write-Host "缺少 ipwatch.conf:请先复制 ipwatch.conf.example 并填写 SERVER / SECRET" -ForegroundColor Red
exit 1
}
$TaskName = "MyBlogIPWatch"
# /TR 里对带空格的脚本路径加引号,PowerShell 会整体作为一个参数传给 schtasks
$Action = "powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"$Report`""
& schtasks /Create /F /TN $TaskName /SC MINUTE /MO 30 /TR $Action | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-Host "创建计划任务失败,请确认以管理员身份运行或检查 schtasks 输出" -ForegroundColor Red
exit 1
}
# 创建后立即运行一次,马上验证配置是否正确
& schtasks /Run /TN $TaskName | Out-Null
Write-Host "计划任务 $TaskName 已创建:每 30 分钟自动检查公网 IP。"
Write-Host "日志文件:$(Join-Path $ScriptDir 'ipwatch.log')"
Write-Host "(首次运行结果可在日志中查看;卸载请运行 uninstall.ps1"
+7
View File
@@ -0,0 +1,7 @@
# MyBlog IP 白名单自动上报配置(家庭 Windows 电脑)
# 用法:复制本文件为 ipwatch.conf 并填写下面两项,再运行 install.ps1
#
# SERVER:博客地址(必须能访问 /myip 与 /api/ipwatch/report),一般填域名
# SECRET:必须与服务器 .env 里的 IPWATCH_SECRET 完全一致
SERVER=https://guzhujushi.cn
SECRET=请填写与服务器 IPWATCH_SECRET 相同的值
+77
View File
@@ -0,0 +1,77 @@
# ============================================================
# MyBlog IP 白名单自动上报脚本(在家庭 Windows 电脑上运行)
# 作用:读取当前公网 IP -> 与上次记录比较 -> 有变化就通知服务器
# 更新阿里云安全组 22 端口白名单(避免换 IP 后 SSH 连不上)
# 用法:
# 手动运行: powershell -ExecutionPolicy Bypass -File .\report.ps1
# 定时运行: 先运行 install.ps1(注册每 30 分钟执行一次的计划任务)
# 退出码:0=成功(含无需更新) 1=配置错误 2=取IP失败 3=IP不合法 4=服务器拒绝 5=网络错误
# ============================================================
$ErrorActionPreference = "Stop"
$ScriptDir = $PSScriptRoot
$ConfFile = Join-Path $ScriptDir "ipwatch.conf"
$LastFile = Join-Path $ScriptDir "lastip.txt"
$LogFile = Join-Path $ScriptDir "ipwatch.log"
# ---------- 1. 读取配置 ----------
$Server = ""
$Secret = ""
if (Test-Path -LiteralPath $ConfFile) {
Get-Content -LiteralPath $ConfFile -Encoding UTF8 | ForEach-Object {
$line = $_.Trim()
if ($line -match '^SERVER\s*=\s*(.+)$') { $Server = $Matches[1].Trim().Trim('"') }
elseif ($line -match '^SECRET\s*=\s*(.+)$') { $Secret = $Matches[1].Trim().Trim('"') }
}
}
if (-not $Server -or -not $Secret) {
Write-Host "配置不完整:请复制 ipwatch.conf.example 为 ipwatch.conf,并填写 SERVER 与 SECRET" -ForegroundColor Red
exit 1
}
# ---------- 2. 日志工具 ----------
function Write-Log([string]$message) {
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $message"
try { Add-Content -LiteralPath $LogFile -Value $line -Encoding UTF8 } catch { }
Write-Host $line
}
# ---------- 3. 获取当前公网 IP(走博客的 /myip 接口) ----------
# 旧系统默认不启用 TLS1.2,先强制开启,否则 HTTPS 请求会失败
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
try {
$current = (Invoke-RestMethod -Uri "$Server/myip" -TimeoutSec 20).ToString().Trim()
} catch {
Write-Log "获取公网 IP 失败:$($_.Exception.Message)"
exit 2
}
if ($current -notmatch '^\d{1,3}(\.\d{1,3}){3}$') {
Write-Log "服务器返回的不是合法 IP$current"
exit 3
}
# ---------- 4. 与上次记录比较,没变化就不打扰服务器 ----------
$last = ""
if (Test-Path -LiteralPath $LastFile) {
$last = (Get-Content -LiteralPath $LastFile -Raw -Encoding UTF8).Trim()
}
if ($current -eq $last) {
Write-Host "公网 IP 未变化:$current"
exit 0
}
# ---------- 5. IP 有变化:上报给服务器,由服务器更新安全组 ----------
try {
$body = @{ secret = $Secret; ip = $current } | ConvertTo-Json
$resp = Invoke-RestMethod -Uri "$Server/api/ipwatch/report" -Method Post -Body $body -ContentType "application/json; charset=utf-8" -TimeoutSec 40
if ($resp.success) {
Set-Content -LiteralPath $LastFile -Value $current -Encoding UTF8
Write-Log "白名单更新成功:$current(服务器:$($resp.message)"
exit 0
} else {
Write-Log "服务器返回失败:$($resp.message)"
exit 4
}
} catch {
Write-Log "上报失败:$($_.Exception.Message)"
exit 5
}
+4
View File
@@ -0,0 +1,4 @@
# 卸载 MyBlog IP 白名单定时任务
$ErrorActionPreference = "Stop"
& schtasks /Delete /F /TN "MyBlogIPWatch" | Out-Null
if ($LASTEXITCODE -eq 0) { Write-Host "计划任务 MyBlogIPWatch 已删除" }
+49
View File
@@ -0,0 +1,49 @@
# ============================================================
# MyBlog 后端 systemd 服务
# ------------------------------------------------------------
# 部署位置:/etc/systemd/system/myblog.service
# 生效命令:
# sudo systemctl daemon-reload
# sudo systemctl enable --now myblog # 开机自启 + 立即启动
# 常用命令:
# sudo systemctl status myblog # 查看状态
# sudo systemctl restart myblog # 重启
# journalctl -u myblog -f # 跟踪日志
# ============================================================
[Unit]
Description=MyBlog FastAPI Backend
# 等待网络就绪后再启动(uvicorn 需要绑定端口)
After=network.target
[Service]
# 专用运行账号:系统用户、禁止登录,只拥有项目文件权限
User=blog
Group=blog
# 工作目录必须是项目根目录(backend 包、.env、uploads 都相对它定位)
WorkingDirectory=/var/www/blog
# 启动命令:使用项目虚拟环境里的 uvicorn,只监听本机 8080(禁止公网直连)
ExecStart=/var/www/blog/venv/bin/uvicorn backend.main:app --host 127.0.0.1 --port 8080
# 崩溃后 3 秒自动重启;开机自启由 enable 管理
Restart=always
RestartSec=3
# 环境变量说明:
# .env 不需要写进 EnvironmentFile —— 应用启动时会在代码里
# 自动读取项目根目录 .envbackend/database.py 的 load_dotenv),
# 这样也能避免 systemd 解析中文值/注释行时出错。
# 部署时请务必在 /var/www/blog/.env 中配置 JWT_SECRET,否则后端拒绝启动。
# ---------- 安全加固(可选) ----------
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
# 允许写入:上传目录(用户上传文件)与 backend 目录(SQLite 数据库 backend/blog.db
ReadWritePaths=/var/www/blog/uploads /var/www/blog/backend
[Install]
WantedBy=multi-user.target
+106
View File
@@ -0,0 +1,106 @@
# ============================================================
# MyBlog Nginx 站点配置
# ------------------------------------------------------------
# 部署位置:/etc/nginx/sites-available/myblog
# 启用命令:sudo ln -s /etc/nginx/sites-available/myblog /etc/nginx/sites-enabled/
# 校验命令:sudo nginx -t
# 重载命令:sudo systemctl reload nginx
#
# 目录约定(与全局架构一致):
# /var/www/blog/frontend 前端静态文件(index.html / *.js / style.css
# /var/www/blog/uploads 上传文件(avatar / article / project
# /var/www/blog/backend 后端代码
# /var/www/blog/.env 环境变量(应用启动时自动加载)
# ============================================================
# ============ HTTP 80:普通访问 + 证书校验 ============
server {
listen 80;
listen [::]:80;
# 改成你的域名(或服务器公网 IP);certbot 会自动填入证书相关配置
server_name your-domain.com;
# 允许上传大小上限:头像最大 4MB、文章视频最大 100MB,留出余量(默认 1MB 会拦截上传)
client_max_body_size 110m;
# ---------- 安全响应头(防点击劫持 / MIME 嗅探 / 泄露来源页) ----------
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# 说明:前端 HTML 内已有 CSP meta;如需更强防护(frame-ancestors 等),
# 可在此追加:add_header Content-Security-Policy "default-src 'self'; ..." always;
# Let's Encrypt 证书签发校验目录(certbot --webroot 模式使用)
# 需要先创建:sudo mkdir -p /var/www/blog/.well-known/acme-challenge
location /.well-known/acme-challenge/ {
root /var/www/blog;
}
# ---------- /myip 返回客户端公网 IPSSH 白名单自动更新用) ----------
# 家庭端脚本先请求此地址获取当前公网 IP,变化后再调用 /api/ipwatch/report
# 注意:certbot 生成 443 的 server 块时,需要把这一段也复制进去
location = /myip {
default_type text/plain;
return 200 $remote_addr;
}
# ---------- /api/* 反向代理到 FastAPI ----------
# FastAPI 只监听 127.0.0.1:8080,公网无法直连,必须走 Nginx 代理
location /api/ {
proxy_pass http://127.0.0.1:8080;
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;
# WASM 预编译等长任务可能超过 60s,放宽到 180s
proxy_read_timeout 180s;
}
# 说明:服务器 /etc/nginx/mime.types 需追加一行 application/wasm wasm;(已执行)
# ---------- /uploads/* 静态文件 ----------
# 头像、文章图片、项目压缩包由 Nginx 直接提供,不经过 Python
location /uploads/ {
alias /var/www/blog/uploads/;
expires 30d;
add_header Cache-Control "public";
add_header X-Content-Type-Options "nosniff" always;
}
# ---------- /demo/* 在线演示(L0 静态托管) ----------
# 上传的项目 zip 若为纯 HTML/CSS/JS,后端解压到 uploads/demos/{项目id}/
# 此处由 Nginx 直接提供(不经过 Python,禁止执行)
location /demo/ {
alias /var/www/blog/uploads/demos/;
index index.html;
try_files $uri $uri/ =404;
add_header X-Content-Type-Options "nosniff" always;
}
# ---------- 前端静态文件(SPA ----------
# root 必须指向 frontend 目录:页面里引用的是 /main.js /style.css 等绝对路径
location / {
root /var/www/blog/frontend;
index index.html;
# SPA 刷新支持:
# 先找真实文件($uri),再找目录($uri/),
# 都找不到就回退到 index.html,由前端 History API 路由接管
try_files $uri $uri/ /index.html;
# 每次重新校验,避免部署后浏览器继续用旧版 JS 导致页面渲染异常
add_header Cache-Control "no-cache";
}
}
# ============ HTTPS 443:用 certbot 一键开启 ============
# 推荐方式(自动改写本配置文件并管理续期):
# sudo apt install certbot python3-certbot-nginx
# sudo certbot --nginx -d your-domain.com
# 执行成功后,本文件会被自动加入 443 监听与证书路径,无需手写。
# 若要手写,参考下面的骨架(把上面 /api/、/uploads/、/ 三段复制进来即可):
# server {
# listen 443 ssl;
# listen [::]:443 ssl;
# server_name your-domain.com;
# ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# ssl_protocols TLSv1.2 TLSv1.3;
# # ... 同上三个 location 块 ...
# }
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# ============================================================
# 手动触发项目 JS→WASM 预编译(在服务器上执行)
# ------------------------------------------------------------
# 用法:bash deploy/precompile_wasm.sh <项目ID>
# 示例:bash deploy/precompile_wasm.sh 1 # 预编译项目一
# 效果:uploads/demos/{id}/wasm/ 下生成 .wasm 与 report.json
# 等价接口:POST /api/project/{id}/precompile(仅博主)
# ============================================================
set -eu
PROJECT_ID="${1:?用法:bash deploy/precompile_wasm.sh <项目ID>}"
cd /var/www/blog
/var/www/blog/venv/bin/python -m backend.services.wasm_builder "$PROJECT_ID"
# 以 root 执行时,把产物属主修正为 blog,保证 Nginx 可读
if [ "$(id -u)" = "0" ]; then
chown -R blog:blog "/var/www/blog/uploads/demos/${PROJECT_ID}/wasm"
fi
+94
View File
@@ -0,0 +1,94 @@
# ============================================================
# MyBlog 一键更新脚本(在 Windows 上运行)
# 作用:打包本地代码 -> 上传到服务器 -> 服务器自动 备份/迁移/重启/验证
# 前置条件:
# 1. 已配置 SSH 免密登录(首次需执行:ssh-keygen 后 ssh-copy-id 服务器)
# 2. 服务器上已按 deploy/DEPLOY.md 完成首次部署(myblog 服务存在)
# 用法(在项目根目录 D:\MyBlog 打开 PowerShell):
# powershell -ExecutionPolicy Bypass -File .\deploy\update.ps1
# 或直接指定服务器:.\deploy\update.ps1 -Server root@8.145.36.108
# 常用参数:
# -Server 服务器地址,默认读取 deploy/deploy.conf(缺省用 root@8.145.36.108
# -IncludeUploads 同时同步本地 uploads/(首次部署或新增静态资源时用)
# 安全说明:
# - 永远不打包 .env 与 backend/blog.db(密钥与数据库只在服务器上)
# - 默认排除 uploads/:网页上传的文件只在服务器,本地打包不覆盖
# ============================================================
param(
[string]$Server = "",
[switch]$IncludeUploads
)
$ErrorActionPreference = "Stop"
$ScriptDir = $PSScriptRoot # deploy 目录
$ProjectRoot = Split-Path -Parent $ScriptDir # 项目根目录
$PackageName = "myblog.tar.gz"
$RemoteScript = "update_server.sh"
function Write-Step([string]$msg) { Write-Host ""; Write-Host "== $msg ==" -ForegroundColor Cyan }
# ---------- 1. 确定服务器地址(命令行参数 > deploy.conf > 默认值) ----------
if (-not $Server -and (Test-Path (Join-Path $ScriptDir "deploy.conf"))) {
$confLine = Get-Content (Join-Path $ScriptDir "deploy.conf") | Where-Object { $_ -match '^\s*SERVER\s*=' } | Select-Object -First 1
if ($confLine) { $Server = ($confLine -split '=', 2)[1].Trim() }
$domainLine = Get-Content (Join-Path $ScriptDir "deploy.conf") | Where-Object { $_ -match '^\s*DOMAIN\s*=' } | Select-Object -First 1
if ($domainLine) { $Domain = ($domainLine -split '=', 2)[1].Trim() }
}
if (-not $Server) { $Server = "root@8.145.36.108" }
Write-Host "目标服务器:$Server"
if ($Domain) { Write-Host "验证域名:$Domain" }
# ---------- 2. 检查依赖工具 ----------
foreach ($tool in @("tar", "scp", "ssh")) {
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) {
throw "缺少工具 $tool,请先安装 OpenSSH 客户端(Windows 设置 -> 可选功能)。"
}
}
# ---------- 3. 打包本地代码(排除密钥/数据库/缓存/上传目录) ----------
Write-Step "打包本地代码"
$tmpPkg = Join-Path $env:TEMP $PackageName
if (Test-Path $tmpPkg) { Remove-Item -LiteralPath $tmpPkg -Force }
Push-Location $ProjectRoot
try {
# 排除本地敏感运行数据:数据库、密钥配置、IPWatch 家庭端密钥与日志
$excludes = @("--exclude=backend/blog.db", "--exclude=backend/__pycache__",
"--exclude=backend/routers/__pycache__",
"--exclude=backend/services/__pycache__", "--exclude=venv",
"--exclude=deploy/ipwatch/ipwatch.conf",
"--exclude=deploy/ipwatch/lastip.txt",
"--exclude=deploy/ipwatch/ipwatch.log")
if (-not $IncludeUploads) { $excludes += "--exclude=uploads" }
$targets = @("frontend", "backend", "deploy", "requirements.txt", ".env.example", "README.md")
& tar -czf $tmpPkg @excludes @targets
if ($LASTEXITCODE -ne 0) { throw "tar 打包失败" }
} finally {
Pop-Location
}
# 校验包内不含敏感文件(防止配置写错把密钥带上服务器)
$bad = tar -tzf $tmpPkg | Select-String "(^|/)\.env$|blog\.db|__pycache__|^uploads/|ipwatch\.(conf|log)$|lastip\.txt" | Select-Object -First 5
if ($bad) { throw "更新包内含不应上传的文件:$($bad -join ', ')" }
Write-Host ("更新包已生成:{0}{1:N0} KB" -f $tmpPkg, ((Get-Item $tmpPkg).Length / 1KB))
# ---------- 4. 上传到服务器 ----------
Write-Step "上传更新包与服务器脚本"
& scp -o BatchMode=yes -o StrictHostKeyChecking=accept-new $tmpPkg "$($Server):/tmp/$PackageName"
if ($LASTEXITCODE -ne 0) { throw "上传失败:请确认已配置 SSH 免密登录" }
& scp -o BatchMode=yes (Join-Path $ScriptDir $RemoteScript) "$($Server):/tmp/$RemoteScript"
if ($LASTEXITCODE -ne 0) { throw "上传服务器脚本失败" }
Write-Host "上传完成"
# ---------- 5. 执行服务器端更新(备份/解压/迁移/重启/验证) ----------
Write-Step "执行服务器端更新(备份、迁移、重启、验证)"
$domainArg = if ($Domain) { " '$Domain'" } else { "" }
# 服务器端脚本若带 Windows 换行(CRLF)会导致 bash 解析失败,先统一转成 LF 再执行
& ssh -o BatchMode=yes $Server "sed -i 's/\r`$//' /tmp/$RemoteScript && bash /tmp/$RemoteScript$domainArg"
if ($LASTEXITCODE -ne 0) {
Write-Host "服务器端更新失败,请检查上方输出。可用备份回滚:/root/blog-backups/" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "✅ 更新完成!请打开 https://$Domain 验证。" -ForegroundColor Green
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# ============================================================
# MyBlog 服务器端更新脚本(由本地 deploy/update.ps1 上传并执行)
# 职责:备份 -> 解压新代码 -> 数据库迁移 -> 重启服务 -> 验证
# 用法:bash /tmp/update_server.sh [域名]
# 适用:Ubuntu + systemdmyblog 服务);需要 root 或已配置免密 sudo
# 安全说明:
# - 更新前自动备份到 /root/blog-backups(含数据库与上传文件),可随时回滚
# - 只增量覆盖代码文件,不删除服务器上的任何文件(不会加 --delete)
# - 数据库迁移是幂等的:重复执行只会补齐缺失的列,不会损坏数据
# ============================================================
set -eu
# bash 支持 pipefaildash/sh 下自动跳过),保证失败即退出
if [ -n "${BASH_VERSION:-}" ]; then set -o pipefail; fi
BLOG_DIR="/var/www/blog"
PKG="/tmp/myblog.tar.gz"
BACKUP_DIR="/root/blog-backups"
DOMAIN="${1:-}"
# 非 root 用户运行时自动加 sudo(前提:已配置免密 sudo)
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
SUDO="sudo"
fi
echo "== [1/5] 更新前备份 =="
$SUDO mkdir -p "$BACKUP_DIR"
TS=$(date +%Y%m%d_%H%M%S)
$SUDO tar -czf "$BACKUP_DIR/blog_backup_$TS.tar.gz" --exclude=venv --exclude='*/__pycache__' -C "$BLOG_DIR" .
echo "备份完成:$BACKUP_DIR/blog_backup_$TS.tar.gz"
echo "== [2/5] 解压新代码 =="
if [ ! -f "$PKG" ]; then
echo "错误:找不到更新包 $PKG,请先通过本地脚本上传" >&2
exit 1
fi
$SUDO tar -xzf "$PKG" -C "$BLOG_DIR"
$SUDO chown -R blog:blog "$BLOG_DIR"
# 前端缓存指纹:每次部署自动生成新版本号,浏览器强制拉取最新 JS/CSS,无需手动 Ctrl+F5
VERSION=$(date +%Y%m%d%H%M%S)
$SUDO sed -i "s/__VERSION__/${VERSION}/g" "$BLOG_DIR/frontend/index.html"
echo "前端版本号:${VERSION}"
# 把 Windows 上传的脚本统一转成 LF 行尾,防止 bash 解析 CR 报错
find "$BLOG_DIR/deploy" -maxdepth 2 -name '*.sh' -type f -exec sed -i 's/\r$//' {} +
echo "解压完成,文件属主已修正为 blog:blog"
echo "== [3/5] 数据库迁移(幂等) =="
python3 - "$BLOG_DIR" <<'PY'
import sqlite3, sys
from pathlib import Path
base = Path(sys.argv[1])
db = base / "backend" / "blog.db"
# 读取 .env 的 DATABASE_URL(与后端 database.py 的解析方式一致,留空用默认)
try:
for line in (base / ".env").read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("DATABASE_URL="):
val = line.split("=", 1)[1].strip().strip('"').strip("'")
if val.startswith("sqlite:///"):
p = val[len("sqlite:///"):]
db = Path(p) if p.startswith("/") else base / p
break
except OSError:
pass
print("目标数据库:", db)
con = sqlite3.connect(db)
cur = con.cursor()
def ensure_table(table, ddl):
rows = cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table,)).fetchall()
if not rows:
cur.execute(ddl)
print(f"建表:{table}")
else:
print(f"已存在:{table}")
def ensure_column(table, col, ddl):
cols = [r[1] for r in cur.execute(f"PRAGMA table_info({table})").fetchall()]
if col not in cols:
cur.execute(f"ALTER TABLE {table} ADD COLUMN {ddl}")
print(f"迁移:{table}.{col} 已新增")
else:
print(f"已存在:{table}.{col}")
ensure_column("articles", "category", "category VARCHAR(30) NOT NULL DEFAULT 'life'")
ensure_column("users", "token_version", "token_version INTEGER NOT NULL DEFAULT 0")
ensure_column("users", "avatar_updated_time", "avatar_updated_time DATETIME")
ensure_column("friends", "created_time", "created_time DATETIME")
ensure_table("projects", """
CREATE TABLE IF NOT EXISTS projects (
id INTEGER NOT NULL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
tech VARCHAR(500),
project_type VARCHAR(20) NOT NULL DEFAULT 'static',
demo_url VARCHAR(500),
download_url VARCHAR(500),
github_url VARCHAR(500),
created_time DATETIME NOT NULL,
updated_time DATETIME NOT NULL
)""")
ensure_column("projects", "visibility", "visibility VARCHAR(20) NOT NULL DEFAULT 'public'")
con.commit()
con.close()
print("数据库迁移完成")
PY
echo "== [4/5] 重启后端服务 =="
$SUDO systemctl restart myblog
sleep 3
if ! $SUDO systemctl is-active --quiet myblog; then
echo "错误:myblog 服务未正常运行,请查看日志:journalctl -u myblog -n 50" >&2
exit 1
fi
echo "myblog 服务运行正常"
echo "== [5/5] 验证 =="
echo "--- 后端接口(应返回 success:true ---"
curl -s http://127.0.0.1:8080/api/article/list | head -c 200
echo
echo "--- 新前端是否生效(发布管理弹窗标记 pub-confirm 数量) ---"
grep -c "pub-confirm" "$BLOG_DIR/frontend/manage.js" || true
if [ -n "$DOMAIN" ]; then
echo "--- 线上 HTTPS$DOMAIN ---"
curl -sk -o /dev/null -w "%{http_code}\n" "https://$DOMAIN/" || echo "HTTPS 检查失败(可能是证书或网络问题)"
fi
echo "更新完成。如需回滚:备份在 $BACKUP_DIR/"