Files
order_site/deploy/mac-dev.sh
T

397 lines
11 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
set -Eeuo pipefail
# Mac 本机 Docker 开发环境启动脚本。
# 用法:
# bash deploy/mac-dev.sh
# bash deploy/mac-dev.sh --reset-db
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE="${PROJECT_ROOT}/.env"
ENV_EXAMPLE="${PROJECT_ROOT}/.env.mac-docker.example"
COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.dev.yml"
BACKEND_DATA_DIR="${PROJECT_ROOT}/apps/backend/data"
READY_TIMEOUT="${READY_TIMEOUT:-120}"
HEALTH_URL="${DEV_HEALTH_URL:-http://localhost/health/ready}"
RESET_DB=false
RESET_DB_CONFIRM="${RESET_DB_CONFIRM:-}"
COMPOSE_CMD=(docker compose -f "${COMPOSE_FILE}")
log() {
printf '\033[1;34m[mac-dev]\033[0m %s\n' "$*"
}
warn() {
printf '\033[1;33m[mac-dev]\033[0m %s\n' "$*"
}
fail() {
printf '\033[1;31m[mac-dev]\033[0m %s\n' "$*" >&2
exit 1
}
print_usage() {
cat <<'EOF'
用法:
bash deploy/mac-dev.sh
bash deploy/mac-dev.sh --reset-db
参数:
--reset-db 删除 dev Compose PostgreSQL 数据卷 postgres_dev_data,并用当前迁移重新初始化数据库。
--yes 跳过 --reset-db 的交互确认,也可使用环境变量 RESET_DB_CONFIRM=1。
-h, --help 显示帮助。
环境变量:
READY_TIMEOUT 等待容器和健康检查的秒数,默认 120。
DEV_HEALTH_URL 本机健康检查地址,默认 http://localhost/health/ready。
注意:
--reset-db 会删除本机开发库里的订单、任务、后台用户、审计日志和迁移记录。
它不会删除 apps/backend/data 下的平台 JSON 配置、MinIO 文件数据和 node_modules 缓存 volume。
EOF
}
parse_args() {
while (($# > 0)); do
case "$1" in
--reset-db)
RESET_DB=true
;;
--yes)
RESET_DB_CONFIRM=1
;;
-h|--help)
print_usage
exit 0
;;
*)
fail "未知参数:$1。执行 bash deploy/mac-dev.sh --help 查看用法。"
;;
esac
shift
done
}
read_env_value() {
local key="$1"
awk -F= -v key="${key}" '
$0 !~ /^[[:space:]]*#/ && $1 == key {
value = substr($0, index($0, "=") + 1)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
gsub(/^"|"$/, "", value)
gsub(/^'\''|'\''$/, "", value)
print value
exit
}
' "${ENV_FILE}"
}
read_effective_env_value() {
local key="$1"
local default_value="$2"
local value="${!key:-}"
if [[ -z "${value}" ]]; then
value="$(read_env_value "${key}" || true)"
fi
printf '%s\n' "${value:-${default_value}}"
}
ensure_docker() {
command -v docker >/dev/null 2>&1 || fail "未找到 docker,请先安装并启动 Docker Desktop for Mac。"
docker compose version >/dev/null 2>&1 || fail "未找到 docker compose 插件,请升级 Docker Desktop。"
command -v curl >/dev/null 2>&1 || fail "未找到 curl,请先安装 curl。"
}
ensure_env_file() {
if [[ -f "${ENV_FILE}" ]]; then
return
fi
[[ -f "${ENV_EXAMPLE}" ]] || fail "未找到本机开发环境模板:${ENV_EXAMPLE}"
cp "${ENV_EXAMPLE}" "${ENV_FILE}"
log "未找到 .env,已从 .env.mac-docker.example 生成本机开发配置。"
}
validate_env_file() {
[[ "${READY_TIMEOUT}" =~ ^[1-9][0-9]*$ ]] || fail "READY_TIMEOUT 必须是正整数秒数,当前值:${READY_TIMEOUT}"
validate_database_url_for_dev_compose
}
validate_database_url_for_dev_compose() {
local database_url
database_url="$(read_env_value DATABASE_URL)"
if [[ -z "${database_url}" ]]; then
return
fi
if [[ "${database_url}" == *"@localhost:"* \
|| "${database_url}" == *"@127.0.0.1:"* \
|| "${database_url}" == *"@[::1]:"* ]]; then
fail ".env 的 DATABASE_URL 当前指向本机地址。Docker dev 后端容器内的 localhost 不是 postgres 容器,请改为 postgres 服务名。"
fi
}
validate_reset_database_target() {
local database_url
database_url="$(read_env_value DATABASE_URL)"
if [[ -n "${database_url}" && "${database_url}" != *"@postgres:"* ]]; then
fail "--reset-db 只支持重置本脚本管理的 dev Compose PostgreSQL。当前 DATABASE_URL 未指向 postgres 服务,请手动确认外部数据库后再处理。"
fi
}
confirm_reset_database() {
if [[ "${RESET_DB}" != "true" ]]; then
return
fi
validate_reset_database_target
warn "即将重置 Mac 本机开发 PostgreSQL 数据库:会删除订单、任务、后台用户、审计日志和所有迁移记录。"
warn "不会删除 apps/backend/data 下的平台 JSON 配置,也不会删除 node_modules 缓存 volume。"
if [[ "${RESET_DB_CONFIRM}" == "1" ]]; then
warn "检测到 RESET_DB_CONFIRM=1 或 --yes,跳过交互确认。"
return
fi
if [[ ! -t 0 ]]; then
fail "当前不是交互式终端。若确认要重置数据库,请执行:RESET_DB_CONFIRM=1 bash deploy/mac-dev.sh --reset-db"
fi
local answer
printf '请输入 RESET 确认重置数据库:'
read -r answer
[[ "${answer}" == "RESET" ]] || fail "未确认重置数据库,已停止。"
}
ensure_data_dir() {
mkdir -p "${BACKEND_DATA_DIR}"
}
check_compose_config() {
log "检查 Docker Compose dev 配置"
"${COMPOSE_CMD[@]}" config >/dev/null || fail "Docker Compose dev 配置校验失败。"
}
resolve_compose_project_name() {
local project_name
project_name="$(read_env_value COMPOSE_PROJECT_NAME || true)"
if [[ -z "${project_name}" ]]; then
project_name="$(basename "${PROJECT_ROOT}")"
fi
printf '%s\n' "${project_name}"
}
resolve_postgres_volume_name() {
local container_id volume_name project_name expected_volume
container_id="$("${COMPOSE_CMD[@]}" ps -aq postgres 2>/dev/null | head -n 1 || true)"
if [[ -n "${container_id}" ]]; then
volume_name="$(
docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/lib/postgresql/data"}}{{if eq .Type "volume"}}{{.Name}}{{end}}{{end}}{{end}}' "${container_id}" 2>/dev/null || true
)"
if [[ -n "${volume_name}" ]]; then
printf '%s\n' "${volume_name}"
return
fi
fi
project_name="$(resolve_compose_project_name)"
expected_volume="${project_name}_postgres_dev_data"
if docker volume inspect "${expected_volume}" >/dev/null 2>&1; then
printf '%s\n' "${expected_volume}"
return
fi
volume_name="$(
docker volume ls --format '{{.Name}}' \
| awk '$0 ~ /(^|_)postgres_dev_data$/ { print; exit }' || true
)"
if [[ -n "${volume_name}" ]]; then
printf '%s\n' "${volume_name}"
return
fi
printf '%s\n' "${expected_volume}"
}
reset_database() {
if [[ "${RESET_DB}" != "true" ]]; then
return
fi
local volume_name
volume_name="$(resolve_postgres_volume_name)"
warn "开始重置本机开发数据库,将删除 Docker volume${volume_name}"
"${COMPOSE_CMD[@]}" stop web frontend backend postgres >/dev/null 2>&1 || true
"${COMPOSE_CMD[@]}" rm -f -s postgres >/dev/null 2>&1 || true
if docker volume inspect "${volume_name}" >/dev/null 2>&1; then
docker volume rm "${volume_name}" >/dev/null || fail "删除 PostgreSQL 开发数据卷失败:${volume_name}"
log "已删除 PostgreSQL 开发数据卷:${volume_name}"
return
fi
warn "未找到 PostgreSQL 开发数据卷:${volume_name},将按空库继续启动。"
}
wait_for_service() {
local service="$1"
local label="$2"
local attempt container_id status health_status
log "等待 ${label} 就绪"
for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
container_id="$("${COMPOSE_CMD[@]}" ps -q "${service}" 2>/dev/null || true)"
if [[ -n "${container_id}" ]]; then
status="$(docker inspect -f '{{.State.Status}}' "${container_id}" 2>/dev/null || true)"
health_status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "${container_id}" 2>/dev/null || true)"
if [[ -n "${health_status}" && "${health_status}" == "healthy" ]]; then
log "${label} 已就绪"
return
fi
if [[ -z "${health_status}" && "${status}" == "running" ]]; then
log "${label} 已启动"
return
fi
if [[ "${status}" == "exited" || "${status}" == "dead" ]]; then
break
fi
fi
sleep 1
done
print_diagnostics
fail "${label} 启动超时,请根据上面的日志排查。"
}
start_dev_stack() {
log "启动 Mac 本机 Docker 开发环境"
if "${COMPOSE_CMD[@]}" up -d --build; then
wait_for_service postgres "PostgreSQL"
wait_for_service minio "MinIO"
wait_for_service backend "后端"
wait_for_service frontend "前端"
wait_for_service web "Web 入口"
return
fi
print_diagnostics
fail "Mac 本机 Docker 开发环境启动失败,请根据上面的日志排查。"
}
run_database_migrations() {
log "执行数据库迁移"
if "${COMPOSE_CMD[@]}" exec -T backend npm run db:migrate; then
log "数据库迁移完成"
return
fi
print_diagnostics
fail "数据库迁移失败,请根据上面的日志排查。"
}
wait_for_health() {
log "等待开发入口健康检查:${HEALTH_URL}"
for ((attempt = 1; attempt <= READY_TIMEOUT; attempt++)); do
if curl -fsS "${HEALTH_URL}" >/dev/null 2>&1; then
log "开发环境已就绪"
return
fi
sleep 1
done
print_diagnostics
fail "开发环境已启动但健康检查未通过,请根据日志排查。"
}
print_applied_migrations() {
log "已应用数据库迁移"
local sql migrations
sql='SELECT COALESCE(filename, name) FROM schema_migrations ORDER BY COALESCE(filename, name);'
if migrations="$("${COMPOSE_CMD[@]}" exec -T postgres sh -lc 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "$1"' sh "${sql}" 2>&1)"; then
printf '%s\n' "${migrations:-"(无迁移记录)"}"
return
fi
warn "读取迁移记录失败:"
printf '%s\n' "${migrations}"
}
print_diagnostics() {
warn "当前 dev 容器状态:"
"${COMPOSE_CMD[@]}" ps || true
warn "后端最近日志:"
local backend_logs
backend_logs="$("${COMPOSE_CMD[@]}" logs --tail=120 backend 2>&1 || true)"
printf '%s\n' "${backend_logs}"
if printf '%s\n' "${backend_logs}" | grep -q 'password authentication failed for user "postgres"'; then
warn "检测到 PostgreSQL 密码认证失败。通常是 postgres_dev_data 已用旧密码初始化,而 .env 改成了新密码。"
warn "无本地数据保留需求时可执行:RESET_DB_CONFIRM=1 bash deploy/mac-dev.sh --reset-db"
fi
warn "前端最近日志:"
"${COMPOSE_CMD[@]}" logs --tail=80 frontend || true
warn "MinIO 最近日志:"
"${COMPOSE_CMD[@]}" logs --tail=80 minio || true
warn "后端 ready 接口:"
"${COMPOSE_CMD[@]}" exec -T backend sh -lc 'curl -fsS http://127.0.0.1:3000/health/ready || true' || true
print_applied_migrations || true
}
print_endpoints() {
local minio_console_port
minio_console_port="$(read_effective_env_value MINIO_CONSOLE_PORT 19001)"
log "常用入口"
printf '%s\n' " 前端、后台、领取页:http://localhost"
printf '%s\n' " 后端健康检查:http://localhost/health"
printf '%s\n' " 后端 ready 检查:http://localhost/health/ready"
printf '%s\n' " MinIO 控制台:http://localhost:${minio_console_port}"
}
main() {
parse_args "$@"
cd "${PROJECT_ROOT}"
ensure_docker
ensure_env_file
validate_env_file
confirm_reset_database
ensure_data_dir
check_compose_config
reset_database
start_dev_stack
run_database_migrations
wait_for_health
print_applied_migrations
log "Mac 本机开发环境启动完成"
"${COMPOSE_CMD[@]}" ps
print_endpoints
}
main "$@"