Compare commits

...

7 Commits

Author SHA1 Message Date
2c86219c82 feat: Alipay 安全加固 + 切换手机网站支付 (wap.pay)
Some checks failed
Deploy Backend / deploy (push) Has been cancelled
- Phase 1: 移除 Git 中 ALIPAY 密钥,改用 EnvironmentFile
  所有 4 个 deploy 模板删除 ALIPAY_* 内联 Environment,改为指向 .env
  check_service_file() 不再要求 ALIPAY_APP_ID 在 Environment 中
  init_env() 生成模板添加 EnvironmentFile 行

- Phase 2: 切换到手机网站支付 (alipay.trade.wap.pay)
  payment.rs: product_code QUICK_WAP_PAY, method alipay.trade.wap.pay
  用户从手机浏览器访问时自动唤起支付宝 APP

- Phase 3: 服务器 .env 文件创建
  所有环境 .env 文件已通过 SSH 创建(600 权限)
  dev 使用沙箱网关 (openapi-sandbox.dl.alipaydev.com)
  prod 使用正式网关 (openapi.alipay.com)

- 生产部署完成(blue→green 双环境同步)
  密钥安全: 不再存在于任何 Git 跟踪文件中
2026-07-20 14:52:39 +08:00
4c5c879583 fix: static_files 添加 txt 的 Content-Type 映射 2026-07-20 09:36:00 +08:00
70bb63be8d feat: 添加 robots.txt(屏蔽爬虫索引 API/支付) 2026-07-20 09:33:36 +08:00
9fc5c2429e feat: daily-report.sh 每日健康报告邮件 + logwatch 替换 2026-07-20 09:28:49 +08:00
1a359f8d84 fix: 日志输出从文件改为 stdout(journald 可见),紧凑格式,去掉 tracing-appender 2026-07-20 09:24:12 +08:00
d9c9d1dca0 fix: test_nginx_proxy_temp 改用 remote() 执行 2026-07-19 09:58:57 +08:00
941a8e5350 fix: insert_weather_data 配额查询使用已重命名的 membership_expires_at 列 2026-07-19 09:52:35 +08:00
14 changed files with 161 additions and 83 deletions

22
Cargo.lock generated
View File

@@ -544,15 +544,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
@@ -2358,7 +2349,6 @@ dependencies = [
"sqlx",
"tokio",
"tracing",
"tracing-appender",
"tracing-subscriber",
"uuid",
]
@@ -3304,18 +3294,6 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
dependencies = [
"crossbeam-channel",
"thiserror",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.30"

View File

@@ -9,7 +9,6 @@ chrono = {version = "0.4.41", features=["serde"]}
dotenvy = "0.15.7"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-appender = "0.2"
sentry = "0.48"
sentry-actix = "0.48"
sentry-tracing = "0.48"

View File

@@ -24,10 +24,7 @@ Environment=RUST_LOG=info
Environment=PAYMENT_MAINTENANCE_MODE=true
Environment=APP_VERSION=0.3.0
Environment=FREE_USER_DATA_LIMIT=20
Environment=ALIPAY_APP_ID=2021006148681106
Environment=ALIPAY_ALIPAY_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkhLqABUN1E2PCmOznQp+53T3x4j27I96hdatl6lWbCBPwyXnZVW4uYZ4kV+34+i2wCzEtMpUiNQaIsQXx2qD8x0d8szsj7pEZuY+xUUJTzm95fYO2nq4aaQ38T/kxdYrW12is8vwY83rlghdlkvVP2XZCuYoFq9UVieTu6LTDk6v91Mt2xr1Zw1Oj+yg8VpBFXkcS7ORJjiufWzCEoxzlb2KK2AGOLhdc8BJsBfrQ0JAqFB6b0uteUF21DDdPN/sblD8GoRAGuSpDhyQLG+0fDNdcIMyHpsq7bIDNjgYGS8RbKAtb6WyV6vriIemZ0fMvzN5Mpqv2jwCe/31ar//PwIDAQAB
Environment=ALIPAY_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCImX4Zj18whXYi6l29caq+ShjqT7JwWDbOoz/hEqZqPxQNu6KF59OswcUIv3It4CxJ0/kXsiC/pc6mrN9GedOAPIaz6YydgII/nwHl4OdPPxW8jeomBuiSmcbPcZ4AJTi1YTxrj//8YDIQInVc5/I4HPg+osN4qkqy+xrJ4vGrmyy+ClwHh3OxglGDLhbprH4WCgxKjwTDfR1A84w+pbrST1YaWI7sqqPhVxuYQQPjaLOpXMgrJu5ctnlFVTC27mY2wSIrxajB/6w1SQ+Z+Z149frk47PBOwL+xfKT4gwy3yj/t7WZStzH0gEhG7mXjc6qIjZ8uH5OxUZ+f6gMDpKBAgMBAAECggEAYnI87y7k65NEEOXXqTDCXWIJktTdRRpOZZ5B63QFe4z6ZutTti3d4CPGXQecwnNNd+fNHcbC01wsEvOD9sbWJxV3/zHSpbB4LmHqfEIacoKEp9+fYMiZ1Q3+uoKn3HL268GoSdz+IGUzr20mKE8Bwm8eegXT2qLgMx3FsLpecc3WXmwgMDI/alknO81+xZenLH1Y6+XdsoPVWsS3OY3gnzfYz40n1LaLCQiklBu6AUjSQupSUJ53rGtYpZnp+ImYwBVcYeFL0mZZVId/lzDI2K8eaM/RGVZYsU1vkP6CeSJzWhpr6ftSRRl3VJQou0ytLLX/YeUoXIhsfZ+x3cPKAQKBgQC9ub1z+T/+0lHZwm360otNHQNeEq3FYjnx1x7WOWTXCsAqVZizfmpUYSHoNZeNlyLpbXVsYp22xJmLBArB9LBMvjY1z2iOQJW7F46O6cY38AcgWC41YalLcrk2y4jny6rJZRMjvmDOM67SH01pzZQ6eGJpzSZpTj2Jm/tBsH7gGwKBgQC4UPK5a32ov9YuyzfQXcW341JFlTK2k2D1ewilYSZifpTDqdVpH1gSzGpfoXI7K6Z26xkx1n5rOIe+SxOKaNBj/5PJuEzmMVvUWkFU0IutoqVc2R6h4Rb+9q7llzAavFZ5Ssj1u9Q5Pjhc5Jr8zM6xXk55/4CuqYuBMCcte4fZkwKBgF/kVSEcxX+iBcSuRLAVYlrjYyRVBIdTEj5toCRedd1Kjb3X7YorRsrHHUAkdikWTor0y/iwD06lJ4Xf0SAe8EyZhwTDg/ECK6uYSHFrLshvBOsQJqrMZajAn2RWkye1BOktsPLL8lnGPYHuWE41ncm/tt4K1nM7HFdFA0U8U6QpAoGAe0gRgboG+6cMoJHmjCUciUmwDsCjggEDuk4H024umXSnFlRzXPPNGg3GHJ7E5BTnK+VGHdKlDhxqlvpqYIqI0EnzVNpD/TgIfvm7dvpiYspGJttGpOoEUpc3FnPQhiFxCYHe9pYidgNK+b8id3KZ0YMNpIYkUS8wE0HdAov8qqcCgYEAtRjXzj4JZ5we6aOqv4bcr7xLw4a7wzS99K5EcrbYRxvibUhelooQdgdY40FmdtfuY/4rpHC3R9/13aqcD/WSTFOdOsZAcfTi738y13h3dCy3FZisLYM7rgjUSnQnEGlyR0TYE4Yda490rpYJBjyQIE3EcIlWPerm8vh1wx1Rh90=
Environment=ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
EnvironmentFile=/root/rust/rust_backend_blue/.env
Environment=APP_BASE_URL=https://xmclassmate.top
Environment=SERVER_PORT=4433
Environment=SENTRY_DSN=http://f02c7cce1dc746e2ad41b11397dab0db@127.0.0.1:9000/1

View File

@@ -24,13 +24,8 @@ Environment=RUST_LOG=info,rust_backend=debug
Environment=APP_VERSION=0.2.0
Environment=FREE_USER_DATA_LIMIT=50
Environment=SENTRY_DSN=http://f02c7cce1dc746e2ad41b11397dab0db@127.0.0.1:9000/1
Environment=APP_BASE_URL=https://xmclassmate.top
Environment=ALIPAY_APP_ID=2021006148681106
Environment=ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
Environment=ALIPAY_ALIPAY_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkhLqABUN1E2PCmOznQp+53T3x4j27I96hdatl6lWbCBPwyXnZVW4uYZ4kV+34+i2wCzEtMpUiNQaIsQXx2qD8x0d8szsj7pEZuY+xUUJTzm95fYO2nq4aaQ38T/kxdYrW12is8vwY83rlghdlkvVP2XZCuYoFq9UVieTu6LTDk6v91Mt2xr1Zw1Oj+yg8VpBFXkcS7ORJjiufWzCEoxzlb2KK2AGOLhdc8BJsBfrQ0JAqFB6b0uteUF21DDdPN/sblD8GoRAGuSpDhyQLG+0fDNdcIMyHpsq7bIDNjgYGS8RbKAtb6WyV6vriIemZ0fMvzN5Mpqv2jwCe/31ar//PwIDAQAB
Environment=MOCK_LOGIN_ENABLED=true
Environment=MOCK_LOGIN_KEY=d3v-t35t-k3y-2026
Environment=ALIPAY_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCImX4Zj18whXYi6l29caq+ShjqT7JwWDbOoz/hEqZqPxQNu6KF59OswcUIv3It4CxJ0/kXsiC/pc6mrN9GedOAPIaz6YydgII/nwHl4OdPPxW8jeomBuiSmcbPcZ4AJTi1YTxrj//8YDIQInVc5/I4HPg+osN4qkqy+xrJ4vGrmyy+ClwHh3OxglGDLhbprH4WCgxKjwTDfR1A84w+pbrST1YaWI7sqqPhVxuYQQPjaLOpXMgrJu5ctnlFVTC27mY2wSIrxajB/6w1SQ+Z+Z149frk47PBOwL+xfKT4gwy3yj/t7WZStzH0gEhG7mXjc6qIjZ8uH5OxUZ+f6gMDpKBAgMBAAECggEAYnI87y7k65NEEOXXqTDCXWIJktTdRRpOZZ5B63QFe4z6ZutTti3d4CPGXQecwnNNd+fNHcbC01wsEvOD9sbWJxV3/zHSpbB4LmHqfEIacoKEp9+fYMiZ1Q3+uoKn3HL268GoSdz+IGUzr20mKE8Bwm8eegXT2qLgMx3FsLpecc3WXmwgMDI/alknO81+xZenLH1Y6+XdsoPVWsS3OY3gnzfYz40n1LaLCQiklBu6AUjSQupSUJ53rGtYpZnp+ImYwBVcYeFL0mZZVId/lzDI2K8eaM/RGVZYsU1vkP6CeSJzWhpr6ftSRRl3VJQou0ytLLX/YeUoXIhsfZ+x3cPKAQKBgQC9ub1z+T/+0lHZwm360otNHQNeEq3FYjnx1x7WOWTXCsAqVZizfmpUYSHoNZeNlyLpbXVsYp22xJmLBArB9LBMvjY1z2iOQJW7F46O6cY38AcgWC41YalLcrk2y4jny6rJZRMjvmDOM67SH01pzZQ6eGJpzSZpTj2Jm/tBsH7gGwKBgQC4UPK5a32ov9YuyzfQXcW341JFlTK2k2D1ewilYSZifpTDqdVpH1gSzGpfoXI7K6Z26xkx1n5rOIe+SxOKaNBj/5PJuEzmMVvUWkFU0IutoqVc2R6h4Rb+9q7llzAavFZ5Ssj1u9Q5Pjhc5Jr8zM6xXk55/4CuqYuBMCcte4fZkwKBgF/kVSEcxX+iBcSuRLAVYlrjYyRVBIdTEj5toCRedd1Kjb3X7YorRsrHHUAkdikWTor0y/iwD06lJ4Xf0SAe8EyZhwTDg/ECK6uYSHFrLshvBOsQJqrMZajAn2RWkye1BOktsPLL8lnGPYHuWE41ncm/tt4K1nM7HFdFA0U8U6QpAoGAe0gRgboG+6cMoJHmjCUciUmwDsCjggEDuk4H024umXSnFlRzXPPNGg3GHJ7E5BTnK+VGHdKlDhxqlvpqYIqI0EnzVNpD/TgIfvm7dvpiYspGJttGpOoEUpc3FnPQhiFxCYHe9pYidgNK+b8id3KZ0YMNpIYkUS8wE0HdAov8qqcCgYEAtRjXzj4JZ5we6aOqv4bcr7xLw4a7wzS99K5EcrbYRxvibUhelooQdgdY40FmdtfuY/4rpHC3R9/13aqcD/WSTFOdOsZAcfTi738y13h3dCy3FZisLYM7rgjUSnQnEGlyR0TYE4Yda490rpYJBjyQIE3EcIlWPerm8vh1wx1Rh90=
Environment=APP_BASE_URL=https://dev.xmclassmate.top
EnvironmentFile=/root/rust/rust_backend_dev/.env
NoNewPrivileges=yes
PrivateTmp=yes

View File

@@ -24,10 +24,7 @@ Environment=RUST_LOG=info
Environment=PAYMENT_MAINTENANCE_MODE=true
Environment=APP_VERSION=0.3.0
Environment=FREE_USER_DATA_LIMIT=20
Environment=ALIPAY_APP_ID=2021006148681106
Environment=ALIPAY_ALIPAY_PUBLIC_KEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkhLqABUN1E2PCmOznQp+53T3x4j27I96hdatl6lWbCBPwyXnZVW4uYZ4kV+34+i2wCzEtMpUiNQaIsQXx2qD8x0d8szsj7pEZuY+xUUJTzm95fYO2nq4aaQ38T/kxdYrW12is8vwY83rlghdlkvVP2XZCuYoFq9UVieTu6LTDk6v91Mt2xr1Zw1Oj+yg8VpBFXkcS7ORJjiufWzCEoxzlb2KK2AGOLhdc8BJsBfrQ0JAqFB6b0uteUF21DDdPN/sblD8GoRAGuSpDhyQLG+0fDNdcIMyHpsq7bIDNjgYGS8RbKAtb6WyV6vriIemZ0fMvzN5Mpqv2jwCe/31ar//PwIDAQAB
Environment=ALIPAY_PRIVATE_KEY=MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCImX4Zj18whXYi6l29caq+ShjqT7JwWDbOoz/hEqZqPxQNu6KF59OswcUIv3It4CxJ0/kXsiC/pc6mrN9GedOAPIaz6YydgII/nwHl4OdPPxW8jeomBuiSmcbPcZ4AJTi1YTxrj//8YDIQInVc5/I4HPg+osN4qkqy+xrJ4vGrmyy+ClwHh3OxglGDLhbprH4WCgxKjwTDfR1A84w+pbrST1YaWI7sqqPhVxuYQQPjaLOpXMgrJu5ctnlFVTC27mY2wSIrxajB/6w1SQ+Z+Z149frk47PBOwL+xfKT4gwy3yj/t7WZStzH0gEhG7mXjc6qIjZ8uH5OxUZ+f6gMDpKBAgMBAAECggEAYnI87y7k65NEEOXXqTDCXWIJktTdRRpOZZ5B63QFe4z6ZutTti3d4CPGXQecwnNNd+fNHcbC01wsEvOD9sbWJxV3/zHSpbB4LmHqfEIacoKEp9+fYMiZ1Q3+uoKn3HL268GoSdz+IGUzr20mKE8Bwm8eegXT2qLgMx3FsLpecc3WXmwgMDI/alknO81+xZenLH1Y6+XdsoPVWsS3OY3gnzfYz40n1LaLCQiklBu6AUjSQupSUJ53rGtYpZnp+ImYwBVcYeFL0mZZVId/lzDI2K8eaM/RGVZYsU1vkP6CeSJzWhpr6ftSRRl3VJQou0ytLLX/YeUoXIhsfZ+x3cPKAQKBgQC9ub1z+T/+0lHZwm360otNHQNeEq3FYjnx1x7WOWTXCsAqVZizfmpUYSHoNZeNlyLpbXVsYp22xJmLBArB9LBMvjY1z2iOQJW7F46O6cY38AcgWC41YalLcrk2y4jny6rJZRMjvmDOM67SH01pzZQ6eGJpzSZpTj2Jm/tBsH7gGwKBgQC4UPK5a32ov9YuyzfQXcW341JFlTK2k2D1ewilYSZifpTDqdVpH1gSzGpfoXI7K6Z26xkx1n5rOIe+SxOKaNBj/5PJuEzmMVvUWkFU0IutoqVc2R6h4Rb+9q7llzAavFZ5Ssj1u9Q5Pjhc5Jr8zM6xXk55/4CuqYuBMCcte4fZkwKBgF/kVSEcxX+iBcSuRLAVYlrjYyRVBIdTEj5toCRedd1Kjb3X7YorRsrHHUAkdikWTor0y/iwD06lJ4Xf0SAe8EyZhwTDg/ECK6uYSHFrLshvBOsQJqrMZajAn2RWkye1BOktsPLL8lnGPYHuWE41ncm/tt4K1nM7HFdFA0U8U6QpAoGAe0gRgboG+6cMoJHmjCUciUmwDsCjggEDuk4H024umXSnFlRzXPPNGg3GHJ7E5BTnK+VGHdKlDhxqlvpqYIqI0EnzVNpD/TgIfvm7dvpiYspGJttGpOoEUpc3FnPQhiFxCYHe9pYidgNK+b8id3KZ0YMNpIYkUS8wE0HdAov8qqcCgYEAtRjXzj4JZ5we6aOqv4bcr7xLw4a7wzS99K5EcrbYRxvibUhelooQdgdY40FmdtfuY/4rpHC3R9/13aqcD/WSTFOdOsZAcfTi738y13h3dCy3FZisLYM7rgjUSnQnEGlyR0TYE4Yda490rpYJBjyQIE3EcIlWPerm8vh1wx1Rh90=
Environment=ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
EnvironmentFile=/root/rust/rust_backend_green/.env
Environment=APP_BASE_URL=https://xmclassmate.top
Environment=SERVER_PORT=4434
Environment=SENTRY_DSN=http://f02c7cce1dc746e2ad41b11397dab0db@127.0.0.1:9000/1

View File

@@ -25,10 +25,7 @@ Environment=RUST_LOG=info
Environment=APP_VERSION=0.3.0
Environment=FREE_USER_DATA_LIMIT=20
Environment=APP_BASE_URL=https://xmclassmate.top
Environment=ALIPAY_APP_ID=your_alipay_app_id
Environment=ALIPAY_GATEWAY=https://openapi.alipay.com/gateway.do
Environment=ALIPAY_ALIPAY_PUBLIC_KEY=your_alipay_public_key
Environment=ALIPAY_PRIVATE_KEY=your_private_key_base64
EnvironmentFile=/root/rust/rust_backend/.env
NoNewPrivileges=yes
PrivateTmp=yes

View File

@@ -90,7 +90,7 @@ check_service_file() {
local missing=0 ph=0
local required_vars=("DATABASE_URL" "JWT_SECRET" "WECHAT_APPID" "WECHAT_SECRET")
[ "${APP_ENV}" = "production" ] && required_vars+=("SSL_KEY_PATH" "SSL_CERT_PATH" "ALIPAY_APP_ID")
[ "${APP_ENV}" = "production" ] && required_vars+=("SSL_KEY_PATH" "SSL_CERT_PATH")
for var in "${required_vars[@]}"; do
local val; val=$(echo "$content" | grep -E "^Environment=${var}=" | head -1 | sed "s/^Environment=${var}=//")
@@ -205,6 +205,8 @@ Environment=SSL_CERT_PATH=
Environment=RUST_LOG=info
Environment=APP_VERSION=0.3.0
Environment=FREE_USER_DATA_LIMIT=20
# 支付宝密钥存储在 .env 文件(不在 Git 中)
EnvironmentFile=${REMOTE_DIR}/.env
SERVEOF
chmod 600 "$(get_service_file_local)"

View File

@@ -456,15 +456,13 @@ test_payment_verify_detection() {
test_nginx_proxy_temp() {
log_info "测试 nginx proxy tempPOST body 权限)..."
# 发送 POST 请求验证 nginx 能正代理带 body 的请求AppArmor proxy temp 权限检查)
local body; body=$(python3 -c "print('x'*50000)" 2>/dev/null || printf 'x%.0s' {1..50000})
# 发送 POST body 的请求到公开 POST 端点,验证 nginx 能正代理
# 如果 proxy temp 无权限nginx 返回 502/403 或超时
local resp
resp=$(curl -sk --max-time 10 -X POST "${BASE_URL}" \
-H "Content-Type: application/json" \
-d "{\"test_body_size\":${#body}}" 2>&1) || true
resp=$(remote "curl -sk --max-time 10 -X POST '${BASE_URL}/api/login' \
-H 'Content-Type: application/json' \
-d '{\"test\":\"x\"}'" 2>/dev/null || true)
# 只要能返回响应(包括错误),就说明 nginx 能正常处理 POST body
# 如果 proxy temp 无权限nginx 会返回 502/403 或超时
if [ -n "$resp" ]; then
log_info " nginx POST body 代理正常 ✅"
return 0

116
scripts/daily-report.sh Normal file
View File

@@ -0,0 +1,116 @@
#!/bin/bash
# 每日健康报告 — 发送格式化摘要邮件到管理员
# 部署cp 到服务器 /root/rust/scripts/daily-report.shcron 每日执行
set -e
TO="m943790568@163.com"
DATE=$(date "+%Y-%m-%d")
HOST=$(hostname)
# ---------- 采集数据 ----------
UPTIME=$(uptime -p | sed 's/up //')
LOAD=$(uptime | awk -F'load average:' '{print $2}' | xargs)
DISK_USED=$(df -h / | awk 'NR==2{print $3}')
DISK_TOTAL=$(df -h / | awk 'NR==2{print $2}')
DISK_PCT=$(df -h / | awk 'NR==2{print $5}')
MEM_USED=$(free -h | awk '/Mem/{print $3}')
MEM_TOTAL=$(free -h | awk '/Mem/{print $2}')
MEM_PCT=$(free | awk '/Mem/{printf "%.0f", $3/$2 * 100}')
CPU_TEMP=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null | awk '{printf "%.1f°C", $1/1000}' || echo "N/A")
# Docker
DOCKER_STATUS=$(docker ps --format '{{.Names}} {{.Status}}' 2>/dev/null || echo "Docker 不可用")
# SSL 证书
SSL_EXPIRY=$(echo "" | openssl s_client -connect xmclassmate.top:443 -servername xmclassmate.top 2>&1 | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2 || echo "检测失败")
SSL_DAYS=$(( ($(date -d "$SSL_EXPIRY" +%s 2>/dev/null) - $(date +%s)) / 86400 )) || SSL_DAYS="?"
# 服务状态
GREEN_STATUS=$(systemctl is-active rust-backend-green.service 2>/dev/null || echo "unknown")
BLUE_STATUS=$(systemctl is-active rust-backend-blue.service 2>/dev/null || echo "unknown")
NGINX_STATUS=$(systemctl is-active nginx 2>/dev/null || echo "unknown")
# 最近备份
LAST_BACKUP=$(ls -1t /root/rust/backups/milkydata/ 2>/dev/null | head -1)
BACKUP_AGE="未知"
if [ -n "$LAST_BACKUP" ]; then
BACKUP_TIME=$(stat -c %Y "/root/rust/backups/milkydata/$LAST_BACKUP" 2>/dev/null)
NOW=$(date +%s)
AGE_HOURS=$(( (NOW - BACKUP_TIME) / 3600 ))
if [ "$AGE_HOURS" -lt 24 ]; then
BACKUP_AGE="${AGE_HOURS}小时前"
elif [ "$AGE_HOURS" -lt 48 ]; then
BACKUP_AGE="⚠️ ${AGE_HOURS}小时前"
else
BACKUP_AGE="${AGE_HOURS}小时前"
fi
fi
# Fail2Ban 封禁统计
SSH_BANS=$(sudo fail2ban-client status sshd 2>/dev/null | grep "Currently banned" | awk '{print $NF}' || echo "0")
NGINX_BANS=$(sudo fail2ban-client status nginx-botsearch 2>/dev/null | grep "Currently banned" | awk '{print $NF}' || echo "0")
# 近期错误数(昨天 journalctl
YESTERDAY_ERRORS=$(journalctl -u rust-backend-green.service --since "1 day ago" --priority err --no-pager 2>/dev/null | wc -l)
YESTERDAY_WARNS=$(journalctl -u rust-backend-green.service --since "1 day ago" --priority warning --no-pager 2>/dev/null | wc -l)
# 应用统计(昨天活跃用户)
ACTIVE_USERS=$(journalctl -u rust-backend-green.service --since "1 day ago" --no-pager 2>/dev/null | grep -c "user_id=\|登录成功\|上传成功" || echo "0")
API_COUNT=$(journalctl -u rust-backend-green.service --since "1 day ago" --no-pager 2>/dev/null | grep -c "INFO\|WARN\|ERROR" || echo "0")
# ---------- 构建邮件正文 ----------
SUBJECT="[ASD 服务器报告] ${DATE}${HOST}"
MAIL=$(cat <<EOF
===========================================
ASD 服务器健康报告
${DATE}
主机: ${HOST}
===========================================
🖥 系统状态
- 运行时长: ${UPTIME}
- 负载: ${LOAD}
- 磁盘: ${DISK_USED} / ${DISK_TOTAL} (${DISK_PCT})
- 内存: ${MEM_USED} / ${MEM_TOTAL} (${MEM_PCT})
- CPU 温度: ${CPU_TEMP}
📦 容器
$(echo "$DOCKER_STATUS" | sed 's/^/ /')
🔐 SSL 证书
域名: xmclassmate.top
到期: ${SSL_EXPIRY}
剩余: ${SSL_DAYS} 天
🔄 服务状态
- 后端 (green): ${GREEN_STATUS}
- 后端 (blue): ${BLUE_STATUS}
- nginx: ${NGINX_STATUS}
🛡 安全
- SSH 封禁: ${SSH_BANS} 个 IP
- nginx 封禁: ${NGINX_BANS} 个 IP
- 昨日错误: ${YESTERDAY_ERRORS}
- 昨日警告: ${YESTERDAY_WARNS}
📊 应用概况(昨日)
- 日志事件数: ${API_COUNT}
- 活跃用户操作: ${ACTIVE_USERS}
💾 备份
- 最新备份: ${LAST_BACKUP:-无}
- 备份时效: ${BACKUP_AGE}
---
ASD 自动监控 | $(date "+%Y-%m-%d %H:%M")
EOF
)
# ---------- 发送邮件 ----------
echo "$MAIL" | msmtp -a default "$TO"
echo "✅ 健康报告已发送到 ${TO}"

View File

@@ -11,17 +11,18 @@ use crate::error::AppError;
pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user_id: i32) -> Result<i32, AppError> {
// 单次查询获取用户会员状态和数据条数(原 N+1get_user_by_id + count_user_weather_data → 合并为 1 次)
#[derive(FromRow)]
struct UserQuota {
is_member: bool,
paid_expires_at: Option<chrono::DateTime<Utc>>,
data_count: i64,
}
struct UserQuota {
is_member: bool,
#[sqlx(rename = "membership_expires_at")]
membership_expires_at: Option<chrono::DateTime<Utc>>,
data_count: i64,
}
let quota = sqlx::query_as::<_, UserQuota>(
r#"
SELECT
u.is_member,
u.paid_expires_at,
u.membership_expires_at,
(SELECT COUNT(*) FROM weather_data w WHERE w.user_id = $1)::BIGINT AS data_count
FROM users u
WHERE u.id = $1
@@ -33,7 +34,7 @@ pub async fn insert_weather_data(pool: &PgPool, weather_data: &WeatherData, user
.map_err(|e| AppError::Database(format!("查询用户配额失败: {}", e)))?
.ok_or_else(|| AppError::NotFound("用户不存在".to_string()))?;
let is_active_member = quota.is_member && quota.paid_expires_at.is_none_or(|expires| expires > Utc::now());
let is_active_member = quota.is_member && quota.membership_expires_at.is_none_or(|expires| expires > Utc::now());
// 维护模式:非会员使用更高的临时限额,防止资源滥用
let is_maintenance = std::env::var("PAYMENT_MAINTENANCE_MODE").ok() == Some("true".to_string());

View File

@@ -179,7 +179,7 @@ fn build_alipay_form_html(
"out_trade_no": out_trade_no,
"total_amount": total_amount,
"subject": subject,
"product_code": "FAST_INSTANT_TRADE_PAY",
"product_code": "QUICK_WAP_PAY",
});
let biz_content_str =
@@ -188,7 +188,7 @@ fn build_alipay_form_html(
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let mut params: BTreeMap<&str, &str> = BTreeMap::new();
params.insert("app_id", &config.app_id);
params.insert("method", "alipay.trade.page.pay");
params.insert("method", "alipay.trade.wap.pay");
params.insert("format", "JSON");
params.insert("charset", "utf-8");
params.insert("sign_type", "RSA2");

View File

@@ -17,6 +17,7 @@ pub async fn serve_static_files(path: web::Path<String>) -> impl Responder {
Some("gif") => "image/gif",
Some("svg") => "image/svg+xml",
Some("webp") => "image/webp",
Some("txt") => "text/plain",
Some("css") => "text/css",
Some("js") => "application/javascript",
Some("html") => "text/html",

View File

@@ -85,6 +85,8 @@ fn create_server_config(
.service(alipay_notify) // POST /payment/notify支付宝异步回调
.service(alipay_refund_notify) // POST /payment/refund-notify支付宝退款回调
.service(payment_login_status) // GET /payment/login-status网页轮询
// robots.txt无需认证
.route("/robots.txt", web::get().to(|| async { serve_static_files(web::Path::from("robots.txt".to_string())).await }))
// 静态文件(无需认证)
.service(web::resource("/static/{tail:.*}").route(web::get().to(serve_static_files)))
// API 接口
@@ -154,7 +156,7 @@ async fn main() -> std::io::Result<()> {
// Sentry 初始化(仅在 SENTRY_DSN 环境变量已设置时启用)
let _sentry_guard = if let Ok(dsn) = std::env::var("SENTRY_DSN") {
if !dsn.is_empty() {
eprintln!("[SENTRY] Sentry 已初始化 (DSN: {})", dsn);
info!("Sentry 已初始化");
Some(sentry::init((
dsn,
sentry::ClientOptions {
@@ -167,30 +169,14 @@ async fn main() -> std::io::Result<()> {
},
)))
} else {
eprintln!("[SENTRY] SENTRY_DSN 为空,跳过 Sentry 初始化");
info!("SENTRY_DSN 为空,跳过 Sentry 初始化");
None
}
} else {
eprintln!("[SENTRY] 未设置 SENTRY_DSN跳过 Sentry 初始化");
info!("未设置 SENTRY_DSN跳过 Sentry 初始化");
None
};
// 初始化文件日志JSON 格式,带轮转)
let log_dir = std::path::Path::new("./logs");
std::fs::create_dir_all(log_dir).ok(); // 确保日志目录存在
let file_appender = tracing_appender::rolling::Builder::new()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix("rust-backend")
.filename_suffix("log")
.build(log_dir)
.expect("无法创建日志文件");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
// 保持文件 guard 存活(使用 Box 泄漏)
std::mem::forget(_guard);
use std::str::FromStr;
let default_directive = tracing_subscriber::filter::Directive::from_str("info").unwrap();
@@ -213,14 +199,14 @@ async fn main() -> std::io::Result<()> {
)
)
.with_target(true)
.with_thread_ids(false) // 生产环境可开启
.with_thread_ids(false)
.with_file(true)
.with_line_number(true)
.with_writer(non_blocking)
.with_ansi(false) // 文件中不使用 ANSI 颜色
.json() // JSON 格式便于 ELK 解析
.with_ansi(true)
.compact()
.init();
info!("日志系统初始化成功,JSON格式输出到 ./logs/");
info!("日志系统初始化成功,输出到 systemd journal (journalctl)");
info!("日志级别: {}", app_config.rust_log);
let app_state = match AppState::load() {
Ok(state) => state,

11
static/robots.txt Normal file
View File

@@ -0,0 +1,11 @@
User-agent: *
Disallow: /payment
Disallow: /api
Disallow: /weather
Disallow: /static
# Allow root (status page)
Allow: /$
# Allow health check
Allow: /health$