From 0bc767933efc0d291394eb128f0899b896b42e17 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Fri, 10 Jul 2026 12:15:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=A4=87=E4=BB=BD=E6=81=A2=E5=A4=8D=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deployment/DEPLOYMENT_GUIDE.md | 15 ++++++- ai_knowledge_base_v2/scripts/mysql_backup.sh | 40 ++++++++++++++----- ai_knowledge_base_v2/scripts/mysql_restore.sh | 38 ++++++++++++++++++ ai_knowledge_base_v2/scripts/phase6_verify.sh | 4 ++ 4 files changed, 86 insertions(+), 11 deletions(-) create mode 100755 ai_knowledge_base_v2/scripts/mysql_restore.sh diff --git a/ai_knowledge_base_v2/deployment/DEPLOYMENT_GUIDE.md b/ai_knowledge_base_v2/deployment/DEPLOYMENT_GUIDE.md index a0d535e..a569b29 100644 --- a/ai_knowledge_base_v2/deployment/DEPLOYMENT_GUIDE.md +++ b/ai_knowledge_base_v2/deployment/DEPLOYMENT_GUIDE.md @@ -122,13 +122,24 @@ alembic upgrade head ## 备份 -可使用脚本: +备份脚本会生成压缩 SQL 和 SHA-256 校验文件,默认保留 14 天。密码只通过环境变量传入,不写入命令参数: ```bash cd ai_knowledge_base_v2 -BACKUP_DIR=./backups ./scripts/mysql_backup.sh +MYSQL_PASSWORD='数据库密码' BACKUP_DIR=./backups ./scripts/mysql_backup.sh ``` +恢复前应停止用户流量并再次备份当前数据库。恢复脚本要求显式确认,并会先验证校验和与压缩文件完整性: + +```bash +MYSQL_PASSWORD='数据库密码' \ +BACKUP_FILE='./backups/ai_knowledge_base_v2_YYYYMMDD_HHMMSS.sql.gz' \ +RESTORE_CONFIRM=YES \ +./scripts/mysql_restore.sh +``` + +恢复后必须检查迁移版本、`/api/ready`、管理员登录、用户登录和一次真实问答。备份文件与 `CONFIG_ENCRYPTION_KEY` 必须分别保存;只有数据库备份而没有对应加密密钥时,敏感配置无法解密。 + ## 回滚原则 1. 先停止新版本服务。 diff --git a/ai_knowledge_base_v2/scripts/mysql_backup.sh b/ai_knowledge_base_v2/scripts/mysql_backup.sh index 37c546f..2aa5ab1 100755 --- a/ai_knowledge_base_v2/scripts/mysql_backup.sh +++ b/ai_knowledge_base_v2/scripts/mysql_backup.sh @@ -5,20 +5,42 @@ BACKUP_DIR="${BACKUP_DIR:-./backups}" MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}" MYSQL_PORT="${MYSQL_PORT:-3307}" MYSQL_USER="${MYSQL_USER:-ai_kb}" -MYSQL_PASSWORD="${MYSQL_PASSWORD:-ai_kb}" MYSQL_DATABASE="${MYSQL_DATABASE:-ai_knowledge_base_v2}" +BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-14}" +: "${MYSQL_PASSWORD:?必须通过 MYSQL_PASSWORD 配置数据库密码}" + +command -v mysqldump >/dev/null || { echo "未找到 mysqldump" >&2; exit 1; } +command -v gzip >/dev/null || { echo "未找到 gzip" >&2; exit 1; } +command -v shasum >/dev/null || { echo "未找到 shasum" >&2; exit 1; } mkdir -p "$BACKUP_DIR" -OUTPUT_FILE="$BACKUP_DIR/${MYSQL_DATABASE}_$(date +%Y%m%d_%H%M%S).sql" +timestamp="$(date +%Y%m%d_%H%M%S)" +output_file="$BACKUP_DIR/${MYSQL_DATABASE}_${timestamp}.sql.gz" +temporary_file="${output_file}.tmp" +trap 'rm -f "$temporary_file"' EXIT -mysqldump \ - -h "$MYSQL_HOST" \ - -P "$MYSQL_PORT" \ - -u "$MYSQL_USER" \ - -p"$MYSQL_PASSWORD" \ +MYSQL_PWD="$MYSQL_PASSWORD" mysqldump \ + --host="$MYSQL_HOST" \ + --port="$MYSQL_PORT" \ + --user="$MYSQL_USER" \ --single-transaction \ + --quick \ --routines \ --triggers \ - "$MYSQL_DATABASE" > "$OUTPUT_FILE" + --events \ + --set-gtid-purged=OFF \ + --default-character-set=utf8mb4 \ + "$MYSQL_DATABASE" | gzip -9 > "$temporary_file" -echo "备份完成:$OUTPUT_FILE" +gzip -t "$temporary_file" +test -s "$temporary_file" +mv "$temporary_file" "$output_file" +(cd "$(dirname "$output_file")" && shasum -a 256 "$(basename "$output_file")" > "$(basename "$output_file").sha256") + +if [[ "$BACKUP_RETENTION_DAYS" =~ ^[0-9]+$ ]] && (( BACKUP_RETENTION_DAYS > 0 )); then + find "$BACKUP_DIR" -type f \( -name "${MYSQL_DATABASE}_*.sql.gz" -o -name "${MYSQL_DATABASE}_*.sql.gz.sha256" \) \ + -mtime "+$BACKUP_RETENTION_DAYS" -delete +fi + +echo "备份完成:$output_file" +echo "校验文件:${output_file}.sha256" diff --git a/ai_knowledge_base_v2/scripts/mysql_restore.sh b/ai_knowledge_base_v2/scripts/mysql_restore.sh new file mode 100755 index 0000000..7e97c83 --- /dev/null +++ b/ai_knowledge_base_v2/scripts/mysql_restore.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +MYSQL_HOST="${MYSQL_HOST:-127.0.0.1}" +MYSQL_PORT="${MYSQL_PORT:-3307}" +MYSQL_USER="${MYSQL_USER:-ai_kb}" +MYSQL_DATABASE="${MYSQL_DATABASE:-ai_knowledge_base_v2}" +: "${MYSQL_PASSWORD:?必须通过 MYSQL_PASSWORD 配置数据库密码}" +: "${BACKUP_FILE:?必须通过 BACKUP_FILE 指定 .sql.gz 备份文件}" + +if [[ "${RESTORE_CONFIRM:-}" != "YES" ]]; then + echo "恢复会覆盖目标数据库数据,确认后请设置 RESTORE_CONFIRM=YES" >&2 + exit 2 +fi + +command -v mysql >/dev/null || { echo "未找到 mysql 客户端" >&2; exit 1; } +command -v gzip >/dev/null || { echo "未找到 gzip" >&2; exit 1; } +command -v shasum >/dev/null || { echo "未找到 shasum" >&2; exit 1; } +test -f "$BACKUP_FILE" || { echo "备份文件不存在:$BACKUP_FILE" >&2; exit 1; } +test -f "${BACKUP_FILE}.sha256" || { echo "缺少校验文件:${BACKUP_FILE}.sha256" >&2; exit 1; } + +(cd "$(dirname "$BACKUP_FILE")" && shasum -a 256 -c "$(basename "$BACKUP_FILE").sha256") +gzip -t "$BACKUP_FILE" + +MYSQL_PWD="$MYSQL_PASSWORD" gzip -dc "$BACKUP_FILE" | MYSQL_PWD="$MYSQL_PASSWORD" mysql \ + --host="$MYSQL_HOST" \ + --port="$MYSQL_PORT" \ + --user="$MYSQL_USER" \ + --default-character-set=utf8mb4 \ + "$MYSQL_DATABASE" + +MYSQL_PWD="$MYSQL_PASSWORD" mysql \ + --host="$MYSQL_HOST" \ + --port="$MYSQL_PORT" \ + --user="$MYSQL_USER" \ + --database="$MYSQL_DATABASE" \ + --batch --skip-column-names \ + --execute="SELECT CONCAT('恢复完成,迁移版本:', COALESCE((SELECT version_num FROM alembic_version LIMIT 1), '未知'));" diff --git a/ai_knowledge_base_v2/scripts/phase6_verify.sh b/ai_knowledge_base_v2/scripts/phase6_verify.sh index a6d684e..4d6efaa 100755 --- a/ai_knowledge_base_v2/scripts/phase6_verify.sh +++ b/ai_knowledge_base_v2/scripts/phase6_verify.sh @@ -46,4 +46,8 @@ BOOTSTRAP_ADMIN_USERNAME=admin \ BOOTSTRAP_ADMIN_PASSWORD='Strong-Test-Password-2026' \ docker compose -f docker-compose.prod.yml config --quiet +echo "== 备份恢复脚本语法检查 ==" +bash -n scripts/mysql_backup.sh +bash -n scripts/mysql_restore.sh + echo "阶段六自动验证完成"