chore: 干净 ERP 基座 — 删除 health/ai/wechat 业务代码
删除内容: - 前端: health/(67文件), ai/(2文件), Copilot, MediaPicker, 相关API/Store/Hook - 后端: wechat_handler, wechat_service, wechat_user entity, analytics handler, ai_workflow_seed - 配置: WechatConfig, AppConfig.wechat, AuthState wechat 字段 - 启动: 微信凭据检查块, ensure_ai_workflows() 调用 - 迁移: 新增 m20260613_000170_drop_wechat_users.rs - 脚本: api_test_health_alert.py, api_test_mp.py, mpsync.sh/ps1 - E2E: health-data page, flows/ 目录 保留: erp-core/auth/workflow/message/config/plugin + 基座前端 + 通用组件
This commit is contained in:
6
docker/.env.example
Normal file
6
docker/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
POSTGRES_USER=erp
|
||||
POSTGRES_PASSWORD=erp_dev_2024
|
||||
POSTGRES_DB=erp
|
||||
POSTGRES_PORT=5432
|
||||
REDIS_PASSWORD=erp_redis_dev
|
||||
REDIS_PORT=6379
|
||||
70
docker/.env.production.example
Normal file
70
docker/.env.production.example
Normal file
@@ -0,0 +1,70 @@
|
||||
# HMS 云端部署环境变量
|
||||
# 复制此文件为 .env.production 并填写实际值
|
||||
# cp .env.production.example .env.production
|
||||
|
||||
# ===== 必填 =====
|
||||
|
||||
# PostgreSQL 连接(host 网络模式,直连宿主机)
|
||||
ERP__DATABASE__URL=postgres://erp:YOUR_PG_PASSWORD@localhost:5432/erp
|
||||
|
||||
# Redis 连接
|
||||
ERP__REDIS__URL=redis://:YOUR_REDIS_PASSWORD@localhost:6379
|
||||
|
||||
# JWT 密钥(至少 32 字符随机字符串)
|
||||
ERP__JWT__SECRET=CHANGE_ME_TO_A_RANDOM_STRING_AT_LEAST_32_CHARS
|
||||
|
||||
# 超级管理员初始密码(首次启动时创建 admin 用户)
|
||||
ERP__AUTH__SUPER_ADMIN_PASSWORD=CHANGE_ME_ADMIN_PASSWORD
|
||||
|
||||
# PII 加密密钥(AES-256 KEK,64 位十六进制)
|
||||
ERP__CRYPTO__KEK=CHANGE_ME_64_HEX_CHARS_FOR_AES256_KEY
|
||||
|
||||
# 健康数据加密密钥
|
||||
ERP__HEALTH__AES_KEY=CHANGE_ME_64_HEX_CHARS
|
||||
ERP__HEALTH__HMAC_KEY=CHANGE_ME_64_HEX_CHARS
|
||||
|
||||
# ===== 可选 =====
|
||||
|
||||
# 服务端口(默认 3000)
|
||||
ERP__SERVER__PORT=3000
|
||||
|
||||
# Prometheus 指标端口(默认 9090)
|
||||
ERP__SERVER__METRICS_PORT=9090
|
||||
|
||||
# CORS 允许的来源(逗号分隔)
|
||||
ERP__CORS__ALLOWED_ORIGINS=https://your-domain.com,https://www.your-domain.com
|
||||
|
||||
# 上传目录
|
||||
ERP__STORAGE__UPLOAD_DIR=/app/uploads
|
||||
|
||||
# 日志级别
|
||||
ERP__LOG__LEVEL=info
|
||||
|
||||
# 微信小程序配置(不需要小程序功能可留空)
|
||||
ERP__WECHAT__APPID=
|
||||
ERP__WECHAT__SECRET=
|
||||
ERP__WECHAT__DEV_MODE=false
|
||||
|
||||
# AI 模块配置(不需要 AI 功能可留空)
|
||||
ERP__AI__DEFAULT_PROVIDER=ollama
|
||||
ERP__AI__API_KEY=
|
||||
ERP__AI__BASE_URL=http://localhost:11434
|
||||
ERP__AI__MODEL=qwen2.5:7b
|
||||
|
||||
# ===== DevOps =====
|
||||
|
||||
# 备份加密密码(openssl AES-256-CBC,必填用于生产)
|
||||
BACKUP_PASSPHRASE=CHANGE_ME_BACKUP_ENCRYPTION_PASSWORD
|
||||
|
||||
# 备份保留天数
|
||||
BACKUP_KEEP_DAYS=7
|
||||
|
||||
# 备份执行时间(cron 格式)
|
||||
BACKUP_CRON=0 2 * * *
|
||||
|
||||
# uploads 备份时间
|
||||
UPLOADS_BACKUP_CRON=0 3 * * *
|
||||
|
||||
# Grafana 管理员密码
|
||||
GRAFANA_ADMIN_PASSWORD=CHANGE_ME_GRAFANA_ADMIN
|
||||
GRAFANA_ROOT_URL=http://localhost:3001
|
||||
1
docker/.gitignore
vendored
Normal file
1
docker/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env.production
|
||||
95
docker/backup.sh
Normal file
95
docker/backup.sh
Normal file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostgreSQL 自动备份脚本(含加密)
|
||||
# 用法:
|
||||
# 手动: ./docker/backup.sh
|
||||
# 自动: 由 docker compose backup 服务每日 02:00 执行
|
||||
#
|
||||
# 加密方式(二选一):
|
||||
# BACKUP_PASSPHRASE — 使用 openssl AES-256-CBC 对称加密(无额外依赖)
|
||||
# GPG_RECIPIENT — 使用 GPG 非对称加密(需预置公钥)
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_DIR="${BACKUP_DIR:-/backups}"
|
||||
PG_HOST="${PGHOST:-postgres}"
|
||||
PG_PORT="${PGPORT:-5432}"
|
||||
PG_USER="${PGUSER:-erp}"
|
||||
PG_DB="${PGDATABASE:-erp}"
|
||||
KEEP_DAYS="${KEEP_DAYS:-7}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
FILENAME="${PG_DB}_${TIMESTAMP}.sql.gz"
|
||||
ENCRYPTED_FILENAME="${FILENAME}.enc"
|
||||
FILEPATH="${BACKUP_DIR}/${FILENAME}"
|
||||
ENCRYPTED_FILEPATH="${BACKUP_DIR}/${ENCRYPTED_FILENAME}"
|
||||
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
echo "[$(date -Iseconds)] 开始备份 ${PG_DB} → ${FILEPATH}"
|
||||
|
||||
if pg_dump \
|
||||
-h "${PG_HOST}" \
|
||||
-p "${PG_PORT}" \
|
||||
-U "${PG_USER}" \
|
||||
-d "${PG_DB}" \
|
||||
--format=plain \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
| gzip > "${FILEPATH}"; then
|
||||
SIZE=$(du -h "${FILEPATH}" | cut -f1)
|
||||
echo "[$(date -Iseconds)] 备份完成: ${FILENAME} (${SIZE})"
|
||||
else
|
||||
echo "[$(date -Iseconds)] 备份失败!" >&2
|
||||
rm -f "${FILEPATH}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── 加密备份 ──
|
||||
if [ -n "${BACKUP_PASSPHRASE:-}" ]; then
|
||||
echo "[$(date -Iseconds)] 使用 AES-256-CBC 加密备份..."
|
||||
if openssl enc -aes-256-cbc -salt -pbkdf2 -pass "pass:${BACKUP_PASSPHRASE}" \
|
||||
-in "${FILEPATH}" -out "${ENCRYPTED_FILEPATH}"; then
|
||||
rm -f "${FILEPATH}"
|
||||
ENC_SIZE=$(du -h "${ENCRYPTED_FILEPATH}" | cut -f1)
|
||||
echo "[$(date -Iseconds)] 加密完成: ${ENCRYPTED_FILENAME} (${ENC_SIZE})"
|
||||
else
|
||||
echo "[$(date -Iseconds)] 加密失败!保留未加密备份" >&2
|
||||
rm -f "${ENCRYPTED_FILEPATH}"
|
||||
fi
|
||||
elif [ -n "${GPG_RECIPIENT:-}" ]; then
|
||||
echo "[$(date -Iseconds)] 使用 GPG 加密备份..."
|
||||
if gpg --batch --yes --encrypt --recipient "${GPG_RECIPIENT}" "${FILEPATH}"; then
|
||||
rm -f "${FILEPATH}"
|
||||
ENC_SIZE=$(du -h "${ENCRYPTED_FILEPATH}" | cut -f1)
|
||||
echo "[$(date -Iseconds)] 加密完成: ${ENCRYPTED_FILENAME} (${ENC_SIZE})"
|
||||
else
|
||||
echo "[$(date -Iseconds)] GPG 加密失败!保留未加密备份" >&2
|
||||
rm -f "${FILEPATH}.gpg"
|
||||
fi
|
||||
else
|
||||
echo "[$(date -Iseconds)] 警告: 未设置 BACKUP_PASSPHRASE 或 GPG_RECIPIENT,备份未加密!" >&2
|
||||
fi
|
||||
|
||||
# ── 备份完整性校验 ──
|
||||
LATEST_FILE=$(ls -t "${BACKUP_DIR}/${PG_DB}"_*.sql.gz* 2>/dev/null | head -1)
|
||||
if [ -n "${LATEST_FILE}" ] && [ -f "${LATEST_FILE}" ]; then
|
||||
if [[ "${LATEST_FILE}" == *.enc ]]; then
|
||||
echo "[$(date -Iseconds)] 加密备份文件存在: $(basename "${LATEST_FILE}")"
|
||||
elif gzip -t "${LATEST_FILE}" 2>/dev/null; then
|
||||
echo "[$(date -Iseconds)] 备份完整性校验通过"
|
||||
else
|
||||
echo "[$(date -Iseconds)] 警告: 备份文件可能损坏: ${LATEST_FILE}" >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 清理过期备份 ──
|
||||
DELETED=$(find "${BACKUP_DIR}" -name "${PG_DB}_*.sql.gz*" -mtime +${KEEP_DAYS} -delete -print | wc -l)
|
||||
if [ "${DELETED}" -gt 0 ]; then
|
||||
echo "[$(date -Iseconds)] 已清理 ${DELETED} 个过期备份(>${KEEP_DAYS}天)"
|
||||
fi
|
||||
|
||||
# ── 恢复指引 ──
|
||||
echo ""
|
||||
echo "恢复方法:"
|
||||
echo " # 解密(如加密):"
|
||||
echo " openssl enc -d -aes-256-cbc -pbkdf2 -pass pass:\$BACKUP_PASSPHRASE -in ${ENCRYPTED_FILEPATH} -out ${FILEPATH}"
|
||||
echo " # 恢复:"
|
||||
echo " gunzip -c ${FILEPATH} | psql -h \$PGHOST -U \$PGUSER -d \$PGDB"
|
||||
40
docker/docker-compose.cloud.yml
Normal file
40
docker/docker-compose.cloud.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
# 云端部署配置 — 仅启动应用容器,PG/Redis 使用宿主机已安装的服务
|
||||
# 使用方式: docker compose -f docker/docker-compose.cloud.yml up -d
|
||||
#
|
||||
# 前置条件:
|
||||
# 1. 宿主机已安装 PostgreSQL 16 + Redis 7
|
||||
# 2. PostgreSQL 已创建数据库和用户
|
||||
# 3. 复制 .env.production.example 为 .env.production 并填写实际值
|
||||
# 4. OpenResty 反代配置:
|
||||
# - /api/* → http://localhost:3000
|
||||
# - /uploads/* → http://localhost:3000
|
||||
# - / → 前端静态文件 (挂载 /opt/hms/static/)
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
container_name: hms-server
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
env_file:
|
||||
- .env.production
|
||||
volumes:
|
||||
- ../uploads:/app/uploads
|
||||
- ../config:/app/config:ro
|
||||
- ../static:/app/static
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 60s
|
||||
retries: 3
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "2"
|
||||
memory: 1024M
|
||||
reservations:
|
||||
cpus: "0.5"
|
||||
memory: 256M
|
||||
173
docker/docker-compose.production.yml
Normal file
173
docker/docker-compose.production.yml
Normal file
@@ -0,0 +1,173 @@
|
||||
# 生产环境 Docker Compose 配置
|
||||
# 使用方式: docker compose -f docker/docker-compose.yml -f docker/docker-compose.production.yml up -d
|
||||
|
||||
services:
|
||||
# ── Nginx 反代 + TLS 终端 ──
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
container_name: hms-nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
- nginx_logs:/var/log/nginx
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:80"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "0.5"
|
||||
memory: 128M
|
||||
networks:
|
||||
- hms-internal
|
||||
|
||||
# ── HMS 应用服务器 ──
|
||||
app:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
container_name: hms-server
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "3000"
|
||||
- "9090"
|
||||
env_file:
|
||||
- .env.production
|
||||
environment:
|
||||
ERP__DATABASE__URL: postgres://${POSTGRES_USER:-erp}:${POSTGRES_PASSWORD}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-erp}
|
||||
ERP__REDIS__URL: redis://:${REDIS_PASSWORD}@redis:${REDIS_PORT:-6379}
|
||||
volumes:
|
||||
- app-uploads:/app/uploads
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:3000/api/v1/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
start_period: 60s
|
||||
retries: 3
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "2"
|
||||
memory: 1024M
|
||||
reservations:
|
||||
cpus: "0.5"
|
||||
memory: 256M
|
||||
networks:
|
||||
- hms-internal
|
||||
|
||||
# ── 每日自动备份(含加密)──
|
||||
backup:
|
||||
image: postgres:16-alpine
|
||||
container_name: hms-backup
|
||||
restart: unless-stopped
|
||||
entrypoint: >
|
||||
sh -c "
|
||||
echo '$$BACKUP_CRON /usr/local/bin/backup.sh' > /etc/crontabs/root &&
|
||||
crond -f -l 2
|
||||
"
|
||||
environment:
|
||||
PGHOST: postgres
|
||||
PGPORT: "${POSTGRES_PORT:-5432}"
|
||||
PGUSER: "${POSTGRES_USER:-erp}"
|
||||
PGDATABASE: "${POSTGRES_DB:-erp}"
|
||||
BACKUP_DIR: /backups
|
||||
KEEP_DAYS: "${BACKUP_KEEP_DAYS:-7}"
|
||||
BACKUP_CRON: "${BACKUP_CRON:-0 2 * * *}"
|
||||
BACKUP_PASSPHRASE: "${BACKUP_PASSPHRASE:-}"
|
||||
volumes:
|
||||
- ./backup.sh:/usr/local/bin/backup.sh:ro
|
||||
- backup_data:/backups
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- hms-internal
|
||||
|
||||
# ── uploads 文件备份(同步到宿主机)──
|
||||
uploads-backup:
|
||||
image: alpine:3.20
|
||||
container_name: hms-uploads-backup
|
||||
restart: unless-stopped
|
||||
entrypoint: >
|
||||
sh -c "
|
||||
echo '$$UPLOADS_BACKUP_CRON rsync -a --delete /source/uploads/ /backup/uploads/' > /etc/crontabs/root &&
|
||||
crond -f -l 2
|
||||
"
|
||||
environment:
|
||||
UPLOADS_BACKUP_CRON: "${UPLOADS_BACKUP_CRON:-0 3 * * *}"
|
||||
volumes:
|
||||
- app-uploads:/source/uploads:ro
|
||||
- uploads_backup_data:/backup/uploads
|
||||
networks:
|
||||
- hms-internal
|
||||
|
||||
# ── Prometheus 监控 ──
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.1.0
|
||||
container_name: hms-prometheus
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.retention.time=30d"
|
||||
- "--storage.tsdb.retention.size=2GB"
|
||||
- "--web.enable-lifecycle"
|
||||
volumes:
|
||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
|
||||
- prometheus_data:/prometheus
|
||||
expose:
|
||||
- "9090"
|
||||
networks:
|
||||
- hms-internal
|
||||
|
||||
# ── Grafana 可视化 ──
|
||||
grafana:
|
||||
image: grafana/grafana:11.4.0
|
||||
container_name: hms-grafana
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GF_SECURITY_ADMIN_USER: "${GRAFANA_ADMIN_USER:-admin}"
|
||||
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD:-}"
|
||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
||||
GF_SERVER_ROOT_URL: "${GRAFANA_ROOT_URL:-http://localhost:3001}"
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./grafana/provisioning:/etc/grafana/provisioning:ro
|
||||
expose:
|
||||
- "3000"
|
||||
depends_on:
|
||||
- prometheus
|
||||
networks:
|
||||
- hms-internal
|
||||
|
||||
volumes:
|
||||
app-uploads:
|
||||
driver: local
|
||||
backup_data:
|
||||
driver: local
|
||||
uploads_backup_data:
|
||||
driver: local
|
||||
nginx_logs:
|
||||
driver: local
|
||||
prometheus_data:
|
||||
driver: local
|
||||
grafana_data:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
hms-internal:
|
||||
driver: bridge
|
||||
50
docker/docker-compose.yml
Normal file
50
docker/docker-compose.yml
Normal file
@@ -0,0 +1,50 @@
|
||||
version: "3.8"
|
||||
|
||||
# WARNING: 生产环境必须通过 .env 文件或环境变量覆盖默认密码
|
||||
# 不要在生产环境使用默认密码
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: erp-postgres
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-erp}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-erp_dev_2024}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-erp}
|
||||
expose:
|
||||
- "5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-erp}"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: 512M
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: erp-redis
|
||||
command: redis-server --requirepass ${REDIS_PASSWORD:-erp_redis_dev} --appendonly yes
|
||||
expose:
|
||||
- "6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-erp_redis_dev}", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.0"
|
||||
memory: 512M
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
96
docker/nginx/nginx.conf
Normal file
96
docker/nginx/nginx.conf
Normal file
@@ -0,0 +1,96 @@
|
||||
upstream hms_backend {
|
||||
server app:3000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name _;
|
||||
|
||||
# ── TLS ──
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
|
||||
# ── 安全头 ──
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self' wss:; frame-ancestors 'none'" always;
|
||||
|
||||
# ── 日志 ──
|
||||
access_log /var/log/nginx/hms_access.log;
|
||||
error_log /var/log/nginx/hms_error.log warn;
|
||||
|
||||
# ── 上传文件(化验单/体检报告)──
|
||||
location /uploads/ {
|
||||
proxy_pass http://hms_backend;
|
||||
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;
|
||||
# 大文件上传限制
|
||||
client_max_body_size 50m;
|
||||
}
|
||||
|
||||
# ── SSE(消息推送/AI 分析)──
|
||||
location ~ ^/api/v1/(message|ai)/.*sse {
|
||||
proxy_pass http://hms_backend;
|
||||
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;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 86400s;
|
||||
chunked_transfer_encoding on;
|
||||
}
|
||||
|
||||
# ── API 反代 ──
|
||||
location /api/ {
|
||||
proxy_pass http://hms_backend;
|
||||
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;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
client_max_body_size 50m;
|
||||
}
|
||||
|
||||
# ── 健康检查 ──
|
||||
location /health {
|
||||
proxy_pass http://hms_backend/api/v1/health;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# ── 指标(仅内网可访问)──
|
||||
location /metrics {
|
||||
# 生产环境应限制为 Prometheus 访问
|
||||
allow 172.16.0.0/12;
|
||||
allow 10.0.0.0/8;
|
||||
deny all;
|
||||
proxy_pass http://hms_backend:9090/metrics;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
3
docker/nginx/ssl/.gitignore
vendored
Normal file
3
docker/nginx/ssl/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*
|
||||
!.gitkeep
|
||||
!.gitignore
|
||||
8
docker/nginx/ssl/.gitkeep
Normal file
8
docker/nginx/ssl/.gitkeep
Normal file
@@ -0,0 +1,8 @@
|
||||
# 将 SSL 证书放置在此目录
|
||||
# 必需文件: fullchain.pem + privkey.pem
|
||||
# 生产环境建议使用 Let's Encrypt 或云服务商证书管理
|
||||
#
|
||||
# Let's Encrypt 示例:
|
||||
# certbot certonly --standalone -d your-domain.com
|
||||
# cp /etc/letsencrypt/live/your-domain.com/fullchain.pem .
|
||||
# cp /etc/letsencrypt/live/your-domain.com/privkey.pem .
|
||||
103
docker/prometheus/alerts.yml
Normal file
103
docker/prometheus/alerts.yml
Normal file
@@ -0,0 +1,103 @@
|
||||
groups:
|
||||
# ── 系统级告警 ──
|
||||
- name: system
|
||||
rules:
|
||||
- alert: HMSHighMemoryUsage
|
||||
expr: process_resident_memory_bytes > 800000000
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "HMS 内存使用超过 800MB"
|
||||
description: "当前值: {{ $value | humanize }}B"
|
||||
|
||||
- alert: HMSHighMemoryCritical
|
||||
expr: process_resident_memory_bytes > 1000000000
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "HMS 内存使用超过 1GB(危险)"
|
||||
description: "当前值: {{ $value | humanize }}B"
|
||||
|
||||
- alert: HMSHighCPU
|
||||
expr: rate(process_cpu_seconds_total[5m]) > 0.8
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "HMS CPU 使用率超过 80%"
|
||||
|
||||
# ── 应用级告警 ──
|
||||
- name: application
|
||||
rules:
|
||||
- alert: HMSHighErrorRate
|
||||
expr: rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "API 5xx 错误率超过 5%"
|
||||
description: "当前错误率: {{ $value | humanizePercentage }}"
|
||||
|
||||
- alert: HMSSlowResponses
|
||||
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 2
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "95% 请求响应时间超过 2 秒"
|
||||
|
||||
- alert: HMSInstanceDown
|
||||
expr: up{job="hms"} == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "HMS 服务不可达"
|
||||
|
||||
# ── 数据库告警 ──
|
||||
- name: database
|
||||
rules:
|
||||
- alert: HMSPostgresConnectionsHigh
|
||||
expr: pg_stat_activity_count > 80
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "PostgreSQL 活跃连接数超过 80"
|
||||
|
||||
- alert: HMSPostgresReplicationLag
|
||||
expr: pg_replication_lag > 30
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "PostgreSQL 复制延迟超过 30 秒"
|
||||
|
||||
- alert: HMSBackupMissing
|
||||
expr: time() - hms_last_backup_timestamp > 86400 * 2
|
||||
for: 1h
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "数据库备份超过 48 小时未执行"
|
||||
|
||||
# ── Redis 告警 ──
|
||||
- name: redis
|
||||
rules:
|
||||
- alert: HMSRedisMemoryHigh
|
||||
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.9
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Redis 内存使用超过 90%"
|
||||
|
||||
- alert: HMSRedisDown
|
||||
expr: redis_up == 0
|
||||
for: 2m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Redis 服务不可达"
|
||||
32
docker/prometheus/prometheus.yml
Normal file
32
docker/prometheus/prometheus.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
rule_files:
|
||||
- "alerts.yml"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "hms"
|
||||
metrics_path: /metrics
|
||||
static_configs:
|
||||
- targets: ["app:9090"]
|
||||
labels:
|
||||
service: "hms-server"
|
||||
|
||||
- job_name: "postgres"
|
||||
static_configs:
|
||||
- targets: ["postgres-exporter:9187"]
|
||||
labels:
|
||||
service: "postgresql"
|
||||
|
||||
- job_name: "redis"
|
||||
static_configs:
|
||||
- targets: ["redis-exporter:9121"]
|
||||
labels:
|
||||
service: "redis"
|
||||
|
||||
- job_name: "nginx"
|
||||
static_configs:
|
||||
- targets: ["nginx-exporter:9113"]
|
||||
labels:
|
||||
service: "nginx"
|
||||
43
docker/restore.sh
Normal file
43
docker/restore.sh
Normal file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostgreSQL 备份恢复脚本
|
||||
# 用法: BACKUP_PASSPHRASE=xxx ./docker/restore.sh /backups/erp_20260521_020000.sql.gz.enc
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_FILE="${1:?用法: restore.sh <备份文件路径>}"
|
||||
PG_HOST="${PGHOST:-postgres}"
|
||||
PG_PORT="${PGPORT:-5432}"
|
||||
PG_USER="${PGUSER:-erp}"
|
||||
PG_DB="${PGDATABASE:-erp}"
|
||||
|
||||
if [ ! -f "${BACKUP_FILE}" ]; then
|
||||
echo "错误: 文件不存在: ${BACKUP_FILE}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[$(date -Iseconds)] 恢复目标: ${PG_HOST}:${PG_PORT}/${PG_DB}"
|
||||
echo "[$(date -Iseconds)] 备份文件: ${BACKUP_FILE}"
|
||||
|
||||
# 解密(如果是加密文件)
|
||||
if [[ "${BACKUP_FILE}" == *.enc ]]; then
|
||||
if [ -z "${BACKUP_PASSPHRASE:-}" ]; then
|
||||
echo "错误: 加密备份需要设置 BACKUP_PASSPHRASE 环境变量" >&2
|
||||
exit 1
|
||||
fi
|
||||
DECRYPTED="${BACKUP_FILE%.enc}"
|
||||
echo "[$(date -Iseconds)] 解密中..."
|
||||
openssl enc -d -aes-256-cbc -pbkdf2 -pass "pass:${BACKUP_PASSPHRASE}" \
|
||||
-in "${BACKUP_FILE}" -out "${DECRYPTED}"
|
||||
BACKUP_FILE="${DECRYPTED}"
|
||||
fi
|
||||
|
||||
# 解压并恢复
|
||||
echo "[$(date -Iseconds)] 恢复中..."
|
||||
gunzip -c "${BACKUP_FILE}" | psql -h "${PG_HOST}" -p "${PG_PORT}" -U "${PG_USER}" -d "${PG_DB}"
|
||||
|
||||
echo "[$(date -Iseconds)] 恢复完成"
|
||||
|
||||
# 清理解密文件
|
||||
if [ -n "${DECRYPTED:-}" ] && [ -f "${DECRYPTED}" ]; then
|
||||
rm -f "${DECRYPTED}"
|
||||
echo "[$(date -Iseconds)] 已清理解密临时文件"
|
||||
fi
|
||||
Reference in New Issue
Block a user