chore: establish V1.1 recovery baseline
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — 生产环境环境变量
|
||||
# ============================================
|
||||
# ⚠️ 此文件为模板,请勿提交真实值到 Git
|
||||
# ⚠️ 生产密钥必须通过 k8s secret / Vault / 密钥管理服务注入
|
||||
# ============================================
|
||||
|
||||
# --- Database ---
|
||||
DB_NAME=danyang_match
|
||||
DB_USER=danyang
|
||||
DB_PASSWORD_FILE=/run/secrets/db_password # Docker Swarm secret 文件路径
|
||||
# PostgreSQL 连接参数(如不使用 Docker secrets 则直接设置 DB_PASSWORD)
|
||||
# DB_PASSWORD=your-secure-password-here
|
||||
|
||||
# --- Redis ---
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your-redis-password-here
|
||||
|
||||
# --- MinIO (对象存储) ---
|
||||
MINIO_ENDPOINT=http://minio:9000
|
||||
MINIO_ACCESS_KEY=your-minio-access-key
|
||||
MINIO_SECRET_KEY=your-minio-secret-key
|
||||
|
||||
# --- JWT ---
|
||||
JWT_ACCESS_SECRET=your-256-bit-jwt-access-secret-here-change-in-production
|
||||
JWT_REFRESH_SECRET=your-256-bit-jwt-refresh-secret-here-change-in-production
|
||||
|
||||
# --- Checkin Token ---
|
||||
CHECKIN_TOKEN_SECRET=your-hmac-checkin-secret-here-change-in-production
|
||||
|
||||
# --- AES-256 Encryption Key (32 bytes = 64 hex chars) ---
|
||||
ENCRYPTION_KEY=32539f3d2fa8123dcec899cc3dbee7347034123a126ac22fcb4702e10c454d50
|
||||
|
||||
# --- WeChat Mini Program ---
|
||||
WECHAT_APP_ID=your-wechat-app-id
|
||||
WECHAT_APP_SECRET=your-wechat-app-secret
|
||||
|
||||
# --- Feature Flags ---
|
||||
FEATURES_APPEAL_ENABLED=false
|
||||
|
||||
# --- Spring Boot ---
|
||||
SPRING_PROFILES_ACTIVE=production
|
||||
@@ -0,0 +1,95 @@
|
||||
# Staging Docker Compose(预发环境,仅开发/测试)
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_DB: danyang_match_staging
|
||||
POSTGRES_USER: staging
|
||||
POSTGRES_PASSWORD: staging_dev_password_change_me
|
||||
ports:
|
||||
- "5433:5432"
|
||||
volumes:
|
||||
- postgres_staging_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U staging -d danyang_match_staging"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6380:6379"
|
||||
command: redis-server --appendonly yes --requirepass staging-redis-pass-change-me
|
||||
volumes:
|
||||
- redis_staging_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "-a", "staging-redis-pass-change-me", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: staging
|
||||
MINIO_ROOT_PASSWORD: staging_dev_password
|
||||
ports:
|
||||
- "9010:9000"
|
||||
- "9011:9001"
|
||||
volumes:
|
||||
- minio_staging_data:/data
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
container_name: danyang-match-staging-backend
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: staging
|
||||
DB_HOST: postgres
|
||||
DB_PORT: 5432
|
||||
DB_NAME: danyang_match_staging
|
||||
DB_USER: staging
|
||||
DB_PASSWORD: staging_dev_password_change_me
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: staging-redis-pass-change-me
|
||||
MINIO_ENDPOINT: http://minio:9000
|
||||
MINIO_ACCESS_KEY: staging
|
||||
MINIO_SECRET_KEY: staging_dev_password
|
||||
JWT_ACCESS_SECRET: staging-access-secret-dev-only
|
||||
JWT_REFRESH_SECRET: staging-refresh-secret-dev-only
|
||||
CHECKIN_TOKEN_SECRET: staging-checkin-secret-dev-only
|
||||
ENCRYPTION_KEY: 32539f3d2fa8123dcec899cc3dbee7347034123a126ac22fcb4702e10c454d50
|
||||
FEATURES_APPEAL_ENABLED: "true"
|
||||
ports:
|
||||
- "8081:8080"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/health/ready || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
admin-web:
|
||||
build:
|
||||
context: ../admin-web
|
||||
dockerfile: Dockerfile
|
||||
container_name: danyang-match-staging-admin
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "3001:80"
|
||||
|
||||
volumes:
|
||||
postgres_staging_data:
|
||||
redis_staging_data:
|
||||
minio_staging_data:
|
||||
@@ -0,0 +1,152 @@
|
||||
# Production Nginx Configuration
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 2048;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format production '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'rt=$request_time';
|
||||
|
||||
access_log /var/log/nginx/access.log production;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss;
|
||||
|
||||
# Rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=50r/s;
|
||||
limit_req_zone $binary_remote_addr zone=auth:10m rate=10r/s;
|
||||
limit_req_zone $binary_remote_addr zone=upload:10m rate=5r/s;
|
||||
limit_req_zone $binary_remote_addr zone=ws:10m rate=20r/s;
|
||||
|
||||
# Maintenance mode flag
|
||||
map $http_x_maintenance_mode $maintenance_mode {
|
||||
default 0;
|
||||
"1" 1;
|
||||
}
|
||||
|
||||
upstream backend {
|
||||
server backend:8080;
|
||||
}
|
||||
|
||||
# HTTP -> HTTPS redirect (uncomment for production)
|
||||
# server {
|
||||
# listen 80;
|
||||
# server_name match.danyang.gov.cn;
|
||||
# return 301 https://$server_name$request_uri;
|
||||
# }
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name match.danyang.gov.cn;
|
||||
|
||||
# SSL
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
|
||||
# Security headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:;" always;
|
||||
|
||||
# Maintenance mode
|
||||
location = /maintenance {
|
||||
return 503 '<h1>系统维护中</h1><p>请稍后再访问。</p>';
|
||||
}
|
||||
|
||||
# Health checks (no auth needed)
|
||||
location = /api/v1/health/live {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
location = /api/v1/health/ready {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
location = /version {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Admin Web (SPA)
|
||||
location /admin/ {
|
||||
alias /usr/share/nginx/html/;
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api/ {
|
||||
limit_req zone=api burst=100 nodelay;
|
||||
proxy_pass http://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_connect_timeout 30s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
location /ws/ {
|
||||
limit_req zone=ws burst=20 nodelay;
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
|
||||
# Auth endpoints (strict rate limiting)
|
||||
location ~* ^/api/v1/auth/ {
|
||||
limit_req zone=auth burst=5 nodelay;
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# Upload endpoints
|
||||
location ~* ^/api/v1/photos/upload {
|
||||
limit_req zone=upload burst=5 nodelay;
|
||||
client_max_body_size 10M;
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
# MinIO public bucket (read-only)
|
||||
location /uploads/ {
|
||||
proxy_pass http://backend/;
|
||||
proxy_set_header Host $host;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'request_id=$upstream_http_x_request_id';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
|
||||
|
||||
# Rate limiting zone
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
|
||||
limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/s;
|
||||
limit_req_zone $binary_remote_addr zone=upload:10m rate=10r/s;
|
||||
|
||||
upstream backend {
|
||||
server backend:8080;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Health check for nginx itself
|
||||
location /nginx-health {
|
||||
return 200 'ok';
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# API proxy
|
||||
location /api/ {
|
||||
limit_req zone=api burst=50 nodelay;
|
||||
proxy_pass http://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_connect_timeout 30s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
|
||||
# WebSocket proxy
|
||||
location /ws/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
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_read_timeout 3600s;
|
||||
}
|
||||
|
||||
# Auth endpoints - stricter rate limit
|
||||
location /api/v1/auth/ {
|
||||
limit_req zone=auth burst=10 nodelay;
|
||||
proxy_pass http://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;
|
||||
}
|
||||
|
||||
# Admin web static files
|
||||
location /admin/ {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /admin/index.html;
|
||||
}
|
||||
|
||||
# Don't cache API responses
|
||||
location /api/ {
|
||||
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
|
||||
add_header Pragma "no-cache";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — PostgreSQL 备份脚本
|
||||
# ============================================
|
||||
# 用法:
|
||||
# ./scripts/backup.sh # 备份全部数据库
|
||||
# ./scripts/backup.sh danyang_match # 备份指定库
|
||||
# ./scripts/backup.sh --daily # 仅保留最近7天的备份
|
||||
# ============================================
|
||||
set -euo pipefail
|
||||
|
||||
DB_NAME="${1:-danyang_match}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-/app/backups}"
|
||||
RETENTION_DAYS="${RETENTION_DAYS:-30}"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始备份: ${DB_NAME}"
|
||||
|
||||
# 创建备份目录
|
||||
mkdir -p "${BACKUP_DIR}"
|
||||
|
||||
# 执行备份
|
||||
pg_dump \
|
||||
-h "${DB_HOST:-postgres}" \
|
||||
-U "${DB_USER:-danyang}" \
|
||||
-d "${DB_NAME}" \
|
||||
--no-owner \
|
||||
--no-privileges \
|
||||
--clean \
|
||||
--if-exists \
|
||||
--inserts \
|
||||
| gzip > "${BACKUP_FILE}"
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 备份完成: ${BACKUP_FILE}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 文件大小: $(du -h "${BACKUP_FILE}" | cut -f1)"
|
||||
|
||||
# 清理过期备份
|
||||
find "${BACKUP_DIR}" -name "${DB_NAME}_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 已清理 ${RETENTION_DAYS} 天前的备份"
|
||||
|
||||
# 保留最近 N 份(防止误删)
|
||||
BACKUP_COUNT=$(ls -1 "${BACKUP_DIR}"/${DB_NAME}_*.sql.gz 2>/dev/null | wc -l)
|
||||
if [ "${BACKUP_COUNT}" -gt 10 ]; then
|
||||
ls -1t "${BACKUP_DIR}"/${DB_NAME}_*.sql.gz | tail -n +11 | xargs rm -f
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 保留最近10份备份,其余已清理"
|
||||
fi
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 备份任务完成"
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — CI/CD 触发脚本
|
||||
# ============================================
|
||||
# 由 GitHub Actions 调用,执行以下步骤:
|
||||
# 1. 构建 Backend Docker 镜像
|
||||
# 2. 构建 Admin Web Docker 镜像
|
||||
# 3. 推送镜像到 Docker Hub / 私有仓库
|
||||
# 4. 触发远程部署
|
||||
# ============================================
|
||||
set -euo pipefail
|
||||
|
||||
IMAGE_REPO="${DOCKER_REGISTRY:-registry.cn-hangzhou.aliyuncs.com/danyang-match}"
|
||||
VERSION="${GITHUB_SHA:-latest}"
|
||||
BRANCH="${GITHUB_REF_NAME:-main}"
|
||||
|
||||
echo "============================================"
|
||||
echo " 丹阳公益婚恋平台 — CI 构建"
|
||||
echo " 分支: ${BRANCH} | 版本: ${VERSION}"
|
||||
echo "============================================"
|
||||
|
||||
# 构建 Backend 镜像
|
||||
echo "[1/3] 构建 Backend 镜像..."
|
||||
docker build -t "${IMAGE_REPO}/backend:${VERSION}" -f backend/Dockerfile backend/
|
||||
docker tag "${IMAGE_REPO}/backend:${VERSION}" "${IMAGE_REPO}/backend:latest"
|
||||
|
||||
# 构建 Admin Web 镜像
|
||||
echo "[2/3] 构建 Admin Web 镜像..."
|
||||
docker build -t "${IMAGE_REPO}/admin:${VERSION}" -f admin-web/Dockerfile admin-web/
|
||||
docker tag "${IMAGE_REPO}/admin:${VERSION}" "${IMAGE_REPO}/admin:latest"
|
||||
|
||||
# 推送镜像
|
||||
if [ "${BRANCH}" = "main" ] || [ "${BRANCH}" = "master" ]; then
|
||||
echo "[3/3] 推送镜像到仓库..."
|
||||
docker push "${IMAGE_REPO}/backend:${VERSION}"
|
||||
docker push "${IMAGE_REPO}/backend:latest"
|
||||
docker push "${IMAGE_REPO}/admin:${VERSION}"
|
||||
docker push "${IMAGE_REPO}/admin:latest"
|
||||
echo "✅ 镜像推送完成"
|
||||
else
|
||||
echo "⏭ 非主分支,跳过推送"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ CI 完成!版本: ${VERSION}"
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — 一键部署脚本
|
||||
# ============================================
|
||||
# 用法:
|
||||
# ./scripts/deploy.sh # 部署最新镜像
|
||||
# ./scripts/deploy.sh v1.2.3 # 部署指定版本
|
||||
# ./scripts/deploy.sh --rollback # 回滚到上一版本
|
||||
# ============================================
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:-latest}"
|
||||
ACTION="${2:-deploy}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-../infrastructure/docker-compose.prod.yml}"
|
||||
GIT_REPO="${GIT_REPO:-https://github.com/danyang-match/platform.git}"
|
||||
|
||||
echo "============================================"
|
||||
echo " 丹阳公益婚恋服务平台 — 部署脚本"
|
||||
echo " 版本: ${VERSION} | 动作: ${ACTION}"
|
||||
echo "============================================"
|
||||
|
||||
# 检查环境变量
|
||||
if [ -z "${DOCKER_REGISTRY:-}" ]; then
|
||||
echo "警告: DOCKER_REGISTRY 未设置,将使用本地构建"
|
||||
fi
|
||||
|
||||
# 进入基础设施目录
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
case "${ACTION}" in
|
||||
deploy)
|
||||
echo "[1/4] 拉取最新代码..."
|
||||
if [ -d ".git" ]; then
|
||||
git fetch origin
|
||||
git checkout main || git checkout master
|
||||
git pull --ff-only
|
||||
fi
|
||||
|
||||
echo "[2/4] 构建并启动服务..."
|
||||
# 设置版本号
|
||||
export APP_VERSION="${VERSION}"
|
||||
docker compose -f "${COMPOSE_FILE}" up -d --build --pull always
|
||||
|
||||
echo "[3/4] 等待服务健康检查..."
|
||||
sleep 10
|
||||
docker compose -f "${COMPOSE_FILE}" ps | grep -E "backend|postgres|redis" || true
|
||||
|
||||
echo "[4/4] 验证健康状态..."
|
||||
curl -sf "http://localhost/api/v1/health/live" && echo " ✅ 后端健康" || echo " ❌ 后端不健康"
|
||||
curl -sf "http://localhost/version" && echo " ✅ 版本信息" || echo " ❌ 版本接口失败"
|
||||
|
||||
echo ""
|
||||
echo "✅ 部署完成!版本: ${VERSION}"
|
||||
echo " 访问地址: https://match.danyang.gov.cn/admin/"
|
||||
;;
|
||||
|
||||
rollback)
|
||||
echo "[1/2] 回滚到上一版本..."
|
||||
# 查找上一个 image tag
|
||||
PREV_VERSION=$(docker images --format '{{.Tag}}' danyang-match-backend 2>/dev/null | grep -v "^latest$" | sort -V | tail -2 | head -1)
|
||||
if [ -z "${PREV_VERSION}" ]; then
|
||||
echo "错误: 找不到可回滚的镜像版本"
|
||||
exit 1
|
||||
fi
|
||||
echo " 回滚版本: ${PREV_VERSION}"
|
||||
|
||||
export APP_VERSION="${PREV_VERSION}"
|
||||
docker compose -f "${COMPOSE_FILE}" up -d --no-build backend
|
||||
|
||||
echo "[2/2] 验证回滚..."
|
||||
sleep 5
|
||||
curl -sf "http://localhost/api/v1/health/live" && echo " ✅ 回滚成功" || echo " ❌ 回滚后不健康"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "用法: $0 [version] [deploy|rollback]"
|
||||
echo "示例:"
|
||||
echo " $0 v1.2.3 deploy"
|
||||
echo " $0 rollback"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — 上线首日监控检查
|
||||
# ============================================
|
||||
set -euo pipefail
|
||||
|
||||
echo "============================================"
|
||||
echo " 丹阳公益婚恋平台 — 上线首日监控"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# 1. 服务健康
|
||||
echo "【1. 服务健康】"
|
||||
docker compose -f "${COMPOSE_FILE:-../infrastructure/docker-compose.prod.yml}" ps 2>/dev/null | grep -E "backend|postgres|redis|nginx" || echo " ⚠️ Docker Compose未运行,跳过容器检查"
|
||||
|
||||
# 2. 错误日志
|
||||
echo ""
|
||||
echo "【2. 错误日志检查(最近100行)】"
|
||||
docker logs danyang-match-backend --tail 100 2>/dev/null | grep -i "error\|exception\|fatal" | tail -5 || echo " ✅ 无ERROR日志"
|
||||
|
||||
# 3. 数据库连接
|
||||
echo ""
|
||||
echo "【3. 数据库连接】"
|
||||
docker exec danyang-match-postgres pg_isready -U "${DB_USER:-danyang}" 2>/dev/null && \
|
||||
echo " ✅ PostgreSQL连接正常" || echo " ❌ PostgreSQL连接异常"
|
||||
|
||||
# 4. Redis连接
|
||||
echo ""
|
||||
echo "【4. Redis连接】"
|
||||
docker exec danyang-match-redis redis-cli ping 2>/dev/null | grep -q PONG && \
|
||||
echo " ✅ Redis连接正常" || echo " ⚠️ Redis不可用(可接受:非核心依赖)"
|
||||
|
||||
# 5. 磁盘空间
|
||||
echo ""
|
||||
echo "【5. 磁盘空间】"
|
||||
DISK_USAGE=$(df -h / 2>/dev/null | awk 'NR==2{print $5}' || echo "unknown")
|
||||
echo " 根分区使用: ${DISK_USAGE}"
|
||||
if [ "${DISK_USAGE%"%"}" -gt 80 ] 2>/dev/null; then
|
||||
echo " ⚠️ 磁盘使用超过80%,请关注"
|
||||
fi
|
||||
|
||||
# 6. 内存使用
|
||||
echo ""
|
||||
echo "【6. 内存使用】"
|
||||
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}" 2>/dev/null || true
|
||||
|
||||
# 7. 关键API响应时间
|
||||
echo ""
|
||||
echo "【7. API响应时间】"
|
||||
for endpoint in "/api/v1/health/live" "/api/v1/users" "/api/v1/matches/recommendations"; do
|
||||
TIME=$(curl -s -o /dev/null -w "%{time_total}s" "http://localhost:8080${endpoint}" 2>/dev/null || echo "N/A")
|
||||
echo " ${endpoint}: ${TIME}"
|
||||
done
|
||||
|
||||
# 8. 举报/申诉数量
|
||||
echo ""
|
||||
echo "【8. 业务指标(生产环境)】"
|
||||
echo " ⚠️ 请通过后台管理界面查看实时统计数据"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " 上线首日监控完成"
|
||||
echo " 如发现异常,请立即执行:"
|
||||
echo " docker compose -f infrastructure/docker-compose.prod.yml restart"
|
||||
echo " 或联系技术负责人"
|
||||
echo "============================================"
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — 灰度发布脚本
|
||||
# ============================================
|
||||
# 用法:
|
||||
# ./scripts/gray-release.sh 10 # 10%流量灰度
|
||||
# ./scripts/gray-release.sh 50 # 50%流量
|
||||
# ./scripts/gray-release.sh 100 # 全量发布
|
||||
# ============================================
|
||||
set -euo pipefail
|
||||
|
||||
PERCENTAGE="${1:-10}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-../infrastructure/docker-compose.prod.yml}"
|
||||
|
||||
if [ "${PERCENTAGE}" -lt 1 ] || [ "${PERCENTAGE}" -gt 100 ]; then
|
||||
echo "错误: 灰度比例应在1-100之间"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo " 丹阳公益婚恋平台 — 灰度发布"
|
||||
echo " 灰度比例: ${PERCENTAGE}%"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================"
|
||||
|
||||
# 步骤1: 启动新版本容器(不暴露端口)
|
||||
echo "[1/4] 启动新版本容器(隐藏端口,不接收流量)..."
|
||||
docker compose -f "${COMPOSE_FILE}" up -d --no-recreate backend-new \
|
||||
-e APP_VERSION="gray-${PERCENTAGE}pct" || {
|
||||
echo "❌ 新版本容器启动失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 步骤2: 健康检查新版本
|
||||
echo "[2/4] 健康检查新版本..."
|
||||
sleep 15
|
||||
if curl -sf "http://localhost:8081/api/v1/health/ready" > /dev/null 2>&1; then
|
||||
echo " ✅ 新版本健康检查通过"
|
||||
else
|
||||
echo " ⚠️ 新版本健康检查失败,回滚中..."
|
||||
docker compose -f "${COMPOSE_FILE}" down backend-new
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 步骤3: 更新Nginx配置(通过环境变量控制流量比例)
|
||||
echo "[3/4] 更新Nginx流量分配..."
|
||||
# 实际生产环境使用环境变量或配置中心控制
|
||||
echo " 灰度比例已设置为 ${PERCENTAGE}%"
|
||||
echo " 请通过配置中心或Nginx upstream权重调整流量分配"
|
||||
|
||||
# 步骤4: 监控新版本指标
|
||||
echo "[4/4] 监控新版本指标(30秒观察期)..."
|
||||
sleep 30
|
||||
echo " 观察期完成。如发现问题,执行以下命令回滚:"
|
||||
echo " docker compose -f ${COMPOSE_FILE} down backend-new"
|
||||
echo " docker compose -f ${COMPOSE_FILE} up -d backend"
|
||||
|
||||
echo ""
|
||||
echo "✅ 灰度发布配置完成!比例: ${PERCENTAGE}%"
|
||||
echo ""
|
||||
echo "回滚指令:"
|
||||
echo " ./scripts/gray-release.sh rollback"
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — 健康检查与监控脚本
|
||||
# ============================================
|
||||
# 用法:
|
||||
# ./scripts/health-check.sh # 检查所有服务
|
||||
# ./scripts/health-check.sh --detail # 详细输出
|
||||
# ============================================
|
||||
set -euo pipefail
|
||||
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-../infrastructure/docker-compose.prod.yml}"
|
||||
DETAIL="${2:-}"
|
||||
EXIT_CODE=0
|
||||
|
||||
check_service() {
|
||||
local name="$1"
|
||||
local check="$2"
|
||||
local result
|
||||
result=$(eval "$check" 2>/dev/null || echo "FAIL")
|
||||
if [ "${result}" = "OK" ]; then
|
||||
echo " ✅ ${name}: ${result}"
|
||||
else
|
||||
echo " ❌ ${name}: ${result}"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "============================================"
|
||||
echo " 丹阳公益婚恋服务平台 — 健康检查"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================"
|
||||
|
||||
# 容器状态
|
||||
echo ""
|
||||
echo "【容器状态】"
|
||||
docker compose -f "${COMPOSE_FILE}" ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null | head -10
|
||||
|
||||
# 服务检查
|
||||
echo ""
|
||||
echo "【服务检查】"
|
||||
check_service "PostgreSQL" \
|
||||
"pg_isready -h postgres -U ${DB_USER:-danyang} -d ${DB_NAME:-danyang_match} 2>/dev/null && echo OK || echo FAIL"
|
||||
|
||||
check_service "Redis" \
|
||||
"redis-cli -h redis${REDIS_PASSWORD:+ -a ${REDIS_PASSWORD}} ping 2>/dev/null | grep -q PONG && echo OK || echo FAIL"
|
||||
|
||||
check_service "MinIO" \
|
||||
"curl -sf http://minio:9000/minio/health/live 2>/dev/null | grep -q '\"status\":\"online\"' && echo OK || echo FAIL"
|
||||
|
||||
check_service "Backend API" \
|
||||
"curl -sf http://localhost:8080/api/v1/health/live 2>/dev/null | grep -q success && echo OK || echo FAIL"
|
||||
|
||||
check_service "Backend Ready" \
|
||||
"curl -sf http://localhost:8080/api/v1/health/ready 2>/dev/null | grep -q success && echo OK || echo FAIL"
|
||||
|
||||
check_service "Nginx" \
|
||||
"curl -sf http://localhost/nginx-health 2>/dev/null && echo OK || echo FAIL"
|
||||
|
||||
# 详细输出
|
||||
if [ "${DETAIL}" = "--detail" ]; then
|
||||
echo ""
|
||||
echo "【磁盘使用】"
|
||||
docker system df 2>/dev/null || true
|
||||
|
||||
echo ""
|
||||
echo "【内存使用】"
|
||||
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ ${EXIT_CODE} -eq 0 ]; then
|
||||
echo "✅ 所有服务正常"
|
||||
else
|
||||
echo "❌ 存在异常服务,请检查上方输出"
|
||||
fi
|
||||
|
||||
exit ${EXIT_CODE}
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — PostgreSQL 恢复脚本
|
||||
# ============================================
|
||||
# 用法:
|
||||
# ./scripts/restore.sh <backup-file>
|
||||
# ./scripts/restore.sh backups/danyang_match_20260828_143000.sql.gz
|
||||
# ============================================
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "用法: $0 <备份文件路径>"
|
||||
echo "示例: $0 /app/backups/danyang_match_20260828_143000.sql.gz"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_FILE="$1"
|
||||
|
||||
if [ ! -f "${BACKUP_FILE}" ]; then
|
||||
echo "错误: 备份文件不存在: ${BACKUP_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DB_NAME="${DB_NAME:-danyang_match}"
|
||||
DB_USER="${DB_USER:-danyang}"
|
||||
DB_HOST="${DB_HOST:-postgres}"
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 开始恢复: ${BACKUP_FILE} -> ${DB_NAME}"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ 将覆盖现有数据库,请确认!"
|
||||
|
||||
# 恢复数据
|
||||
gunzip -c "${BACKUP_FILE}" | psql \
|
||||
-h "${DB_HOST}" \
|
||||
-U "${DB_USER}" \
|
||||
-d "${DB_NAME}" \
|
||||
--On-error-stop
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 恢复完成"
|
||||
|
||||
# 验证
|
||||
TABLE_COUNT=$(psql -h "${DB_HOST}" -U "${DB_USER}" -d "${DB_NAME}" -tAc \
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public'")
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 验证: 公共 schema 下 ${TABLE_COUNT} 张表"
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# 丹阳公益婚恋服务平台 — 生产上线冒烟测试
|
||||
# ============================================
|
||||
set -euo pipefail
|
||||
|
||||
BASE_URL="${BASE_URL:-https://match.danyang.gov.cn}"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
check() {
|
||||
local name="$1"
|
||||
local url="$2"
|
||||
local expected="$3"
|
||||
local code
|
||||
code=$(curl -sf -o /dev/null -w "%{http_code}" "${BASE_URL}${url}" 2>/dev/null || echo "000")
|
||||
if [ "$code" = "$expected" ]; then
|
||||
PASS=$((PASS + 1))
|
||||
echo " ✅ $name (HTTP $code)"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
echo " ❌ $name (期望$expected, 实际$code)"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "============================================"
|
||||
echo " 丹阳公益婚恋平台 — 生产冒烟测试"
|
||||
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
echo "【健康检查】"
|
||||
check "后端live" "/api/v1/health/live" "200"
|
||||
check "后端ready" "/api/v1/health/ready" "200"
|
||||
check "版本信息" "/version" "200"
|
||||
|
||||
echo ""
|
||||
echo "【认证流程】"
|
||||
check "微信登录端点" "/api/v1/auth/wechat/login" "401" # 无code参数应400或401
|
||||
|
||||
echo ""
|
||||
echo "【核心功能】"
|
||||
check "用户资料接口" "/api/v1/profile" "401" # 未登录应401
|
||||
check "推荐列表接口" "/api/v1/matches/recommendations" "401"
|
||||
|
||||
echo ""
|
||||
echo "【安全接口】"
|
||||
check "管理员接口无认证拒绝" "/api/v1/operator/users" "401"
|
||||
check "举报提交接口" "/api/v1/risk/reports" "401"
|
||||
|
||||
echo ""
|
||||
echo "【Nginx】"
|
||||
check "Nginx首页" "/" "200"
|
||||
check "HTTPS生效" "https://${BASE_URL##*://}/" "200"
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " 结果: PASS=${PASS} FAIL=${FAIL}"
|
||||
echo "============================================"
|
||||
[ ${FAIL} -eq 0 ] && echo "✅ 冒烟测试通过" && exit 0 || echo "❌ 存在${FAIL}个失败项" && exit 1
|
||||
Reference in New Issue
Block a user