GuDingZiChan/deploy.sh

256 lines
8.6 KiB
Bash
Raw 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"
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")
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 "$@"
}
run_disposal_migration_precheck() {
local mysql_status tree_anomaly_count active_rows active_count active_fingerprint
echo ">>> 执行报废流程迁移前只读核查..."
if [ ! -r "$DISPOSAL_PRECHECK_SQL" ]; then
echo "迁移前核查脚本不存在或不可读: $DISPOSAL_PRECHECK_SQL" >&2
return 1
fi
mysql_status="$(get_container_status "$MYSQL_CONTAINER")"
if [ "$mysql_status" != "running" ]; then
echo "无法确认生产 MySQL:$MYSQL_CONTAINER 当前状态为 $mysql_status,已阻止部署。" >&2
return 1
fi
if ! run_equipment_mysql < "$DISPOSAL_PRECHECK_SQL"; then
echo "报废流程迁移前核查执行失败,已阻止部署。" >&2
return 1
fi
if ! tree_anomaly_count="$(run_equipment_mysql --batch --skip-column-names --raw <<'SQL'
WITH RECURSIVE active_disposal_tree AS (
SELECT
disposal.id AS disposal_id,
equipment.id AS equipment_id,
equipment.parent_id,
0 AS depth,
CAST(CONCAT('/', equipment.id, '/') AS CHAR(4096)) AS visited_path,
0 AS cycle_detected
FROM disposal_record disposal
INNER JOIN equipment equipment ON equipment.id = disposal.equipment_id
WHERE disposal.status IN ('pending', 'approved')
UNION ALL
SELECT
tree.disposal_id,
child.id,
child.parent_id,
tree.depth + 1,
CONCAT(tree.visited_path, child.id, '/'),
LOCATE(CONCAT('/', child.id, '/'), tree.visited_path) > 0
FROM active_disposal_tree tree
INNER JOIN equipment child ON child.parent_id = tree.equipment_id
WHERE tree.depth < 100
AND tree.cycle_detected = 0
)
SELECT COUNT(*)
FROM active_disposal_tree
WHERE cycle_detected = 1 OR depth >= 100;
SQL
)"; then
echo "无法确认未完成报废资产树是否存在环或超深层级,已阻止部署。" >&2
return 1
fi
tree_anomaly_count="$(printf '%s' "$tree_anomaly_count" | tr -d '[:space:]')"
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_rows="$(
printf '%s\n' \
"SELECT id, status, equipment_id, DATE_FORMAT(updated_at, '%Y-%m-%dT%H:%i:%s.%f') FROM disposal_record WHERE status IN ('pending', 'approved') ORDER BY id, status, equipment_id, updated_at;" |
run_equipment_mysql --batch --skip-column-names --raw
)"; then
echo "无法读取未完成报废单指纹数据,已阻止部署。" >&2
return 1
fi
active_count="$(printf '%s\n' "$active_rows" | awk 'NF { count += 1 } END { print count + 0 }')"
if [ "$active_count" -gt 0 ]; then
if ! command -v sha256sum >/dev/null 2>&1; then
echo "当前主机缺少 sha256sum,无法生成未完成报废单指纹,已阻止部署。" >&2
return 1
fi
active_fingerprint="$(printf '%s\n' "$active_rows" | sha256sum | awk '{ print $1 }')"
if ! [[ "$active_fingerprint" =~ ^[0-9a-f]{64}$ ]]; then
echo "未完成报废单指纹不可解析,已阻止部署。" >&2
return 1
fi
echo "检测到 $active_count 条未完成报废单。" >&2
echo "本次未完成报废单指纹: $active_fingerprint" >&2
if [ "$CONFIRM_ACTIVE_DISPOSALS_REVIEWED" != "$active_fingerprint" ]; then
echo "已默认阻止部署。请业务负责人逐单核对上方资产树。" >&2
echo "核对完成后,仅本次执行可显式设置 CONFIRM_ACTIVE_DISPOSALS_REVIEWED=$active_fingerprint。" >&2
return 1
fi
echo "已按当前集合指纹显式确认未完成报废单,本次允许继续部署。"
else
echo "未发现未完成报废单,迁移前核查通过。"
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
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 "=========================================="