feat(P2): 实现质量保障类5项功能(无障碍/视觉回归/通知渠道/漏洞扫描/灾备)
## 新增功能 ### 1. 屏幕阅读器兼容性增强(a11y) - 无障碍工具库:src/shared/lib/a11y.ts - aria-live Hook:src/shared/hooks/use-aria-live.ts - a11y 组件:skip-link/visually-hidden/focus-trap/aria-status - 增强 UI:table.tsx 系统性 ARIA role,dialog.tsx aria-modal - 审计文档:docs/accessibility/a11y-audit.md(WCAG 2.1 AA 清单) ### 2. 视觉回归测试 - 测试套件:tests/visual/(homepage + 3 个 dashboard) - 3 视口(desktop/tablet/mobile)× 2 主题(light/dark) - 动态元素遮罩,避免误报 - playwright.config.ts 新增 visual-chromium 项目 - 文档:docs/testing/visual-regression.md ### 3. 短信/微信推送渠道集成 - 新模块:src/modules/notifications/ - 4 个渠道:SMS(阿里云/腾讯云)、WeChat(公众号)、Email(SMTP)、In-App - 分发器按用户偏好并行多渠道发送 - 外部 SDK 动态 import,Mock 模式开发可用 - 文档:docs/notifications/channels.md ### 4. 漏洞扫描 CI 集成 - CI security-scan job:npm audit + Snyk + Trivy FS + OWASP ZAP - 独立工作流 security.yml:每周一深度扫描 + 容器镜像扫描 - 配置:suppressions.json + .trivyignore - 本地脚本:security-scan.sh/ps1 - 文档:docs/security/scanning.md(SLA 分级) ### 5. 灾备方案 - 脚本:backup-verify/backup-offsite-sync/dr-drill/failover/health-check - CI 增强:备份后校验+异地同步,每周灾备演练 - 独立工作流 dr-drill.yml:每周一凌晨 4 点自动演练 - 文档:docs/dr/dr-plan.md(RTO 4h/RPO 24h)+ dr-runbook.md(6 故障场景) ## 验证 - npx tsc --noEmit:0 错误 - npm run lint:0 错误 0 警告
This commit is contained in:
253
scripts/health-check.sh
Normal file
253
scripts/health-check.sh
Normal file
@@ -0,0 +1,253 @@
|
||||
#!/bin/bash
|
||||
# 健康检查脚本
|
||||
# 用法: ./health-check.sh
|
||||
# 检查应用、数据库、磁盘空间、备份新鲜度,输出 JSON 报告
|
||||
|
||||
set -u
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
用法: $0 [选项]
|
||||
系统健康检查脚本,输出 JSON 格式报告
|
||||
|
||||
选项:
|
||||
--app-url URL 应用健康检查 URL(默认 http://localhost:8015)
|
||||
--no-app 跳过应用健康检查
|
||||
--no-db 跳过数据库检查
|
||||
--no-disk 跳过磁盘空间检查
|
||||
--no-backup 跳过备份新鲜度检查
|
||||
--disk-threshold PCT 磁盘空间阈值百分比(默认 90)
|
||||
--backup-max-age HRS 备份最大年龄(小时,默认 24)
|
||||
--help, -h 显示帮助信息
|
||||
|
||||
环境变量:
|
||||
DATABASE_URL 数据库连接 URL
|
||||
HEALTH_CHECK_URL 应用健康检查 URL(默认 http://localhost:8015)
|
||||
BACKUP_DIR 备份目录(默认 ./backups)
|
||||
HEALTH_CHECK_DISK_THRESHOLD 磁盘阈值(默认 90)
|
||||
HEALTH_CHECK_BACKUP_MAX_AGE 备份最大年龄(小时,默认 24)
|
||||
|
||||
退出码:
|
||||
0 健康
|
||||
1 异常
|
||||
EOF
|
||||
}
|
||||
|
||||
# 解析参数
|
||||
CHECK_APP=1
|
||||
CHECK_DB=1
|
||||
CHECK_DISK=1
|
||||
CHECK_BACKUP=1
|
||||
APP_URL=""
|
||||
DISK_THRESHOLD=""
|
||||
BACKUP_MAX_AGE=""
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
--app-url)
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "ERROR: --app-url requires an argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
APP_URL="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-app) CHECK_APP=0; shift ;;
|
||||
--no-db) CHECK_DB=0; shift ;;
|
||||
--no-disk) CHECK_DISK=0; shift ;;
|
||||
--no-backup) CHECK_BACKUP=0; shift ;;
|
||||
--disk-threshold)
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "ERROR: --disk-threshold requires an argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
DISK_THRESHOLD="$2"
|
||||
shift 2
|
||||
;;
|
||||
--backup-max-age)
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "ERROR: --backup-max-age requires an argument" >&2
|
||||
exit 1
|
||||
fi
|
||||
BACKUP_MAX_AGE="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unknown argument: $1" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# 配置
|
||||
APP_URL="${APP_URL:-${HEALTH_CHECK_URL:-http://localhost:8015}}"
|
||||
BACKUP_DIR="${BACKUP_DIR:-./backups}"
|
||||
DISK_THRESHOLD="${DISK_THRESHOLD:-${HEALTH_CHECK_DISK_THRESHOLD:-90}}"
|
||||
BACKUP_MAX_AGE="${BACKUP_MAX_AGE:-${HEALTH_CHECK_BACKUP_MAX_AGE:-24}}"
|
||||
DATABASE_URL="${DATABASE_URL:-}"
|
||||
|
||||
# JSON 输出辅助函数
|
||||
json_escape() {
|
||||
echo "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g'
|
||||
}
|
||||
|
||||
# 检查结果数组
|
||||
RESULTS=""
|
||||
OVERALL_STATUS="healthy"
|
||||
CHECKS_PASSED=0
|
||||
CHECKS_FAILED=0
|
||||
CHECKS_WARNED=0
|
||||
|
||||
add_result() {
|
||||
local name="$1"
|
||||
local status="$2"
|
||||
local message="$3"
|
||||
local detail="${4:-}"
|
||||
|
||||
local escaped_message escaped_detail
|
||||
escaped_message=$(json_escape "$message")
|
||||
escaped_detail=$(json_escape "$detail")
|
||||
|
||||
local result_entry
|
||||
result_entry=" {\"name\": \"$name\", \"status\": \"$status\", \"message\": \"$escaped_message\""
|
||||
if [ -n "$detail" ]; then
|
||||
result_entry="$result_entry, \"detail\": \"$escaped_detail\""
|
||||
fi
|
||||
result_entry="$result_entry }"
|
||||
|
||||
if [ -z "$RESULTS" ]; then
|
||||
RESULTS="$result_entry"
|
||||
else
|
||||
RESULTS="$RESULTS,
|
||||
$result_entry"
|
||||
fi
|
||||
|
||||
case "$status" in
|
||||
pass) CHECKS_PASSED=$((CHECKS_PASSED + 1)) ;;
|
||||
fail) CHECKS_FAILED=$((CHECKS_FAILED + 1)); OVERALL_STATUS="unhealthy" ;;
|
||||
warn) CHECKS_WARNED=$((CHECKS_WARNED + 1)); [ "$OVERALL_STATUS" = "healthy" ] && OVERALL_STATUS="degraded" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 1. 应用健康检查
|
||||
if [ "$CHECK_APP" -eq 1 ]; then
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" -m 10 "$APP_URL" 2>/dev/null || echo "000")
|
||||
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "302" ]; then
|
||||
add_result "app" "pass" "Application is healthy" "HTTP $HTTP_CODE from $APP_URL"
|
||||
elif [ "$HTTP_CODE" = "000" ]; then
|
||||
add_result "app" "fail" "Application is not reachable" "Cannot connect to $APP_URL"
|
||||
else
|
||||
add_result "app" "fail" "Application returned error" "HTTP $HTTP_CODE from $APP_URL"
|
||||
fi
|
||||
else
|
||||
add_result "app" "warn" "curl not available, skipping app check" ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. 数据库连接检查
|
||||
if [ "$CHECK_DB" -eq 1 ]; then
|
||||
if [ -z "$DATABASE_URL" ]; then
|
||||
add_result "database" "warn" "DATABASE_URL not set, skipping DB check" ""
|
||||
else
|
||||
# 解析 DATABASE_URL
|
||||
DB_USER=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/\([^:]*\):.*/\1/p')
|
||||
DB_PASS=$(echo "$DATABASE_URL" | sed -n 's/.*:\/\/[^:]*:\([^@]*\)@.*/\1/p')
|
||||
DB_HOST=$(echo "$DATABASE_URL" | sed -n 's/.*@\([^:]*\):.*/\1/p')
|
||||
DB_PORT=$(echo "$DATABASE_URL" | sed -n 's/.*:\([0-9]*\)\/.*/\1/p')
|
||||
DB_NAME=$(echo "$DATABASE_URL" | sed -n 's/.*\/\([^?]*\).*/\1/p')
|
||||
|
||||
if command -v mysql >/dev/null 2>&1; then
|
||||
if mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" \
|
||||
-e "SELECT 1;" 2>/dev/null; then
|
||||
# 获取连接信息
|
||||
DB_VERSION=$(mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" \
|
||||
-e "SELECT VERSION();" -s -N 2>/dev/null || echo "unknown")
|
||||
add_result "database" "pass" "Database connection successful" "Host: $DB_HOST:$DB_PORT, DB: $DB_NAME, Version: $DB_VERSION"
|
||||
else
|
||||
add_result "database" "fail" "Database connection failed" "Cannot connect to $DB_HOST:$DB_PORT/$DB_NAME"
|
||||
fi
|
||||
else
|
||||
add_result "database" "warn" "mysql client not available, skipping DB check" ""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. 磁盘空间检查
|
||||
if [ "$CHECK_DISK" -eq 1 ]; then
|
||||
# 获取根分区或当前目录所在分区的使用率
|
||||
DISK_INFO=$(df -h . 2>/dev/null | tail -1)
|
||||
if [ -n "$DISK_INFO" ]; then
|
||||
DISK_USE_PCT=$(echo "$DISK_INFO" | awk '{print $5}' | sed 's/%//')
|
||||
DISK_USE_HUMAN=$(echo "$DISK_INFO" | awk '{print $3}')
|
||||
DISK_TOTAL_HUMAN=$(echo "$DISK_INFO" | awk '{print $2}')
|
||||
DISK_AVAIL_HUMAN=$(echo "$DISK_INFO" | awk '{print $4}')
|
||||
DISK_MOUNT=$(echo "$DISK_INFO" | awk '{print $6}')
|
||||
|
||||
if [ "$DISK_USE_PCT" -ge "$DISK_THRESHOLD" ]; then
|
||||
add_result "disk" "fail" "Disk space critical" "Usage: ${DISK_USE_PCT}% (threshold: ${DISK_THRESHOLD}%), Used: ${DISK_USE_HUMAN}/${DISK_TOTAL_HUMAN}, Available: ${DISK_AVAIL_HUMAN}, Mount: ${DISK_MOUNT}"
|
||||
elif [ "$DISK_USE_PCT" -ge $((DISK_THRESHOLD - 10)) ]; then
|
||||
add_result "disk" "warn" "Disk space warning" "Usage: ${DISK_USE_PCT}% (threshold: ${DISK_THRESHOLD}%), Used: ${DISK_USE_HUMAN}/${DISK_TOTAL_HUMAN}, Available: ${DISK_AVAIL_HUMAN}, Mount: ${DISK_MOUNT}"
|
||||
else
|
||||
add_result "disk" "pass" "Disk space OK" "Usage: ${DISK_USE_PCT}%, Used: ${DISK_USE_HUMAN}/${DISK_TOTAL_HUMAN}, Available: ${DISK_AVAIL_HUMAN}, Mount: ${DISK_MOUNT}"
|
||||
fi
|
||||
else
|
||||
add_result "disk" "warn" "Could not determine disk usage" ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. 备份新鲜度检查
|
||||
if [ "$CHECK_BACKUP" -eq 1 ]; then
|
||||
if [ -d "$BACKUP_DIR" ]; then
|
||||
LATEST_BACKUP=$(ls -t "$BACKUP_DIR"/db_backup_*.sql.gz 2>/dev/null | head -1)
|
||||
if [ -n "$LATEST_BACKUP" ]; then
|
||||
# 获取备份文件修改时间(秒)
|
||||
BACKUP_MTIME=$(stat -c%Y "$LATEST_BACKUP" 2>/dev/null || stat -f%m "$LATEST_BACKUP" 2>/dev/null)
|
||||
CURRENT_TIME=$(date +%s)
|
||||
BACKUP_AGE_HOURS=$(( (CURRENT_TIME - BACKUP_MTIME) / 3600 ))
|
||||
|
||||
BACKUP_SIZE=$(stat -c%s "$LATEST_BACKUP" 2>/dev/null || stat -f%z "$LATEST_BACKUP" 2>/dev/null)
|
||||
BACKUP_SIZE_HUMAN=$(echo "$BACKUP_SIZE" | awk '{split("B KB MB GB TB",v);i=1;while($1>=1024&&i<5){$1/=1024;i++};printf "%.1f%s",$1,v[i]}')
|
||||
|
||||
if [ "$BACKUP_AGE_HOURS" -gt "$BACKUP_MAX_AGE" ]; then
|
||||
add_result "backup" "fail" "Backup is stale" "Latest backup is ${BACKUP_AGE_HOURS}h old (max: ${BACKUP_MAX_AGE}h), File: $(basename "$LATEST_BACKUP"), Size: $BACKUP_SIZE_HUMAN"
|
||||
elif [ "$BACKUP_AGE_HOURS" -gt $((BACKUP_MAX_AGE / 2)) ]; then
|
||||
add_result "backup" "warn" "Backup getting old" "Latest backup is ${BACKUP_AGE_HOURS}h old (max: ${BACKUP_MAX_AGE}h), File: $(basename "$LATEST_BACKUP"), Size: $BACKUP_SIZE_HUMAN"
|
||||
else
|
||||
add_result "backup" "pass" "Backup is fresh" "Latest backup is ${BACKUP_AGE_HOURS}h old, File: $(basename "$LATEST_BACKUP"), Size: $BACKUP_SIZE_HUMAN"
|
||||
fi
|
||||
else
|
||||
add_result "backup" "fail" "No backup files found" "No db_backup_*.sql.gz files in $BACKUP_DIR"
|
||||
fi
|
||||
else
|
||||
add_result "backup" "warn" "Backup directory does not exist" "$BACKUP_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 输出 JSON 报告
|
||||
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
cat <<EOF
|
||||
{
|
||||
"timestamp": "$TIMESTAMP",
|
||||
"status": "$OVERALL_STATUS",
|
||||
"summary": {
|
||||
"total": $((CHECKS_PASSED + CHECKS_FAILED + CHECKS_WARNED)),
|
||||
"passed": $CHECKS_PASSED,
|
||||
"failed": $CHECKS_FAILED,
|
||||
"warned": $CHECKS_WARNED
|
||||
},
|
||||
"checks": [
|
||||
$RESULTS
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
if [ "$OVERALL_STATUS" = "unhealthy" ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user