GuDingZiChan/deploy.sh

356 lines
12 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# 固定资产管理系统部署脚本
set -euo pipefail
PROJECT_DIR="/opt/equipment-management"
COMPOSE_FILE="$PROJECT_DIR/docker-compose.yml"
PROJECT_NAME="equipment-management"
BACKUP_SCRIPT="$PROJECT_DIR/scripts/backup-equipment.sh"
DISPOSAL_PRECHECK_SQL="$PROJECT_DIR/backend/prisma/migrations/20260714_asset_permissions_disposal_dingtalk/precheck.sql"
DISPOSAL_MIGRATION_NAME="20260714_asset_permissions_disposal_dingtalk"
MYSQL_CONTAINER="equipment-mysql"
SKIP_BACKUP="${SKIP_BACKUP:-0}"
CONFIRM_ACTIVE_DISPOSALS_REVIEWED="${CONFIRM_ACTIVE_DISPOSALS_REVIEWED:-}"
HEALTHCHECK_ATTEMPTS="${HEALTHCHECK_ATTEMPTS:-12}"
HEALTHCHECK_INTERVAL_SECONDS="${HEALTHCHECK_INTERVAL_SECONDS:-5}"
HEALTHCHECK_URL="${HEALTHCHECK_URL:-http://localhost:3000/api/health}"
EQUIPMENT_CONTAINERS=("equipment-frontend" "equipment-backend" "equipment-mysql")
DISPOSAL_PRECHECK_REQUIRED=0
INITIAL_DISPOSAL_FINGERPRINT=""
CURRENT_DISPOSAL_FINGERPRINT=""
CURRENT_ACTIVE_DISPOSAL_COUNT=0
get_container_status() {
local container="$1"
docker inspect -f '{{.State.Status}}' "$container" 2>/dev/null || printf 'missing\n'
}
verify_equipment_containers() {
local container status
local failed=0
echo ">>> 固定资产系统容器状态..."
for container in "${EQUIPMENT_CONTAINERS[@]}"; do
status="$(get_container_status "$container")"
echo " $container => $status"
if [ "$status" != "running" ]; then
echo " 错误:固定资产容器 $container 未处于 running 状态。" >&2
failed=1
fi
done
return "$failed"
}
wait_for_backend_health() {
local attempt
echo ">>> 检查固定资产系统后端健康状态..."
for ((attempt = 1; attempt <= HEALTHCHECK_ATTEMPTS; attempt++)); do
if curl -fsS --max-time 5 "$HEALTHCHECK_URL" >/dev/null; then
echo " 后端健康检查通过(第 ${attempt}/${HEALTHCHECK_ATTEMPTS} 次)"
return 0
fi
if [ "$attempt" -lt "$HEALTHCHECK_ATTEMPTS" ]; then
echo " 第 ${attempt}/${HEALTHCHECK_ATTEMPTS} 次检查失败,${HEALTHCHECK_INTERVAL_SECONDS} 秒后重试..."
sleep "$HEALTHCHECK_INTERVAL_SECONDS"
fi
done
echo "后端健康检查失败:${HEALTHCHECK_URL},已尝试 ${HEALTHCHECK_ATTEMPTS} 次。" >&2
return 1
}
run_equipment_mysql() {
docker exec -i "$MYSQL_CONTAINER" sh -eu -c '
if [ -z "${MYSQL_ROOT_PASSWORD:-}" ]; then
echo "equipment-mysql 缺少 MYSQL_ROOT_PASSWORD,无法安全执行迁移前核查。" >&2
exit 1
fi
if [ -z "${MYSQL_DATABASE:-}" ]; then
echo "equipment-mysql 缺少 MYSQL_DATABASE,无法确认固定资产数据库。" >&2
exit 1
fi
defaults_file="$(mktemp /tmp/equipment-mysql-client.XXXXXX)"
trap '\''rm -f "$defaults_file"'\'' EXIT HUP INT TERM
chmod 600 "$defaults_file"
printf "[client]\nuser=root\npassword=%s\n" "$MYSQL_ROOT_PASSWORD" > "$defaults_file"
mysql --defaults-extra-file="$defaults_file" --database="$MYSQL_DATABASE" "$@"
' sh "$@"
}
get_disposal_migration_state() {
local migrations_table_exists state
migrations_table_exists="$(
printf '%s\n' \
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '_prisma_migrations';" |
run_equipment_mysql --batch --skip-column-names --raw
)"
migrations_table_exists="$(printf '%s' "$migrations_table_exists" | tr -d '[:space:]')"
if [ "$migrations_table_exists" = "0" ]; then
printf 'pending\n'
return 0
fi
if [ "$migrations_table_exists" != "1" ]; then
echo "无法确认 Prisma 迁移表状态。" >&2
return 1
fi
state="$(run_equipment_mysql --batch --skip-column-names --raw <<SQL
SELECT CASE
WHEN SUM(CASE WHEN finished_at IS NULL AND rolled_back_at IS NULL THEN 1 ELSE 0 END) > 0 THEN 'incomplete'
WHEN SUM(CASE WHEN finished_at IS NOT NULL AND rolled_back_at IS NULL THEN 1 ELSE 0 END) > 0 THEN 'applied'
ELSE 'pending'
END
FROM _prisma_migrations
WHERE migration_name = '$DISPOSAL_MIGRATION_NAME';
SQL
)"
state="$(printf '%s' "$state" | tr -d '[:space:]')"
case "$state" in
applied|pending|incomplete) printf '%s\n' "$state" ;;
*) echo "报废迁移状态不可解析: $state" >&2; return 1 ;;
esac
}
calculate_active_disposal_fingerprint() {
local display_rows="${1:-0}"
local snapshot_output missing_root_count tree_anomaly_count review_row_count
local actual_review_row_count review_rows active_fingerprint active_count
if [ ! -r "$DISPOSAL_PRECHECK_SQL" ]; then
echo "迁移前核查脚本不存在或不可读: $DISPOSAL_PRECHECK_SQL" >&2
return 1
fi
if ! snapshot_output="$(
run_equipment_mysql --batch --skip-column-names --raw < "$DISPOSAL_PRECHECK_SQL"
)"; then
echo "报废流程迁移前一致性快照核查执行失败,已阻止部署。" >&2
return 1
fi
missing_root_count="$(printf '%s\n' "$snapshot_output" | awk -F '\t' '$1 == "MISSING_ROOT_COUNT" { print $2 }')"
tree_anomaly_count="$(printf '%s\n' "$snapshot_output" | awk -F '\t' '$1 == "TREE_ANOMALY_COUNT" { print $2 }')"
active_count="$(printf '%s\n' "$snapshot_output" | awk -F '\t' '$1 == "ACTIVE_DISPOSAL_COUNT" { print $2 }')"
review_row_count="$(printf '%s\n' "$snapshot_output" | awk -F '\t' '$1 == "REVIEW_ROW_COUNT" { print $2 }')"
review_rows="$(printf '%s\n' "$snapshot_output" | awk -F '\t' '$1 == "REVIEW_ROW" { sub(/^[^\t]*\t/, ""); print }')"
if ! [[ "$missing_root_count" =~ ^[0-9]+$ ]]; then
echo "缺失根资产数量不可解析,已阻止部署。" >&2
return 1
fi
if [ "$missing_root_count" -gt 0 ]; then
echo "检测到 $missing_root_count 条未完成报废单缺少根资产,已阻止部署。" >&2
return 1
fi
if ! [[ "$tree_anomaly_count" =~ ^[0-9]+$ ]]; then
echo "资产树异常数量不可解析,已阻止部署。" >&2
return 1
fi
if [ "$tree_anomaly_count" -gt 0 ]; then
echo "检测到 $tree_anomaly_count 个父子环或深度达到 100 的未完成报废资产节点。" >&2
echo "此类数据异常不允许通过确认指纹绕过,请先修复数据。" >&2
return 1
fi
if ! [[ "$active_count" =~ ^[0-9]+$ ]]; then
echo "未完成报废单数量不可解析,已阻止部署。" >&2
return 1
fi
if ! [[ "$review_row_count" =~ ^[0-9]+$ ]]; then
echo "人工核对行数量不可解析,已阻止部署。" >&2
return 1
fi
actual_review_row_count="$(
printf '%s\n' "$snapshot_output" | awk -F '\t' '$1 == "REVIEW_ROW" { count += 1 } END { print count + 0 }'
)"
if [ "$actual_review_row_count" -ne "$review_row_count" ] || \
{ [ "$active_count" -gt 0 ] && [ "$review_row_count" -lt "$active_count" ]; }; then
echo "一致性快照的统计与人工核对行不一致,已阻止部署。" >&2
return 1
fi
if [ "$display_rows" = "1" ]; then
echo ">>> 同一一致性快照中的未完成报废单及完整资产树:"
if [ "$review_row_count" -gt 0 ]; then
printf '%s\n' "$review_rows"
else
echo " (无未完成报废单)"
fi
fi
if ! command -v sha256sum >/dev/null 2>&1; then
echo "当前主机缺少 sha256sum,无法生成未完成报废单指纹,已阻止部署。" >&2
return 1
fi
# 人工展示与指纹使用同一个 snapshot_output 中的 REVIEW_ROW,避免二次查询产生漂移。
active_fingerprint="$(printf '%s\n' "$review_rows" | sha256sum | awk '{ print $1 }')"
if ! [[ "$active_fingerprint" =~ ^[0-9a-f]{64}$ ]]; then
echo "未完成报废单指纹不可解析,已阻止部署。" >&2
return 1
fi
CURRENT_ACTIVE_DISPOSAL_COUNT="$active_count"
CURRENT_DISPOSAL_FINGERPRINT="$active_fingerprint"
}
run_disposal_migration_precheck() {
local mysql_status migration_state
mysql_status="$(get_container_status "$MYSQL_CONTAINER")"
if [ "$mysql_status" != "running" ]; then
echo "无法确认生产 MySQL:$MYSQL_CONTAINER 当前状态为 $mysql_status,已阻止部署。" >&2
return 1
fi
migration_state="$(get_disposal_migration_state)"
case "$migration_state" in
applied)
echo ">>> 报废流程迁移已应用,跳过一次性迁移前核查。"
return 0
;;
incomplete)
echo "检测到未完成且未回滚的报废流程迁移记录,已阻止部署;请先恢复 Prisma 迁移状态。" >&2
return 1
;;
pending)
DISPOSAL_PRECHECK_REQUIRED=1
;;
esac
echo ">>> 执行报废流程迁移前只读核查..."
calculate_active_disposal_fingerprint 1
INITIAL_DISPOSAL_FINGERPRINT="$CURRENT_DISPOSAL_FINGERPRINT"
if [ "$CURRENT_ACTIVE_DISPOSAL_COUNT" -gt 0 ]; then
echo "检测到 $CURRENT_ACTIVE_DISPOSAL_COUNT 条未完成报废单。" >&2
echo "本次完整资产树指纹: $CURRENT_DISPOSAL_FINGERPRINT" >&2
if [ "$CONFIRM_ACTIVE_DISPOSALS_REVIEWED" != "$CURRENT_DISPOSAL_FINGERPRINT" ]; then
echo "已默认阻止部署。请业务负责人逐单核对上方资产树。" >&2
echo "核对完成后,仅本次执行可显式设置 CONFIRM_ACTIVE_DISPOSALS_REVIEWED=$CURRENT_DISPOSAL_FINGERPRINT。" >&2
return 1
fi
echo "已按当前集合指纹显式确认未完成报废单,本次允许继续部署。"
else
echo "未发现未完成报废单,迁移前核查通过。"
fi
}
recheck_disposal_migration_precheck() {
local migration_state
if [ "$DISPOSAL_PRECHECK_REQUIRED" != "1" ]; then
return 0
fi
echo ">>> 镜像构建后复核报废资产树是否变化..."
migration_state="$(get_disposal_migration_state)"
case "$migration_state" in
applied)
echo "报废流程迁移已由其他发布成功应用,允许继续。"
return 0
;;
incomplete)
echo "构建期间出现未完成的报废流程迁移记录,已阻止更新容器。" >&2
return 1
;;
esac
calculate_active_disposal_fingerprint
if [ "$CURRENT_DISPOSAL_FINGERPRINT" != "$INITIAL_DISPOSAL_FINGERPRINT" ]; then
echo "镜像构建期间未完成报废单或完整资产树发生变化,已阻止更新容器。" >&2
echo "构建前指纹: $INITIAL_DISPOSAL_FINGERPRINT" >&2
echo "当前指纹: $CURRENT_DISPOSAL_FINGERPRINT" >&2
return 1
fi
echo "报废单及完整资产树指纹未变化。"
}
if [ "${DEPLOY_SH_FUNCTIONS_ONLY:-0}" = "1" ]; then
return 0 2>/dev/null || exit 0
fi
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=(docker compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE")
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=(docker-compose -p "$PROJECT_NAME" -f "$COMPOSE_FILE")
else
echo "未找到 docker compose / docker-compose,无法部署。"
exit 1
fi
echo "=========================================="
echo " 固定资产管理系统 - 安全部署脚本"
echo "=========================================="
echo "项目目录: $PROJECT_DIR"
echo "Compose 文件: $COMPOSE_FILE"
echo "Compose 项目名: $PROJECT_NAME"
echo "备份脚本: $BACKUP_SCRIPT"
echo ""
if [ ! -f "$COMPOSE_FILE" ]; then
echo "未找到 $COMPOSE_FILE"
exit 1
fi
if ! [[ "$HEALTHCHECK_ATTEMPTS" =~ ^[1-9][0-9]*$ ]]; then
echo "HEALTHCHECK_ATTEMPTS 必须是正整数: $HEALTHCHECK_ATTEMPTS"
exit 1
fi
if ! [[ "$HEALTHCHECK_INTERVAL_SECONDS" =~ ^[0-9]+$ ]]; then
echo "HEALTHCHECK_INTERVAL_SECONDS 必须是非负整数: $HEALTHCHECK_INTERVAL_SECONDS"
exit 1
fi
cd "$PROJECT_DIR"
if [ "$SKIP_BACKUP" = "1" ]; then
echo ">>> 已设置 SKIP_BACKUP=1,跳过备份"
else
if [ ! -x "$BACKUP_SCRIPT" ]; then
echo "备份脚本不存在或不可执行: $BACKUP_SCRIPT"
exit 1
fi
echo ">>> 先执行固定资产系统备份..."
"$BACKUP_SCRIPT"
fi
run_disposal_migration_precheck
echo ">>> 先构建固定资产系统镜像..."
"${COMPOSE_CMD[@]}" build
recheck_disposal_migration_precheck
echo ">>> 镜像构建成功,开始更新固定资产系统容器..."
"${COMPOSE_CMD[@]}" up -d --remove-orphans
deployment_failed=0
if ! wait_for_backend_health; then
deployment_failed=1
fi
if ! verify_equipment_containers; then
deployment_failed=1
fi
if [ "$deployment_failed" -ne 0 ]; then
echo "固定资产系统部署校验失败,请根据上述错误排查。" >&2
exit 1
fi
echo ""
echo "=========================================="
echo " 部署完成"
SERVER_HOST="${SERVER_HOST:-<server-host>}"
echo " 固定资产前端: http://$SERVER_HOST"
echo " 固定资产后端: http://$SERVER_HOST:3000/api"
echo "=========================================="