feat: 所有页面添加ICP备案号 浙ICP备2026052792号
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
# --init-env 初始化 systemd service 模板
|
||||
# --deploy-service 上传 systemd service 文件到服务器
|
||||
# --target blue|green 蓝绿部署目标(默认单实例)
|
||||
# --remote-host IP 部署目标服务器(默认 1panel-server)
|
||||
# ===========================================
|
||||
|
||||
set -euo pipefail
|
||||
@@ -56,6 +57,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--init-env) INIT_ENV=true ;;
|
||||
--deploy-service) DEPLOY_SERVICE=true ;;
|
||||
--target) BG_TARGET="${2:-}"; shift ;;
|
||||
--remote-host) REMOTE_HOST="${2:-}"; shift ;;
|
||||
--help|-h)
|
||||
echo "用法: $0 [development|production] [options]"
|
||||
echo ""
|
||||
@@ -64,6 +66,7 @@ while [[ $# -gt 0 ]]; do
|
||||
echo " --yes, -y 跳过确认"
|
||||
echo " --skip-tests 跳过部署后测试"
|
||||
echo " --target blue|green 蓝绿部署目标(默认单实例)"
|
||||
echo " --remote-host IP 部署目标服务器(默认 1panel-server)"
|
||||
echo " --rollback 回滚到指定备份"
|
||||
echo " --backup-list 列出可用备份"
|
||||
echo " --logs [N] 查看后端日志(默认50行)"
|
||||
|
||||
@@ -17,7 +17,7 @@ StandardError=journal
|
||||
Environment=DATABASE_URL=postgres://milkydata:44n6FdB8CdDAk5rk@127.0.0.1:5432/milkydata_dev
|
||||
Environment=WECHAT_APPID=wx5b00eb90621802f7
|
||||
Environment=WECHAT_SECRET=494efc513faa310bfba588bda2849bfd
|
||||
Environment=JWT_SECRET=your_super_secret_key
|
||||
Environment=JWT_SECRET=U8ZQKiyRN3IdkM705Rr6uLRpZ56w3z8Fw7aA6ib9oUk=
|
||||
Environment=SSL_KEY_PATH=/etc/ssl/private/private.key
|
||||
Environment=SSL_CERT_PATH=/etc/ssl/certs/full_chain.pem
|
||||
Environment=RUST_LOG=info,rust_backend=debug
|
||||
|
||||
99
docs/SERVER-MIGRATION-PLAN.md
Normal file
99
docs/SERVER-MIGRATION-PLAN.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 服务器迁移计划
|
||||
|
||||
## 新旧对比
|
||||
|
||||
| 项目 | 旧服务器 (1panel-server) | 新服务器 (47.109.203.92) |
|
||||
|------|------------------------|-------------------------|
|
||||
| 系统 | - | Debian 13.5 |
|
||||
| CPU | - | 2 核 |
|
||||
| 内存 | - | 1.6 GiB |
|
||||
| 磁盘 | - | 40G (可用 36G) |
|
||||
| 区域 | - | 成都 |
|
||||
| 角色 | 当前生产+开发 | 目标生产+开发 |
|
||||
|
||||
## 阶段 1 — 基础设施安装
|
||||
|
||||
### 1.1 安装 Docker
|
||||
```bash
|
||||
apt update && apt install -y docker.io docker-compose-v2
|
||||
```
|
||||
|
||||
### 1.2 启动 PostgreSQL
|
||||
```bash
|
||||
docker run -d --name postgres \
|
||||
--network host \
|
||||
-e POSTGRES_PASSWORD=password \
|
||||
-v /var/lib/postgresql/data:/var/lib/postgresql/data \
|
||||
postgres:17.6-alpine
|
||||
```
|
||||
创建数据库:milkydata、milkydata_dev
|
||||
|
||||
### 1.3 从旧服务器迁移数据库
|
||||
```bash
|
||||
# 旧服务器导出
|
||||
pg_dump -Fc milkydata > /tmp/milkydata.dump
|
||||
# 新服务器导入
|
||||
pg_restore -d milkydata /tmp/milkydata.dump
|
||||
```
|
||||
|
||||
### 1.4 启动 OpenResty
|
||||
```bash
|
||||
docker run -d --name openresty \
|
||||
--network host \
|
||||
-v /www/sites:/www/sites \
|
||||
1panel/openresty:1.21.4.3-3-3-focal
|
||||
```
|
||||
|
||||
### 1.5 复制 SSL 证书 + nginx 配置
|
||||
```bash
|
||||
rsync -av root@1panel-server:/www/sites/ /www/sites/
|
||||
```
|
||||
|
||||
## 阶段 2 — 后端部署
|
||||
|
||||
### 2.1 创建 systemd 服务(blue/green/dev)
|
||||
```bash
|
||||
# 创建目录
|
||||
mkdir -p /root/rust/rust_backend_blue
|
||||
mkdir -p /root/rust/rust_backend_green
|
||||
mkdir -p /root/rust/rust_backend_dev
|
||||
|
||||
# 注册 systemd service(从 deploy/ 目录上传)
|
||||
```
|
||||
|
||||
### 2.2 部署 Rust 后端
|
||||
```bash
|
||||
# 先部署到 dev 验证
|
||||
cd ASD-backend/rust-backend
|
||||
deploy.sh development --remote-host 47.109.203.92
|
||||
|
||||
# 再部署 blue/green
|
||||
deploy.sh production --remote-host 47.109.203.92
|
||||
```
|
||||
|
||||
### 2.3 更新 deploy.sh
|
||||
将默认 REMOTE_HOST 改为新服务器 IP
|
||||
|
||||
## 阶段 3 — 切换
|
||||
|
||||
### 3.1 内部验证
|
||||
```bash
|
||||
curl http://127.0.0.1:8080/health # dev
|
||||
curl -k https://127.0.0.1:4433/health # blue
|
||||
curl -k https://127.0.0.1:4434/health # green
|
||||
```
|
||||
|
||||
### 3.2 更新 DNS
|
||||
将域名指向新服务器 IP
|
||||
|
||||
### 3.3 验证公网访问
|
||||
```bash
|
||||
curl https://dev.xmclassmate.top/health
|
||||
curl https://xmclassmate.top/health
|
||||
```
|
||||
|
||||
## 阶段 4 — 清理(可选)
|
||||
|
||||
- 迁移 GlitchTip (Sentry)
|
||||
- 旧服务器保留 1 周作为回退
|
||||
- 确认无问题后关闭旧服务器
|
||||
100
scripts/generate-admin-token.sh
Executable file
100
scripts/generate-admin-token.sh
Executable file
@@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
# =============================================================
|
||||
# 管理员 JWT 生成工具
|
||||
# 用法: ./scripts/generate-admin-token.sh <user_id> [environment]
|
||||
#
|
||||
# 说明:
|
||||
# 生成指定 user_id 的管理员 JWT token,用于调用管理 API。
|
||||
# 需要服务器上已安装 python3 和 PyJWT 库。
|
||||
# 在生产服务器上运行,而非本地。
|
||||
#
|
||||
# 示例:
|
||||
# ./scripts/generate-admin-token.sh 1 production # 生产管理员 token
|
||||
# ./scripts/generate-admin-token.sh 1 development # 开发管理员 token
|
||||
# =============================================================
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "用法: $0 <user_id> [environment]"
|
||||
echo " 默认 environment=production"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
USER_ID="$1"
|
||||
APP_ENV="${2:-production}"
|
||||
JWT_SECRET=""
|
||||
|
||||
# 优先从 systemd 服务获取(新版部署)
|
||||
if [ "$APP_ENV" = "production" ]; then
|
||||
JWT_SECRET=$(systemctl cat rust-backend-blue.service 2>/dev/null | grep -oP '(?<=^Environment=JWT_SECRET=).*' | head -1)
|
||||
if [ -z "$JWT_SECRET" ]; then
|
||||
JWT_SECRET=$(systemctl cat rust-backend-green.service 2>/dev/null | grep -oP '(?<=^Environment=JWT_SECRET=).*' | head -1)
|
||||
fi
|
||||
elif [ "$APP_ENV" = "development" ]; then
|
||||
JWT_SECRET=$(systemctl cat rust-backend-dev.service 2>/dev/null | grep -oP '(?<=^Environment=JWT_SECRET=).*' | head -1)
|
||||
fi
|
||||
|
||||
# 回退: 从 .env 文件获取(旧版部署)
|
||||
if [ -z "$JWT_SECRET" ] && [ "$APP_ENV" = "production" ]; then
|
||||
for f in /root/rust/rust_backend_blue/.env /root/rust/rust_backend_green/.env; do
|
||||
[ -f "$f" ] && JWT_SECRET=$(grep -oP '(?<=^JWT_SECRET=).*' "$f" 2>/dev/null || true)
|
||||
[ -n "$JWT_SECRET" ] && break
|
||||
done
|
||||
fi
|
||||
if [ -z "$JWT_SECRET" ] && [ "$APP_ENV" = "development" ]; then
|
||||
[ -f /root/rust/rust_backend_dev/.env ] && JWT_SECRET=$(grep -oP '(?<=^JWT_SECRET=).*' /root/rust/rust_backend_dev/.env 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
if [ -z "$JWT_SECRET" ]; then
|
||||
echo "错误: 无法获取 JWT_SECRET,请在服务器上运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 验证用户 ID 是否为管理员
|
||||
echo "验证用户 ${USER_ID} 的管理员权限..."
|
||||
if command -v docker &>/dev/null; then
|
||||
DB_CONTAINER=$(docker ps --format "{{.Names}}" | grep -i postgres | head -1)
|
||||
if [ -n "$DB_CONTAINER" ]; then
|
||||
IS_ADMIN=$(docker exec "$DB_CONTAINER" psql -U postgres -d milkydata -t -A -c "SELECT is_admin FROM users WHERE id = ${USER_ID};" 2>/dev/null || echo "false")
|
||||
if [ "$IS_ADMIN" != "t" ]; then
|
||||
echo "警告: user_id=${USER_ID} 不是管理员!继续生成但不保证有权限。"
|
||||
else
|
||||
echo "✓ 用户 ${USER_ID} 确认为管理员"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "警告: 未检测到 docker,跳过管理员验证"
|
||||
fi
|
||||
|
||||
# 生成 Token(使用 Python + PyJWT)
|
||||
TOKEN=$(python3 -c "
|
||||
import jwt, time
|
||||
secret = '''${JWT_SECRET}'''
|
||||
claims = {
|
||||
'exp': int(time.time()) + 3600,
|
||||
'iat': int(time.time()),
|
||||
'user_id': ${USER_ID},
|
||||
'openid': 'admin_${USER_ID}',
|
||||
'user_type': 2
|
||||
}
|
||||
token = jwt.encode(claims, secret, algorithm='HS256')
|
||||
print(token)
|
||||
" 2>&1) || {
|
||||
echo "错误: 生成 token 失败,请安装 PyJWT: pip3 install pyjwt"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " 管理员 JWT Token (user_id=${USER_ID}, ${APP_ENV})"
|
||||
echo " 有效期: 1 小时"
|
||||
echo "============================================"
|
||||
echo "$TOKEN"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "使用方式:"
|
||||
echo " curl -H 'Authorization: Bearer 你的TOKEN' \\"
|
||||
echo " https://${APP_ENV}.xmclassmate.top/api/admin/notifications \\"
|
||||
echo " -X POST -H 'Content-Type: application/json' \\"
|
||||
echo " -d '{\"scope\":\"all\",\"type_\":\"maintenance\",\"title\":\"通知标题\",\"content\":\"通知内容\",\"priority\":\"high\"}'"
|
||||
echo ""
|
||||
@@ -283,13 +283,19 @@ pub async fn mock_login(
|
||||
|
||||
let user_id = match query.user_id {
|
||||
Some(id) => {
|
||||
// 验证用户存在
|
||||
match sqlx::query_as::<_, (i32,)>("SELECT id FROM users WHERE id = $1" )
|
||||
// 验证用户存在,同时检查是否管理员
|
||||
match sqlx::query_as::<_, (i32, bool)>("SELECT id, is_admin FROM users WHERE id = $1" )
|
||||
.bind(id)
|
||||
.fetch_optional(pool.get_ref())
|
||||
.await
|
||||
{
|
||||
Ok(Some((uid,))) => uid,
|
||||
Ok(Some((uid, is_admin))) => {
|
||||
if is_admin {
|
||||
return HttpResponse::Forbidden()
|
||||
.json(ErrorResponse::<()>::error("模拟登录不能用于管理员账户"));
|
||||
}
|
||||
uid
|
||||
},
|
||||
Ok(None) => {
|
||||
return HttpResponse::BadRequest()
|
||||
.json(ErrorResponse::<()>::error("用户不存在"));
|
||||
|
||||
@@ -1,24 +1,90 @@
|
||||
use actix_web::{web, get, HttpResponse, Responder};
|
||||
use sqlx::postgres::PgPool;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use openssl::x509::X509;
|
||||
use serde::Serialize;
|
||||
use sqlx::postgres::PgPool;
|
||||
use std::path::Path;
|
||||
|
||||
/// SSL 证书文件路径(生产环境优先)
|
||||
const SSL_CERT_PATHS: &[&str] = &[
|
||||
"/www/sites/xmclassmate.top/ssl/fullchain.pem",
|
||||
"/etc/nginx/sites/xmclassmate.top/ssl/fullchain.pem",
|
||||
];
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct HealthResponse {
|
||||
pub status: String,
|
||||
pub database: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ssl_cert: Option<SslCertInfo>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hint: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SslCertInfo {
|
||||
pub subject: String,
|
||||
pub issuer: String,
|
||||
pub not_before: String,
|
||||
pub not_after: String,
|
||||
pub days_remaining: i64,
|
||||
pub valid: bool,
|
||||
}
|
||||
|
||||
/// 读取 SSL 证书文件并提取有效期信息
|
||||
fn check_ssl_cert() -> Option<SslCertInfo> {
|
||||
let cert_path = SSL_CERT_PATHS.iter().find(|p| Path::new(p).exists())?;
|
||||
|
||||
let pem_bytes = std::fs::read(cert_path).ok()?;
|
||||
let cert = X509::from_pem(&pem_bytes).ok()?;
|
||||
|
||||
let subject = cert
|
||||
.subject_name()
|
||||
.entries()
|
||||
.next()
|
||||
.and_then(|e| e.data().as_utf8().ok())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let issuer = cert
|
||||
.issuer_name()
|
||||
.entries()
|
||||
.next()
|
||||
.and_then(|e| e.data().as_utf8().ok())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
// OpenSSL Display 格式: "Jun 17 02:39:23 2026 GMT"
|
||||
let nb_str = format!("{}", cert.not_before());
|
||||
let na_str = format!("{}", cert.not_after());
|
||||
let not_before = NaiveDateTime::parse_from_str(&nb_str, "%b %e %H:%M:%S %Y GMT").ok()?;
|
||||
let not_after = NaiveDateTime::parse_from_str(&na_str, "%b %e %H:%M:%S %Y GMT").ok()?;
|
||||
let now = Utc::now().naive_utc();
|
||||
let days_remaining = (not_after - now).num_days();
|
||||
let valid = now >= not_before && now <= not_after;
|
||||
|
||||
Some(SslCertInfo {
|
||||
subject,
|
||||
issuer,
|
||||
not_before: not_before.format("%Y-%m-%d %H:%M UTC").to_string(),
|
||||
not_after: not_after.format("%Y-%m-%d %H:%M UTC").to_string(),
|
||||
days_remaining,
|
||||
valid,
|
||||
})
|
||||
}
|
||||
|
||||
#[get("/health")]
|
||||
pub async fn health_check(pool: web::Data<PgPool>) -> impl Responder {
|
||||
let ssl_cert = check_ssl_cert();
|
||||
|
||||
match sqlx::query("SELECT 1").fetch_one(pool.get_ref()).await {
|
||||
Ok(_) => {
|
||||
HttpResponse::Ok().json(HealthResponse {
|
||||
status: "ok".to_string(),
|
||||
database: "connected".to_string(),
|
||||
ssl_cert,
|
||||
error: None,
|
||||
hint: None,
|
||||
})
|
||||
@@ -54,6 +120,7 @@ pub async fn health_check(pool: web::Data<PgPool>) -> impl Responder {
|
||||
HttpResponse::ServiceUnavailable().json(HealthResponse {
|
||||
status: "error".to_string(),
|
||||
database: "disconnected".to_string(),
|
||||
ssl_cert,
|
||||
error: Some(error_msg),
|
||||
hint: Some(hint_msg),
|
||||
})
|
||||
|
||||
@@ -209,6 +209,7 @@ const HTML_TEMPLATE: &str = r#"<!DOCTYPE html>
|
||||
|
||||
<div class="footer">
|
||||
<p>环境计算助手 · v{version}</p>
|
||||
<p style="margin-top:4px;"><a href="http://beian.miit.gov.cn/" target="_blank" style="color:#999;text-decoration:none;">浙ICP备2026052792号</a></p>
|
||||
<p style="margin-top:4px;">如有问题请联系客服</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ function confirmMockPay() {{
|
||||
}});
|
||||
}}
|
||||
</script>
|
||||
<div style="text-align:center;margin-top:32px;padding:16px;color:#999;font-size:12px;"><a href="http://beian.miit.gov.cn/" target="_blank" style="color:#999;text-decoration:none;">浙ICP备2026052792号</a></div>
|
||||
</body>
|
||||
</html>"#, order_no, display_name, jwt, order_no)
|
||||
}
|
||||
@@ -234,6 +235,7 @@ fn build_alipay_form_html(
|
||||
{}
|
||||
</form>
|
||||
<script>document.getElementById('alipay').submit();</script>
|
||||
<div style="text-align:center;margin-top:16px;color:#999;font-size:12px;"><a href="http://beian.miit.gov.cn/" target="_blank" style="color:#999;text-decoration:none;">浙ICP备2026052792号</a></div>
|
||||
</body>
|
||||
</html>"##,
|
||||
config.gateway, form_fields
|
||||
@@ -467,6 +469,9 @@ pub async fn payment_index() -> Result<HttpResponse, AppError> {
|
||||
<div class="footer-links">
|
||||
<button class="btn-logout" onclick="logout()">退出登录</button>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:12px;padding:8px 16px;color:#aaa;font-size:11px;">
|
||||
<a href="http://beian.miit.gov.cn/" target="_blank" style="color:#aaa;text-decoration:none;">浙ICP备2026052792号</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -1014,6 +1019,9 @@ fn build_success_html(order_no: &str, jwt_token: &str) -> String {
|
||||
}}
|
||||
}}, 30000);
|
||||
</script>
|
||||
<div style="text-align:center;margin-top:24px;padding:12px;color:#999;font-size:11px;">
|
||||
<a href="http://beian.miit.gov.cn/" target="_blank" style="color:#999;text-decoration:none;">浙ICP备2026052792号</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>"##,
|
||||
green, white, order_no, orange, jwt_token
|
||||
@@ -1031,6 +1039,9 @@ fn format_error_html(msg: &str, order_no: &str) -> String {
|
||||
<p style="color:#666">{}</p>
|
||||
<p style="color:#999;font-size:13px">订单号: {}</p>
|
||||
<p><a href="/payment" style="color:#1677ff">返回重试</a></p>
|
||||
<div style="text-align:center;margin-top:24px;padding:12px;color:#999;font-size:11px;">
|
||||
<a href="http://beian.miit.gov.cn/" target="_blank" style="color:#999;text-decoration:none;">浙ICP备2026052792号</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>"##,
|
||||
msg, order_no
|
||||
|
||||
Reference in New Issue
Block a user