temp
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,10 +18,8 @@ logs/
|
|||||||
.sisyphus/
|
.sisyphus/
|
||||||
|
|
||||||
# Docs
|
# Docs
|
||||||
docs/
|
|
||||||
|
|
||||||
# Scripts
|
# Scripts
|
||||||
scripts/
|
|
||||||
|
|
||||||
# Parent directory reference (if accidentally included)
|
# Parent directory reference (if accidentally included)
|
||||||
ASD-backend/
|
ASD-backend/
|
||||||
|
|||||||
425
docs/CDN-SETUP.md
Normal file
425
docs/CDN-SETUP.md
Normal file
@@ -0,0 +1,425 @@
|
|||||||
|
# 腾讯云 CDN 配置指南
|
||||||
|
|
||||||
|
本文档描述如何为 `xmclassmate.top` 域名配置腾讯云 CDN,实现:
|
||||||
|
1. 隐藏源站服务器 IP
|
||||||
|
2. 加速静态资源访问
|
||||||
|
3. 防止源站被直接攻击
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 域名信息
|
||||||
|
|
||||||
|
| 域名 | 用途 | CDN 适用性 | 说明 |
|
||||||
|
|------|------|------------|------|
|
||||||
|
| `xmclassmate.top` | 主域名 | ✅ 可配置 | 通常作为 API 或主站 |
|
||||||
|
| `www.xmclassmate.top` | WWW | ✅ 可配置 | 网站入口 |
|
||||||
|
| `warden.xmclassmate.top` | Warden | ✅ 可配置 | Warden 服务 |
|
||||||
|
| `gitea.xmclassmate.top` | Gitea | ✅ 可配置 | Git 服务 |
|
||||||
|
| `dev.xmclassmate.top` | 开发环境 | ✅ 可配置 | 后端开发环境 |
|
||||||
|
| `psql.xmclassmate.top` | PostgreSQL | ❌ 不适用 | TCP 协议,CDN 无法代理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 当前架构
|
||||||
|
|
||||||
|
```
|
||||||
|
用户 → DNS (A记录) → 服务器 IP → Nginx → 服务端口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目标架构
|
||||||
|
|
||||||
|
```
|
||||||
|
用户 → CDN → DNS (CNAME) → CDN 节点 → 回源 → Nginx → 服务
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 证书说明
|
||||||
|
|
||||||
|
**已有证书**:`xmclassmate.top`(主域名)
|
||||||
|
|
||||||
|
**子域名处理方式**:
|
||||||
|
|
||||||
|
| 方案 | 适用场景 | 成本 |
|
||||||
|
|------|---------|------|
|
||||||
|
| **腾讯云免费证书** | 每个子域名独立申请 | 免费 |
|
||||||
|
| **泛域名证书** | `*.xmclassmate.top` | 免费/付费 |
|
||||||
|
| **CDN 自有证书** | 直接使用源站证书 | 需上传到 CDN |
|
||||||
|
|
||||||
|
**推荐**:为 `*.xmclassmate.top` 申请泛域名证书,一次覆盖所有子域名。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置步骤
|
||||||
|
|
||||||
|
### 第一步:获取腾讯云 CDN 回源 IP 段
|
||||||
|
|
||||||
|
登录腾讯云 CDN 控制台,获取回源 IP 段:
|
||||||
|
|
||||||
|
**控制台路径**:
|
||||||
|
```
|
||||||
|
内容分发网络 CDN → 域名管理 → 选择域名 → 基本配置 → 回源配置
|
||||||
|
```
|
||||||
|
|
||||||
|
**腾讯云 CDN 回源 IP 段**(常见段):
|
||||||
|
```
|
||||||
|
101.226.0.0/16 # 上海
|
||||||
|
119.147.0.0/16 # 广州
|
||||||
|
123.151.0.0/16 # 天津
|
||||||
|
182.140.0.0/16 # 成都
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:雨云服务器使用腾讯云 CDN,具体 IP 段需在腾讯云 CDN 控制台查看。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第二步:添加加速域名
|
||||||
|
|
||||||
|
**控制台路径**:
|
||||||
|
```
|
||||||
|
内容分发网络 CDN → 域名管理 → 添加域名
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置项**(以 `dev.xmclassmate.top` 为例):
|
||||||
|
|
||||||
|
| 配置项 | 示例值 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 加速域名 | `dev.xmclassmate.top` | 要加速的子域名 |
|
||||||
|
| 业务类型 | Web 网站 | 选择「网站加速」 |
|
||||||
|
| 源站类型 | IP | 填写服务器公网 IP |
|
||||||
|
| 源站地址 | `服务器IP` | 服务器真实 IP |
|
||||||
|
| 回源协议 | HTTPS | 源站已有证书 |
|
||||||
|
| 端口 | 443 | HTTPS 端口 |
|
||||||
|
|
||||||
|
**重复此步骤添加其他域名**:`gitea`、`warden`、`www`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第三步:配置 HTTPS 证书
|
||||||
|
|
||||||
|
#### 方案 A:使用泛域名证书(推荐)
|
||||||
|
|
||||||
|
1. 在腾讯云 SSL 证书控制台申请 `*.xmclassmate.top` 泛域名证书
|
||||||
|
2. 审核通过后,在 CDN 控制台选择该证书
|
||||||
|
|
||||||
|
#### 方案 B:每个子域名独立配置
|
||||||
|
|
||||||
|
1. 为每个子域名申请独立证书
|
||||||
|
2. 上传证书到 CDN
|
||||||
|
|
||||||
|
#### 方案 C:使用源站证书
|
||||||
|
|
||||||
|
如果 Nginx 已配置 HTTPS,可直接「回源协议」选择 HTTPS,CDN 会使用相同证书。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第四步:获取 CDN CNAME
|
||||||
|
|
||||||
|
域名添加成功后,腾讯云会分配一个 CNAME:
|
||||||
|
|
||||||
|
```
|
||||||
|
格式:`xxxxxxxx.xxx.com.w.cdngslb.com`
|
||||||
|
```
|
||||||
|
|
||||||
|
在控制台查看:
|
||||||
|
```
|
||||||
|
内容分发网络 CDN → 域名管理 → dev.xmclassmate.top → 基本配置 → CNAME
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第五步:修改 DNS 解析(雨云 DNSPod)
|
||||||
|
|
||||||
|
**雨云 DNSPod 控制台**:
|
||||||
|
|
||||||
|
| 记录类型 | 主机记录 | 记录值 | TTL |
|
||||||
|
|----------|----------|--------|-----|
|
||||||
|
| CNAME | dev | `xxxxxxxx.xxx.com.w.cdngslb.com` | 600 |
|
||||||
|
| CNAME | gitea | `xxxxxxxx.xxx.com.w.cdngslb.com` | 600 |
|
||||||
|
| CNAME | warden | `xxxxxxxx.xxx.com.w.cdngslb.com` | 600 |
|
||||||
|
| CNAME | www | `xxxxxxxx.xxx.com.w.cdngslb.com` | 600 |
|
||||||
|
|
||||||
|
**注意**:
|
||||||
|
- 将原有的 A 记录**删除或禁用**
|
||||||
|
- CNAME 和 A 记录不能共存于同一主机记录
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第六步:配置 Nginx
|
||||||
|
|
||||||
|
#### 6.1 记录真实客户端 IP
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name dev.xmclassmate.top gitea.xmclassmate.top warden.xmclassmate.top;
|
||||||
|
|
||||||
|
# SSL 证书
|
||||||
|
ssl_certificate /path/to/fullchain.pem;
|
||||||
|
ssl_certificate_key /path/to/privkey.pem;
|
||||||
|
|
||||||
|
# 腾讯云 CDN 回源 IP 段(替换为实际值)
|
||||||
|
set_real_ip_from 10.0.0.0/8;
|
||||||
|
set_real_ip_from 101.226.0.0/16;
|
||||||
|
set_real_ip_from 119.147.0.0/16;
|
||||||
|
set_real_ip_from 123.151.0.0/16;
|
||||||
|
set_real_ip_from 182.140.0.0/16;
|
||||||
|
|
||||||
|
real_ip_header X-Forwarded-For;
|
||||||
|
real_ip_recursive on;
|
||||||
|
|
||||||
|
# 日志格式
|
||||||
|
log_format main '$real_ip_var - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $real_ip_var;
|
||||||
|
proxy_set_header X-Forwarded-For $real_ip_var;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.2 限制只能 CDN 回源访问(可选)
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
geo $cdn_access {
|
||||||
|
default 0;
|
||||||
|
10.0.0.0/8 1;
|
||||||
|
101.226.0.0/16 1;
|
||||||
|
119.147.0.0/16 1;
|
||||||
|
123.151.0.0/16 1;
|
||||||
|
182.140.0.0/16 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
if ($cdn_access = 0) {
|
||||||
|
return 403;
|
||||||
|
}
|
||||||
|
# ... 其他配置
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6.3 重载 Nginx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nginx -t && nginx -s reload
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第七步:配置雨云安全组
|
||||||
|
|
||||||
|
在雨云控制台配置**端口规则**:
|
||||||
|
|
||||||
|
**安全组规则**:
|
||||||
|
|
||||||
|
| 方向 | 来源 | 协议端口 | 策略 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| 入站 | `101.226.0.0/16` | TCP:443 | 允许 |
|
||||||
|
| 入站 | `119.147.0.0/16` | TCP:443 | 允许 |
|
||||||
|
| 入站 | `123.151.0.0/16` | TCP:443 | 允许 |
|
||||||
|
| 入站 | `182.140.0.0/16` | TCP:443 | 允许 |
|
||||||
|
| 入站 | `0.0.0.0/0` | TCP:443 | 拒绝 |
|
||||||
|
|
||||||
|
> **注意**:雨云安全组可能需要单独配置 CDN IP 段,具体操作参考雨云文档。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 第八步:验证配置
|
||||||
|
|
||||||
|
#### 8.1 检查 DNS 解析
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 CNAME 是否生效
|
||||||
|
dig dev.xmclassmate.top
|
||||||
|
|
||||||
|
# 期望输出包含:cdn.com.w.cdngslb.com
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.2 检查 CDN 回源
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 通过 CDN 访问
|
||||||
|
curl -I https://dev.xmclassmate.top
|
||||||
|
|
||||||
|
# 检查响应头是否包含 CDN 信息
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.3 检查 Nginx 日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看是否记录了真实 IP
|
||||||
|
tail -f /var/log/nginx/access.log
|
||||||
|
|
||||||
|
# 期望:$real_ip_var 显示的是用户真实 IP
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.4 验证源站隐藏
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 直接 IP 访问应该被拒绝(如果配置了安全组)
|
||||||
|
curl -I https://服务器IP
|
||||||
|
|
||||||
|
# 期望:连接超时或 403
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## psql.xmclassmate.top 处理
|
||||||
|
|
||||||
|
**PostgreSQL 不适合 CDN**,因为:
|
||||||
|
- CDN 只支持 HTTP/HTTPS 协议
|
||||||
|
- PostgreSQL 使用 TCP 协议(端口 5432)
|
||||||
|
|
||||||
|
**处理方式**:保持现状(A 记录),偶尔手动调试够用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 静态文件缓存配置
|
||||||
|
|
||||||
|
CDN 可以缓存 Rust 后端返回的静态文件(CSS、JS、图片等),减轻源站压力。
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求 /static/css/style.css
|
||||||
|
↓
|
||||||
|
CDN 检查缓存(未命中)
|
||||||
|
↓
|
||||||
|
回源请求 → Rust 后端
|
||||||
|
↓
|
||||||
|
Rust 返回文件 + Cache-Control 头
|
||||||
|
↓
|
||||||
|
CDN 缓存这份响应
|
||||||
|
↓
|
||||||
|
返回给用户
|
||||||
|
|
||||||
|
下一个用户请求 → CDN 直接返回缓存 ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rust 后端配置
|
||||||
|
|
||||||
|
在返回静态文件响应时设置 `Cache-Control` 头:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use actix_web::{HttpResponse, http::header};
|
||||||
|
|
||||||
|
/// 返回静态文件(适合带版本号的文件,如 style.v1.css)
|
||||||
|
pub fn serve_static_file(path: &str) -> HttpResponse {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.insert_header(("Cache-Control", "public, max-age=31536000")) // 缓存 1 年
|
||||||
|
.insert_header(("Content-Type", "text/css"))
|
||||||
|
.body(read_file(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 返回可能更新的静态文件(缓存时间较短)
|
||||||
|
pub fn serve_dynamic_static(path: &str) -> HttpResponse {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.insert_header(("Cache-Control", "public, max-age=86400")) // 缓存 1 天
|
||||||
|
.insert_header(("Content-Type", "application/javascript"))
|
||||||
|
.body(read_file(path))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存时间建议
|
||||||
|
|
||||||
|
| 文件类型 | 缓存时间 | 说明 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| CSS, JS(带版本号) | 1 年 (`max-age=31536000`) | 配合版本号使用 |
|
||||||
|
| 图片 | 1 个月 (`max-age=2592000`) | 很少变动 |
|
||||||
|
| HTML | 不缓存 | 动态内容 |
|
||||||
|
| API 响应 | 不缓存 | 实时数据 |
|
||||||
|
|
||||||
|
### 静态文件命名建议
|
||||||
|
|
||||||
|
为便于 CDN 长期缓存,建议静态文件带版本号或 hash:
|
||||||
|
|
||||||
|
```
|
||||||
|
# 推荐
|
||||||
|
style.v1.css
|
||||||
|
app.v2.js
|
||||||
|
bundle.abc123.css
|
||||||
|
|
||||||
|
# 不推荐(更新时 CDN 仍返回旧缓存)
|
||||||
|
style.css
|
||||||
|
app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 腾讯云 CDN 缓存配置
|
||||||
|
|
||||||
|
在腾讯云 CDN 控制台设置缓存规则:
|
||||||
|
|
||||||
|
```
|
||||||
|
内容分发网络 CDN → 域名管理 → 选择域名 → 缓存配置 → 添加规则
|
||||||
|
|
||||||
|
规则类型:文件类型
|
||||||
|
文件类型:.css,.js,.png,.jpg,.jpeg,.gif,.ico
|
||||||
|
缓存时间:31536000 秒(1年)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 配置检查清单
|
||||||
|
|
||||||
|
| 步骤 | 操作 | 状态 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1 | 获取腾讯云 CDN 回源 IP 段 | ☐ |
|
||||||
|
| 2 | 为每个域名添加 CDN 加速 | ☐ |
|
||||||
|
| 3 | 配置 HTTPS 证书(泛域名或独立) | ☐ |
|
||||||
|
| 4 | 获取 CDN CNAME | ☐ |
|
||||||
|
| 5 | 修改雨云 DNSPod DNS 解析 | ☐ |
|
||||||
|
| 6 | 配置 Nginx 真实 IP | ☐ |
|
||||||
|
| 7 | 配置雨云安全组 | ☐ |
|
||||||
|
| 8 | 验证配置生效 | ☐ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 回滚步骤
|
||||||
|
|
||||||
|
如果配置有问题需要回滚:
|
||||||
|
|
||||||
|
1. **雨云 DNSPod 删除 CNAME,恢复 A 记录**
|
||||||
|
2. **Nginx 移除 `set_real_ip_from` 配置**
|
||||||
|
3. **安全组恢复 0.0.0.0/0 访问**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q1:CDN 配置后多久生效?
|
||||||
|
|
||||||
|
DNS 解析生效时间取决于 TTL,通常 10 分钟到 48 小时。
|
||||||
|
|
||||||
|
### Q2:泛域名证书如何申请?
|
||||||
|
|
||||||
|
腾讯云 SSL 证书控制台 → 免费证书 → 申请证书 → 填写 `*.xmclassmate.top`
|
||||||
|
|
||||||
|
### Q3:雨云安全组在哪里配置?
|
||||||
|
|
||||||
|
雨云控制台 → 云服务器 → 选择服务器 → 安全组
|
||||||
|
|
||||||
|
### Q4:如何确认 CDN 已生效?
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://dev.xmclassmate.top
|
||||||
|
# 查看 Age 或 X-Cache-Lookup 头
|
||||||
|
```
|
||||||
|
|
||||||
|
### Q5:psql 域名如何安全访问?
|
||||||
|
|
||||||
|
保持现状(A 记录),仅供偶尔手动调试使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考链接
|
||||||
|
|
||||||
|
- [腾讯云 CDN 文档](https://cloud.tencent.com/document/product/414)
|
||||||
|
- [腾讯云 CDN 回源配置](https://cloud.tencent.com/document/product/414/65505)
|
||||||
|
- [雨云安全组配置](https://www.rainyun.com/docs/)
|
||||||
|
- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)
|
||||||
98
docs/NOTIFICATIONS.md
Normal file
98
docs/NOTIFICATIONS.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# 通知系统
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
支持三类通知:系统公告(广播)、个人通知(定向)、事件驱动通知。
|
||||||
|
|
||||||
|
### 数据库
|
||||||
|
|
||||||
|
`notifications` 表(迁移 `011_add_notifications.sql`):
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `scope` | `'all'` / `'user'` | 广播或定向 |
|
||||||
|
| `user_id` | INTEGER / NULL | 定向时指定用户 |
|
||||||
|
| `type` | VARCHAR | 通知类型 |
|
||||||
|
| `title` | VARCHAR | 标题 |
|
||||||
|
| `content` | TEXT | 正文 |
|
||||||
|
| `priority` | `high` / `normal` / `low` | 优先级 |
|
||||||
|
| `link` | VARCHAR | 点击跳转路径 |
|
||||||
|
| `is_read` | BOOLEAN | 已读标记 |
|
||||||
|
| `created_at` | TIMESTAMPTZ | 创建时间 |
|
||||||
|
| `expires_at` | TIMESTAMPTZ | 过期时间 |
|
||||||
|
|
||||||
|
### 通知类型
|
||||||
|
|
||||||
|
| type | 触发场景 | scope |
|
||||||
|
|------|---------|-------|
|
||||||
|
| `system_maintenance` | 系统维护 | `all` |
|
||||||
|
| `version_update` | 版本更新 | `all` |
|
||||||
|
| `payment_success` | 支付成功 | `user` |
|
||||||
|
| `member_expiry_soon` | 会员即将到期 | `user` |
|
||||||
|
| `member_expired` | 会员已过期 | `user` |
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### 用户接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/api/notifications` | 通知列表(分页) |
|
||||||
|
| PUT | `/api/notifications/{id}/read` | 标记已读 |
|
||||||
|
| PUT | `/api/notifications/read-all` | 全部已读 |
|
||||||
|
| GET | `/api/notifications/unread-count` | 未读数量 |
|
||||||
|
|
||||||
|
### 管理员接口
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/admin/notifications` | 创建通知(需管理员 JWT) |
|
||||||
|
| DELETE | `/api/admin/notifications/{id}` | 删除通知 |
|
||||||
|
|
||||||
|
## 命令行工具
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 系统广播
|
||||||
|
./scripts/send-notification.sh \
|
||||||
|
--all \
|
||||||
|
--type system_maintenance \
|
||||||
|
--title "系统维护通知" \
|
||||||
|
--content "将于今晚 22:00 进行系统维护,预计耗时 1 小时" \
|
||||||
|
--priority high
|
||||||
|
|
||||||
|
# 定向通知
|
||||||
|
./scripts/send-notification.sh \
|
||||||
|
--user 1 \
|
||||||
|
--type payment_success \
|
||||||
|
--title "会员开通成功" \
|
||||||
|
--content "您已成功开通包月会员" \
|
||||||
|
--link "/pkg-extra/upgrade/upgrade" \
|
||||||
|
--dev
|
||||||
|
|
||||||
|
# 预览(不执行)
|
||||||
|
./scripts/send-notification.sh --all --type version_update --title "测试" --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
|
||||||
|
| 参数 | 必填 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--all` | 是* | 发送给所有用户 |
|
||||||
|
| `--user <id>` | 是* | 发送给指定用户(二选一) |
|
||||||
|
| `--type <type>` | 是 | 通知类型 |
|
||||||
|
| `--title <text>` | 是 | 标题 |
|
||||||
|
| `--content <text>` | 否 | 正文 |
|
||||||
|
| `--priority <lvl>` | 否 | 默认 `normal` |
|
||||||
|
| `--link <path>` | 否 | 跳转路径 |
|
||||||
|
| `--dev` | 否 | 操作开发库 |
|
||||||
|
| `--dry-run` | 否 | 预览不执行 |
|
||||||
|
|
||||||
|
## 后续规划
|
||||||
|
|
||||||
|
此功能将在后续集成到网页管理端,管理员可通过后台界面:
|
||||||
|
|
||||||
|
1. 查看通知列表
|
||||||
|
2. 创建系统公告(选择类型、填写内容、设置优先级)
|
||||||
|
3. 发送个人通知(选择用户)
|
||||||
|
4. 管理已有通知(编辑、删除)
|
||||||
|
5. 查看发送记录
|
||||||
55
scripts/backup-db.sh
Executable file
55
scripts/backup-db.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 数据库备份脚本
|
||||||
|
# 用途: 备份 PostgreSQL 数据库到本地文件
|
||||||
|
# 建议: 每天通过 cron 执行一次
|
||||||
|
# Crontab 示例:
|
||||||
|
# 0 4 * * * /root/rust/rust_backend/scripts/backup-db.sh >> /root/rust/rust_backend/logs/backup.log 2>&1
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_CONTAINER="1Panel-postgresql-FtMo"
|
||||||
|
DB_NAME="${1:-milkydata}"
|
||||||
|
DB_USER="milkydata"
|
||||||
|
BACKUP_DIR="/root/rust/rust_backend/backups"
|
||||||
|
RETENTION_DAYS=7
|
||||||
|
|
||||||
|
LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')]"
|
||||||
|
|
||||||
|
echo "$LOG_PREFIX 开始备份数据库: ${DB_NAME}"
|
||||||
|
|
||||||
|
# 确保备份目录存在
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# 检查容器是否运行
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then
|
||||||
|
echo "$LOG_PREFIX 错误: Docker 容器 ${DB_CONTAINER} 未运行"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 执行备份(自定义格式,可压缩)
|
||||||
|
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_$(date +%Y%m%d_%H%M%S).dump"
|
||||||
|
|
||||||
|
echo "$LOG_PREFIX 导出到: ${BACKUP_FILE}"
|
||||||
|
docker exec "$DB_CONTAINER" pg_dump -U "$DB_USER" -Fc "$DB_NAME" > "$BACKUP_FILE"
|
||||||
|
|
||||||
|
# 检查备份是否成功
|
||||||
|
if [ -f "$BACKUP_FILE" ] && [ -s "$BACKUP_FILE" ]; then
|
||||||
|
FILE_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
|
||||||
|
echo "$LOG_PREFIX 备份成功: ${BACKUP_FILE} (${FILE_SIZE})"
|
||||||
|
else
|
||||||
|
echo "$LOG_PREFIX 错误: 备份文件为空或不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 删除旧备份(保留 RETENTION_DAYS 天)
|
||||||
|
echo "$LOG_PREFIX 清理 ${RETENTION_DAYS} 天前的旧备份..."
|
||||||
|
find "$BACKUP_DIR" -name "${DB_NAME}_*.dump" -mtime "+${RETENTION_DAYS}" -delete
|
||||||
|
|
||||||
|
# 记录保留的备份
|
||||||
|
echo "$LOG_PREFIX 当前备份列表:"
|
||||||
|
ls -lh "$BACKUP_DIR"/*.dump 2>/dev/null || echo " (无备份文件)"
|
||||||
|
|
||||||
|
echo "$LOG_PREFIX 备份完成"
|
||||||
35
scripts/cleanup_refresh_tokens.sh
Normal file
35
scripts/cleanup_refresh_tokens.sh
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 清理过期的 refresh_token
|
||||||
|
# 用途: 删除 refresh_tokens 表中已过期的记录
|
||||||
|
# 建议: 通过 cron 每天执行一次
|
||||||
|
# Crontab 示例:
|
||||||
|
# 0 3 * * * /root/rust/rust_backend/scripts/cleanup_refresh_tokens.sh >> /root/rust/rust_backend/logs/cleanup.log 2>&1
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DB_CONTAINER="1Panel-postgresql-FtMo"
|
||||||
|
DB_NAME="${1:-milkydata}"
|
||||||
|
DB_USER="milkydata"
|
||||||
|
|
||||||
|
LOG_PREFIX="[$(date '+%Y-%m-%d %H:%M:%S')]"
|
||||||
|
|
||||||
|
echo "$LOG_PREFIX 开始清理过期的 refresh_token..."
|
||||||
|
|
||||||
|
# 检查表是否存在
|
||||||
|
table_exists=$(docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -tAc \
|
||||||
|
"SELECT 1 FROM information_schema.tables WHERE table_name = 'refresh_tokens'" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
if [ "$table_exists" != "1" ]; then
|
||||||
|
echo "$LOG_PREFIX 表 refresh_tokens 不存在,跳过清理"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 删除过期记录
|
||||||
|
deleted=$(docker exec "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -tAc \
|
||||||
|
"DELETE FROM refresh_tokens WHERE expires_at <= NOW(); SELECT COUNT(*);" 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
echo "$LOG_PREFIX 已删除 ${deleted} 条过期的 refresh_token"
|
||||||
|
echo "$LOG_PREFIX 清理完成"
|
||||||
141
scripts/send-notification.sh
Executable file
141
scripts/send-notification.sh
Executable file
@@ -0,0 +1,141 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ===========================================
|
||||||
|
# 发送通知 - 管理员工具
|
||||||
|
# 通过 API 创建系统广播或定向通知
|
||||||
|
# ===========================================
|
||||||
|
# 用法:
|
||||||
|
# ./scripts/send-notification.sh --all --type system_maintenance --title "维护通知" --content "内容"
|
||||||
|
# ./scripts/send-notification.sh --user 1 --type payment_success --title "开通成功" --link /upgrade
|
||||||
|
#
|
||||||
|
# 选项:
|
||||||
|
# --all 发送给所有用户(scope=all)
|
||||||
|
# --user <id> 发送给指定用户(scope=user)
|
||||||
|
# --type <type> 通知类型(必填)
|
||||||
|
# --title <text> 通知标题(必填)
|
||||||
|
# --content <text> 通知内容(可选)
|
||||||
|
# --priority <lvl> 优先级: high/normal/low(默认 normal)
|
||||||
|
# --link <url> 点击跳转路径(可选)
|
||||||
|
# --dev 目标: milkydata_dev(默认 milkydata)
|
||||||
|
# --dry-run 预览不执行
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
ENV_FILE="$PROJECT_ROOT/.env"
|
||||||
|
|
||||||
|
# ---------- 默认值 ----------
|
||||||
|
SCOPE=""
|
||||||
|
USER_ID=""
|
||||||
|
NOTIF_TYPE=""
|
||||||
|
TITLE=""
|
||||||
|
CONTENT=""
|
||||||
|
PRIORITY="normal"
|
||||||
|
LINK=""
|
||||||
|
DB_NAME="milkydata"
|
||||||
|
DRY_RUN=false
|
||||||
|
|
||||||
|
# ---------- 获取 SSH 主机 ----------
|
||||||
|
get_ssh_host() {
|
||||||
|
grep '^SSH_SERVER=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 使用说明 ----------
|
||||||
|
usage() {
|
||||||
|
echo "用法: $0 (--all | --user <id>) --type <type> --title <title> [options]"
|
||||||
|
echo ""
|
||||||
|
echo "必填:"
|
||||||
|
echo " --all 发送给所有用户"
|
||||||
|
echo " --user <id> 发送给指定用户"
|
||||||
|
echo " --type <type> 通知类型: system_maintenance / version_update /"
|
||||||
|
echo " payment_success / member_expiry_soon / member_expired"
|
||||||
|
echo " --title <text> 通知标题"
|
||||||
|
echo ""
|
||||||
|
echo "可选:"
|
||||||
|
echo " --content <text> 通知内容"
|
||||||
|
echo " --priority <lvl> high / normal / low(默认 normal)"
|
||||||
|
echo " --link <path> 点击跳转路径(如 /pkg-extra/upgrade/upgrade)"
|
||||||
|
echo " --dev 操作开发库 milkydata_dev"
|
||||||
|
echo " --dry-run 预览,不执行"
|
||||||
|
echo ""
|
||||||
|
echo "示例:"
|
||||||
|
echo " $0 --all --type system_maintenance --title '维护通知' --content '今晚维护' --priority high"
|
||||||
|
echo " $0 --user 1 --type payment_success --title '开通成功' --link /upgrade --dev"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 参数解析 ----------
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--all) SCOPE="all" ;;
|
||||||
|
--user) SCOPE="user"; USER_ID="$2"; shift ;;
|
||||||
|
--type) NOTIF_TYPE="$2"; shift ;;
|
||||||
|
--title) TITLE="$2"; shift ;;
|
||||||
|
--content) CONTENT="$2"; shift ;;
|
||||||
|
--priority) PRIORITY="$2"; shift ;;
|
||||||
|
--link) LINK="$2"; shift ;;
|
||||||
|
--dev) DB_NAME="milkydata_dev" ;;
|
||||||
|
--dry-run) DRY_RUN=true ;;
|
||||||
|
--help|-h) usage ;;
|
||||||
|
*) echo "未知选项: $1"; usage ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------- 必填校验 ----------
|
||||||
|
[ -z "$SCOPE" ] && { echo "错误: 必须指定 --all 或 --user"; usage; }
|
||||||
|
[ "$SCOPE" = "user" ] && [ -z "$USER_ID" ] && { echo "错误: --user 必须指定用户 ID"; usage; }
|
||||||
|
[ -z "$NOTIF_TYPE" ] && { echo "错误: --type 必填"; usage; }
|
||||||
|
[ -z "$TITLE" ] && { echo "错误: --title 必填"; usage; }
|
||||||
|
|
||||||
|
# ---------- 连接信息 ----------
|
||||||
|
SSH_HOST=$(get_ssh_host)
|
||||||
|
[ -z "$SSH_HOST" ] && { echo "错误: 未配置 SSH_SERVER"; exit 1; }
|
||||||
|
|
||||||
|
DB_CONTAINER="1Panel-postgresql-FtMo"
|
||||||
|
DB_USER="milkydata"
|
||||||
|
|
||||||
|
# ---------- 构建 SQL ----------
|
||||||
|
USER_VAL="NULL"
|
||||||
|
[ "$SCOPE" = "user" ] && USER_VAL="$USER_ID"
|
||||||
|
|
||||||
|
CONTENT_VAL="NULL"
|
||||||
|
[ -n "$CONTENT" ] && CONTENT_VAL="'${CONTENT//\'/\'\'}'"
|
||||||
|
|
||||||
|
LINK_VAL="NULL"
|
||||||
|
[ -n "$LINK" ] && LINK_VAL="'${LINK//\'/\'\'}'"
|
||||||
|
|
||||||
|
SQL="INSERT INTO notifications (scope, user_id, type, title, content, priority, link)
|
||||||
|
VALUES ('${SCOPE}', ${USER_VAL}, '${NOTIF_TYPE}', '${TITLE//\'/\'\'}', ${CONTENT_VAL}, '${PRIORITY}', ${LINK_VAL});"
|
||||||
|
|
||||||
|
# ---------- 确认信息 ----------
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════"
|
||||||
|
echo " 操作: 发送通知"
|
||||||
|
echo " 范围: ${SCOPE}"
|
||||||
|
echo " 类型: ${NOTIF_TYPE}"
|
||||||
|
echo " 标题: ${TITLE}"
|
||||||
|
echo " 数据库: ${DB_NAME}(${SSH_HOST})"
|
||||||
|
echo "═══════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
echo "将要执行:"
|
||||||
|
echo " ${SQL}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
[ "$DRY_RUN" = true ] && { echo "预览模式,未执行。"; exit 0; }
|
||||||
|
|
||||||
|
read -p "确认发送? [y/N] " -n 1 -r; echo
|
||||||
|
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "已取消"; exit 0; }
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "执行中..."
|
||||||
|
ssh "$SSH_HOST" "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${SQL}\"" && echo "✅ 通知已发送" || echo "❌ 发送失败"
|
||||||
|
|
||||||
|
# 验证
|
||||||
|
echo ""
|
||||||
|
echo "最近通知:"
|
||||||
|
VERIFY_SQL="SELECT id, scope, user_id, type, title, priority, created_at::date
|
||||||
|
FROM notifications ORDER BY id DESC LIMIT 5;"
|
||||||
|
ssh "$SSH_HOST" "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${VERIFY_SQL}\""
|
||||||
|
echo ""
|
||||||
102
scripts/set-paid-user.sh
Executable file
102
scripts/set-paid-user.sh
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ===========================================
|
||||||
|
# 通过 SSH 在服务器上执行 SQL,设置指定用户的付费状态
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||||
|
ENV_FILE="$PROJECT_ROOT/.env"
|
||||||
|
|
||||||
|
get_ssh_host() {
|
||||||
|
grep '^SSH_SERVER=' "$ENV_FILE" 2>/dev/null | head -1 | cut -d'=' -f2- | tr -d '"' | tr -d "'" | xargs
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "用法: $0 <user_id> [--permanent|--expires YYYY-MM-DD|--cancel]"
|
||||||
|
echo ""
|
||||||
|
echo " user_id 用户 ID(必填)"
|
||||||
|
echo " --monthly 设为月会员(30天)
|
||||||
|
--permanent 设为永久会员(is_member=true, membership_expires_at=NULL)"
|
||||||
|
echo " --expires DATE 设为指定日期到期(格式: YYYY-MM-DD)"
|
||||||
|
echo " --cancel 取消付费状态(is_member=false, membership_expires_at=NULL)"
|
||||||
|
echo ""
|
||||||
|
echo "示例:"
|
||||||
|
echo " $0 1 --permanent # 用户 1 永久会员"
|
||||||
|
echo " $0 1 --expires 2027-12-31 # 2027-12-31 到期"
|
||||||
|
echo " $0 1 --cancel # 取消付费"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 参数解析 ----------
|
||||||
|
USER_ID=""; ACTION=""; EXPIRES_DATE=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--permanent) ACTION="permanent" ;;
|
||||||
|
--expires) ACTION="expires"; EXPIRES_DATE="$2"; shift ;;
|
||||||
|
--monthly) ACTION="monthly" ;;
|
||||||
|
--cancel) ACTION="cancel" ;;
|
||||||
|
--dev) DB_NAME="milkydata_dev" ;;
|
||||||
|
--help|-h) usage ;;
|
||||||
|
*) USER_ID="$1" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
[ -z "$USER_ID" ] && usage
|
||||||
|
[ -z "$ACTION" ] && usage
|
||||||
|
|
||||||
|
# ---------- 连接信息 ----------
|
||||||
|
SSH_HOST=$(get_ssh_host)
|
||||||
|
[ -z "$SSH_HOST" ] && { echo "错误: 未配置 SSH_SERVER"; exit 1; }
|
||||||
|
|
||||||
|
DB_CONTAINER="1Panel-postgresql-FtMo"
|
||||||
|
DB_USER="milkydata"
|
||||||
|
|
||||||
|
# ---------- 构建 SQL ----------
|
||||||
|
case "$ACTION" in
|
||||||
|
permanent)
|
||||||
|
SQL="UPDATE users SET is_member = true, membership_expires_at = NULL WHERE id = ${USER_ID};"
|
||||||
|
DESC="永久会员"
|
||||||
|
;;
|
||||||
|
monthly)
|
||||||
|
SQL="UPDATE users SET is_member = true, membership_expires_at = NOW() + INTERVAL '30 days' WHERE id = ${USER_ID};"
|
||||||
|
DESC="月会员(30天)"
|
||||||
|
;;
|
||||||
|
expires)
|
||||||
|
SQL="UPDATE users SET is_member = true, membership_expires_at = '${EXPIRES_DATE}' WHERE id = ${USER_ID};"
|
||||||
|
DESC="到期: ${EXPIRES_DATE}"
|
||||||
|
;;
|
||||||
|
cancel)
|
||||||
|
SQL="UPDATE users SET is_member = false, membership_expires_at = NULL WHERE id = ${USER_ID};"
|
||||||
|
DESC="取消付费"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "═══════════════════════════════════════"
|
||||||
|
echo " 操作: 用户 ${USER_ID} → ${DESC}"
|
||||||
|
echo " 数据库: ${DB_NAME}(${SSH_HOST})"
|
||||||
|
echo "═══════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
echo "当前状态:"
|
||||||
|
CURRENT_SQL="SELECT id, name, is_member, membership_expires_at FROM users WHERE id = ${USER_ID};"
|
||||||
|
ssh "$SSH_HOST" "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${CURRENT_SQL}\""
|
||||||
|
echo ""
|
||||||
|
echo "将要执行:"
|
||||||
|
echo " ${SQL}"
|
||||||
|
echo ""
|
||||||
|
read -p "确认执行? [y/N] " -n 1 -r; echo
|
||||||
|
[[ ! $REPLY =~ ^[Yy]$ ]] && { echo "已取消"; exit 0; }
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "执行中..."
|
||||||
|
ssh "$SSH_HOST" "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${SQL}\"" && echo "✅ 操作成功" || echo "❌ 操作失败"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "验证结果:"
|
||||||
|
VERIFY_SQL="SELECT id, name, phone, type, is_member, is_admin, membership_expires_at FROM users WHERE id = ${USER_ID};"
|
||||||
|
ssh "$SSH_HOST" "docker exec ${DB_CONTAINER} psql -U ${DB_USER} -d ${DB_NAME} -c \"${VERIFY_SQL}\""
|
||||||
|
echo ""
|
||||||
Reference in New Issue
Block a user