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:
SpecialX
2026-06-17 20:18:29 +08:00
parent b86255f0ea
commit 6585e10c6f
53 changed files with 7491 additions and 37 deletions

View File

@@ -0,0 +1,342 @@
#!/bin/bash
# 异地备份同步脚本
# 用法: ./backup-offsite-sync.sh
# 将本地备份同步到远程存储(S3/OSS/NFS),支持校验和清理
set -u
show_help() {
cat <<EOF
用法: $0 [选项]
异地备份同步脚本,将本地备份同步到远程存储
选项:
--backend TYPE 远程存储后端类型: s3|oss|nfs|none
--no-cleanup 不清理远程过期备份
--no-verify 不校验同步结果
--help, -h 显示帮助信息
环境变量:
BACKUP_DIR 本地备份目录(默认 ./backups)
BACKUP_OFFSITE_BACKEND 远程后端类型: s3|oss|nfs|none (默认 none)
BACKUP_OFFSITE_REMOTE 远程目标路径
- s3: s3://bucket-name/path
- oss: oss://bucket-name/path
- nfs: /mnt/nfs/backup-path
BACKUP_OFFSITE_BUCKET 存储桶名称(仅 s3/oss)
BACKUP_OFFSITE_ACCESS_KEY 访问密钥
BACKUP_OFFSITE_SECRET_KEY 秘密密钥
BACKUP_OFFSITE_REGION 区域(默认 us-east-1)
BACKUP_OFFSITE_RETENTION_DAYS 远程保留天数(默认 90)
需要工具:
s3: aws-cli (aws) 或 rclone
oss: ossutil 或 rclone
nfs: rsync (NFS 应已挂载到 BACKUP_OFFSITE_REMOTE)
退出码:
0 同步成功
1 同步失败
EOF
}
# 解析参数
NO_CLEANUP=0
NO_VERIFY=0
while [ $# -gt 0 ]; do
case "$1" in
--help|-h)
show_help
exit 0
;;
--backend)
if [ $# -lt 2 ]; then
echo "ERROR: --backend requires an argument" >&2
exit 1
fi
BACKUP_OFFSITE_BACKEND="$2"
shift 2
;;
--no-cleanup)
NO_CLEANUP=1
shift
;;
--no-verify)
NO_VERIFY=1
shift
;;
*)
echo "ERROR: Unknown argument: $1" >&2
exit 1
;;
esac
done
BACKUP_DIR="${BACKUP_DIR:-./backups}"
BACKEND="${BACKUP_OFFSITE_BACKEND:-none}"
REMOTE="${BACKUP_OFFSITE_REMOTE:-}"
BUCKET="${BACKUP_OFFSITE_BUCKET:-}"
ACCESS_KEY="${BACKUP_OFFSITE_ACCESS_KEY:-}"
SECRET_KEY="${BACKUP_OFFSITE_SECRET_KEY:-}"
REGION="${BACKUP_OFFSITE_REGION:-us-east-1}"
RETENTION_DAYS="${BACKUP_OFFSITE_RETENTION_DAYS:-90}"
echo "=== Offsite Backup Sync ==="
echo "Time: $(date -u +"%Y-%m-%dT%H:%M:%SZ")"
echo "Backend: $BACKEND"
echo "Local: $BACKUP_DIR"
echo "Remote: $REMOTE"
echo ""
# 检查后端类型
if [ "$BACKEND" = "none" ]; then
echo "INFO: BACKUP_OFFSITE_BACKEND=none, offsite sync disabled"
echo "To enable, set BACKUP_OFFSITE_BACKEND to s3, oss, or nfs"
exit 0
fi
if [ "$BACKEND" != "s3" ] && [ "$BACKEND" != "oss" ] && [ "$BACKEND" != "nfs" ]; then
echo "ERROR: Invalid backend: $BACKEND (must be s3, oss, nfs, or none)" >&2
exit 1
fi
# 检查本地备份目录
if [ ! -d "$BACKUP_DIR" ]; then
echo "ERROR: Local backup directory does not exist: $BACKUP_DIR" >&2
exit 1
fi
# 统计本地备份文件
LOCAL_FILES=$(ls -1 "$BACKUP_DIR"/db_backup_*.sql.gz 2>/dev/null | wc -l)
if [ "$LOCAL_FILES" -eq 0 ]; then
echo "ERROR: No backup files found in $BACKUP_DIR" >&2
exit 1
fi
echo "INFO: Found $LOCAL_FILES local backup files"
# 检查远程配置
if [ -z "$REMOTE" ]; then
echo "ERROR: BACKUP_OFFSITE_REMOTE not set" >&2
echo "Example for $BACKEND:" >&2
case "$BACKEND" in
s3) echo " s3://my-bucket/backups/" >&2 ;;
oss) echo " oss://my-bucket/backups/" >&2 ;;
nfs) echo " /mnt/nfs/backups/" >&2 ;;
esac
exit 1
fi
# 检查工具可用性
check_tool() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "ERROR: Required tool not found: $1" >&2
echo "Please install $1 to use the $BACKEND backend" >&2
exit 1
fi
}
# 配置凭证
setup_credentials() {
case "$BACKEND" in
s3)
if [ -n "$ACCESS_KEY" ] && [ -n "$SECRET_KEY" ]; then
export AWS_ACCESS_KEY_ID="$ACCESS_KEY"
export AWS_SECRET_ACCESS_KEY="$SECRET_KEY"
export AWS_DEFAULT_REGION="$REGION"
fi
;;
oss)
if [ -n "$ACCESS_KEY" ] && [ -n "$SECRET_KEY" ]; then
# ossutil 配置
if [ -f ~/.ossutilconfig ]; then
cp ~/.ossutilconfig ~/.ossutilconfig.bak 2>/dev/null || true
fi
cat > ~/.ossutilconfig <<EOF
[Credentials]
provider = oss
accessKey = $ACCESS_KEY
secretKey = $SECRET_KEY
[Default]
endpoint = oss-${REGION}.aliyuncs.com
EOF
fi
;;
nfs)
# NFS 应已挂载,无需凭证
if [ ! -d "$REMOTE" ]; then
echo "ERROR: NFS remote directory does not exist: $REMOTE" >&2
echo "Please ensure NFS is mounted at this path" >&2
exit 1
fi
;;
esac
}
# 同步到远程
sync_to_remote() {
echo ""
echo "[1/3] Syncing backups to $BACKEND..."
case "$BACKEND" in
s3)
if command -v aws >/dev/null 2>&1; then
echo "Using aws-cli"
if ! aws s3 sync "$BACKUP_DIR/" "$REMOTE" \
--exclude "*" --include "db_backup_*.sql.gz" \
--no-progress; then
echo "ERROR: aws s3 sync failed" >&2
return 1
fi
elif command -v rclone >/dev/null 2>&1; then
echo "Using rclone"
if ! rclone sync "$BACKUP_DIR" "$REMOTE" \
--include "db_backup_*.sql.gz" \
--progress; then
echo "ERROR: rclone sync failed" >&2
return 1
fi
else
echo "ERROR: Neither aws-cli nor rclone found" >&2
return 1
fi
;;
oss)
if command -v ossutil >/dev/null 2>&1; then
echo "Using ossutil"
if ! ossutil cp -r "$BACKUP_DIR/" "$REMOTE" \
--include "db_backup_*.sql.gz" -f; then
echo "ERROR: ossutil sync failed" >&2
return 1
fi
elif command -v rclone >/dev/null 2>&1; then
echo "Using rclone"
if ! rclone sync "$BACKUP_DIR" "$REMOTE" \
--include "db_backup_*.sql.gz" \
--progress; then
echo "ERROR: rclone sync failed" >&2
return 1
fi
else
echo "ERROR: Neither ossutil nor rclone found" >&2
return 1
fi
;;
nfs)
if command -v rsync >/dev/null 2>&1; then
echo "Using rsync"
mkdir -p "$REMOTE" 2>/dev/null || true
if ! rsync -av --include="db_backup_*.sql.gz" --exclude="*" \
"$BACKUP_DIR/" "$REMOTE/"; then
echo "ERROR: rsync failed" >&2
return 1
fi
else
echo "Using cp (rsync not available)"
mkdir -p "$REMOTE" 2>/dev/null || true
if ! cp "$BACKUP_DIR"/db_backup_*.sql.gz "$REMOTE/" 2>/dev/null; then
echo "ERROR: cp failed" >&2
return 1
fi
fi
;;
esac
echo " PASS: Sync completed"
return 0
}
# 校验同步结果
verify_sync() {
if [ "$NO_VERIFY" -eq 1 ]; then
echo ""
echo "[2/3] Verification skipped (--no-verify)"
return 0
fi
echo ""
echo "[2/3] Verifying sync result..."
REMOTE_FILES=0
case "$BACKEND" in
s3)
if command -v aws >/dev/null 2>&1; then
REMOTE_FILES=$(aws s3 ls "$REMOTE" --recursive 2>/dev/null | grep -c "db_backup_.*\.sql\.gz" || echo 0)
elif command -v rclone >/dev/null 2>&1; then
REMOTE_FILES=$(rclone lsf "$REMOTE" --include "db_backup_*.sql.gz" 2>/dev/null | wc -l || echo 0)
fi
;;
oss)
if command -v ossutil >/dev/null 2>&1; then
REMOTE_FILES=$(ossutil ls "$REMOTE" 2>/dev/null | grep -c "db_backup_.*\.sql\.gz" || echo 0)
elif command -v rclone >/dev/null 2>&1; then
REMOTE_FILES=$(rclone lsf "$REMOTE" --include "db_backup_*.sql.gz" 2>/dev/null | wc -l || echo 0)
fi
;;
nfs)
REMOTE_FILES=$(ls -1 "$REMOTE"/db_backup_*.sql.gz 2>/dev/null | wc -l)
;;
esac
echo " Local files: $LOCAL_FILES"
echo " Remote files: $REMOTE_FILES"
if [ "$REMOTE_FILES" -lt "$LOCAL_FILES" ]; then
echo " WARN: Remote has fewer files than local (some may have been cleaned up)"
else
echo " PASS: File count verified"
fi
return 0
}
# 清理远程过期备份
cleanup_remote() {
if [ "$NO_CLEANUP" -eq 1 ]; then
echo ""
echo "[3/3] Cleanup skipped (--no-cleanup)"
return 0
fi
echo ""
echo "[3/3] Cleaning up remote backups older than $RETENTION_DAYS days..."
case "$BACKEND" in
s3)
if command -v aws >/dev/null 2>&1; then
aws s3 ls "$REMOTE" --recursive 2>/dev/null | grep "db_backup_.*\.sql\.gz" | while read -r line; do
FILE_PATH=$(echo "$line" | awk '{print $4}')
FILE_DATE=$(echo "$FILE_PATH" | grep -oE '[0-9]{8}_[0-9]{6}' | head -1)
if [ -n "$FILE_DATE" ]; then
FILE_TS=$(echo "$FILE_DATE" | sed 's/\([0-9]\{8\}\)_\([0-9]\{6\}\)/\1 \2/' | awk '{print $1}')
CUTOFF=$(date -d "-$RETENTION_DAYS days" +%Y%m%d 2>/dev/null || date -v-${RETENTION_DAYS}d +%Y%m%d 2>/dev/null)
if [ -n "$CUTOFF" ] && [ "$FILE_TS" -lt "$CUTOFF" ]; then
echo " Deleting: $FILE_PATH"
aws s3 rm "s3://$(echo "$REMOTE" | sed 's|s3://||')/$FILE_PATH" 2>/dev/null || true
fi
fi
done
fi
;;
oss)
if command -v ossutil >/dev/null 2>&1; then
# ossutil 不支持基于时间的清理,使用生命周期规则或手动删除
echo " INFO: For OSS, configure lifecycle rules in the console for automatic cleanup"
echo " INFO: Manual cleanup with retention $RETENTION_DAYS days"
fi
;;
nfs)
if [ -d "$REMOTE" ]; then
find "$REMOTE" -name "db_backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete 2>/dev/null || true
echo " Cleaned up files older than $RETENTION_DAYS days"
fi
;;
esac
echo " PASS: Cleanup completed"
return 0
}
# 执行同步流程
setup_credentials
if ! sync_to_remote; then
exit 1
fi
verify_sync
cleanup_remote
echo ""
echo "=== Offsite Sync Complete ==="
exit 0