#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.dev.yml" BACKEND_ENV="${ROOT_DIR}/backend/.env" BACKEND_ENV_EXAMPLE="${ROOT_DIR}/backend/.env.example" BACKEND_PID="" FRONTEND_PID="" CLEANED_UP=0 # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[1;34m' NC='\033[0m' log() { printf "${BLUE}[dev]${NC} %s\n" "$*" } log_success() { printf "${GREEN}[dev]${NC} %s\n" "$*" } log_warn() { printf "${YELLOW}[dev]${NC} %s\n" "$*" } log_error() { printf "${RED}[dev]${NC} %s\n" "$*" >&2 } need_cmd() { if ! command -v "$1" >/dev/null 2>&1; then log_error "缺少命令:$1" exit 1 fi } show_help() { cat << EOF 用法: ./scripts/dev.sh [选项] 选项: --reset-db 重置数据库(删除并重建) --seed-only 仅重新加载种子数据 --no-frontend 不启动前端 --no-backend 不启动后端 -h, --help 显示帮助信息 环境变量: DEV_RESET_DB=1 等同于 --reset-db 示例: ./scripts/dev.sh # 正常启动 ./scripts/dev.sh --reset-db # 重置数据库后启动 ./scripts/dev.sh --seed-only # 重新加载种子数据 EOF exit 0 } # 参数解析 RESET_DB="${DEV_RESET_DB:-0}" SEED_ONLY=0 NO_FRONTEND=0 NO_BACKEND=0 while [[ $# -gt 0 ]]; do case "$1" in --reset-db) RESET_DB=1 shift ;; --seed-only) SEED_ONLY=1 shift ;; --no-frontend) NO_FRONTEND=1 shift ;; --no-backend) NO_BACKEND=1 shift ;; -h|--help) show_help ;; *) log_error "未知选项: $1" show_help ;; esac done cleanup() { if [[ "${CLEANED_UP}" == "1" ]]; then return fi CLEANED_UP=1 log "正在停止前后端开发进程..." if [[ -n "${FRONTEND_PID}" ]] && kill -0 "${FRONTEND_PID}" >/dev/null 2>&1; then kill "${FRONTEND_PID}" >/dev/null 2>&1 || true fi if [[ -n "${BACKEND_PID}" ]] && kill -0 "${BACKEND_PID}" >/dev/null 2>&1; then kill "${BACKEND_PID}" >/dev/null 2>&1 || true fi } wait_container_healthy() { local container_name="$1" local label="$2" local attempt log "等待 ${label} 就绪..." for attempt in {1..60}; do local status status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' "${container_name}" 2>/dev/null || true)" if [[ "${status}" == "healthy" || "${status}" == "running" ]]; then log "${label} 已就绪" return 0 fi sleep 2 done printf "%s 启动超时,请检查:docker logs %s\n" "${label}" "${container_name}" >&2 exit 1 } init_database() { if [[ "${RESET_DB}" == "1" ]]; then log_warn "重置数据库..." docker exec hfb-mysql mysql -uroot -prootsecret -e \ "DROP DATABASE IF EXISTS hfb_sys; CREATE DATABASE hfb_sys CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" fi local table_count table_count="$( docker exec hfb-mysql mysql -uhfb -psecret -N -s -e \ "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'hfb_sys' AND table_name = 'users';" 2>/dev/null )" if [[ "${table_count}" == "0" ]]; then log "首次启动,初始化数据库..." local migration for migration in "${ROOT_DIR}"/backend/migrations/*.sql; do log "执行迁移:$(basename "${migration}")" docker exec -i hfb-mysql mysql -uhfb -psecret --default-character-set=utf8mb4 hfb_sys < "${migration}" done log_success "数据库初始化完成" elif [[ "${SEED_ONLY}" == "1" ]]; then log "重新加载种子数据..." docker exec -i hfb-mysql mysql -uhfb -psecret --default-character-set=utf8mb4 hfb_sys < "${ROOT_DIR}/backend/migrations/000002_seed.sql" log_success "种子数据加载完成" else log "数据库已存在,跳过迁移(使用 --reset-db 重建)" fi } load_env_file() { local env_file="$1" local line while IFS= read -r line || [[ -n "${line}" ]]; do [[ -z "${line}" || "${line}" =~ ^[[:space:]]*# ]] && continue line="${line#export }" export "${line}" done < "${env_file}" } start_backend() { if [[ ! -f "${BACKEND_ENV}" ]]; then log "创建 backend/.env" cp "${BACKEND_ENV_EXAMPLE}" "${BACKEND_ENV}" fi log "启动后端:http://localhost:8080" ( cd "${ROOT_DIR}/backend" load_env_file "${BACKEND_ENV}" go run ./cmd/api ) & BACKEND_PID="$!" } start_frontend() { if [[ ! -d "${ROOT_DIR}/frontend/node_modules" ]]; then log "安装前端依赖..." ( cd "${ROOT_DIR}/frontend" npm ci ) fi log "启动前端:http://localhost:5173" ( cd "${ROOT_DIR}/frontend" npm run dev ) & FRONTEND_PID="$!" } watch_processes() { while true; do if ! kill -0 "${BACKEND_PID}" >/dev/null 2>&1; then wait "${BACKEND_PID}" || true printf "后端进程已退出\n" >&2 exit 1 fi if ! kill -0 "${FRONTEND_PID}" >/dev/null 2>&1; then wait "${FRONTEND_PID}" || true printf "前端进程已退出\n" >&2 exit 1 fi sleep 2 done } main() { need_cmd docker need_cmd go need_cmd npm trap cleanup EXIT trap 'cleanup; exit 130' INT TERM log "启动开发依赖:MySQL、Redis、MinIO" docker compose -f "${COMPOSE_FILE}" up -d wait_container_healthy hfb-mysql "MySQL" wait_container_healthy hfb-redis "Redis" wait_container_healthy hfb-minio "MinIO" init_database # 仅加载种子数据模式 if [[ "${SEED_ONLY}" == "1" ]]; then log_success "种子数据加载完成,退出" exit 0 fi if [[ "${NO_BACKEND}" == "0" ]]; then start_backend fi if [[ "${NO_FRONTEND}" == "0" ]]; then start_frontend fi log_success "开发环境已启动" log "前台:http://localhost:5173" log "后台:http://localhost:5173/admin/login(admin / admin123456)" log "按 Ctrl+C 停止前后端;Docker 依赖会保留运行" watch_processes } main "$@"