From d03acf0adda6f830aeefe2af8e8adb736e8b1aa3 Mon Sep 17 00:00:00 2001 From: Milky0217 Date: Tue, 26 May 2026 15:20:00 +0800 Subject: [PATCH] temp --- .gitignore | 2 - docs/CDN-SETUP.md | 425 ++++++++++++++++++++++++++++++ docs/NOTIFICATIONS.md | 98 +++++++ scripts/backup-db.sh | 55 ++++ scripts/cleanup_refresh_tokens.sh | 35 +++ scripts/send-notification.sh | 141 ++++++++++ scripts/set-paid-user.sh | 102 +++++++ 7 files changed, 856 insertions(+), 2 deletions(-) create mode 100644 docs/CDN-SETUP.md create mode 100644 docs/NOTIFICATIONS.md create mode 100755 scripts/backup-db.sh create mode 100644 scripts/cleanup_refresh_tokens.sh create mode 100755 scripts/send-notification.sh create mode 100755 scripts/set-paid-user.sh diff --git a/.gitignore b/.gitignore index 2bccce0..8f17979 100644 --- a/.gitignore +++ b/.gitignore @@ -18,10 +18,8 @@ logs/ .sisyphus/ # Docs -docs/ # Scripts -scripts/ # Parent directory reference (if accidentally included) ASD-backend/ diff --git a/docs/CDN-SETUP.md b/docs/CDN-SETUP.md new file mode 100644 index 0000000..2d11e29 --- /dev/null +++ b/docs/CDN-SETUP.md @@ -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/) diff --git a/docs/NOTIFICATIONS.md b/docs/NOTIFICATIONS.md new file mode 100644 index 0000000..9ad4760 --- /dev/null +++ b/docs/NOTIFICATIONS.md @@ -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 ` | 是* | 发送给指定用户(二选一) | +| `--type ` | 是 | 通知类型 | +| `--title ` | 是 | 标题 | +| `--content ` | 否 | 正文 | +| `--priority ` | 否 | 默认 `normal` | +| `--link ` | 否 | 跳转路径 | +| `--dev` | 否 | 操作开发库 | +| `--dry-run` | 否 | 预览不执行 | + +## 后续规划 + +此功能将在后续集成到网页管理端,管理员可通过后台界面: + +1. 查看通知列表 +2. 创建系统公告(选择类型、填写内容、设置优先级) +3. 发送个人通知(选择用户) +4. 管理已有通知(编辑、删除) +5. 查看发送记录 diff --git a/scripts/backup-db.sh b/scripts/backup-db.sh new file mode 100755 index 0000000..b7d19ae --- /dev/null +++ b/scripts/backup-db.sh @@ -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 备份完成" diff --git a/scripts/cleanup_refresh_tokens.sh b/scripts/cleanup_refresh_tokens.sh new file mode 100644 index 0000000..1a421f0 --- /dev/null +++ b/scripts/cleanup_refresh_tokens.sh @@ -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 清理完成" diff --git a/scripts/send-notification.sh b/scripts/send-notification.sh new file mode 100755 index 0000000..5b9c6cb --- /dev/null +++ b/scripts/send-notification.sh @@ -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 发送给指定用户(scope=user) +# --type 通知类型(必填) +# --title 通知标题(必填) +# --content 通知内容(可选) +# --priority 优先级: high/normal/low(默认 normal) +# --link 点击跳转路径(可选) +# --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 ) --type --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 "" diff --git a/scripts/set-paid-user.sh b/scripts/set-paid-user.sh new file mode 100755 index 0000000..a04193b --- /dev/null +++ b/scripts/set-paid-user.sh @@ -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 ""