chore(init): 初始化解忧商行项目并完成 Phase 0 基座

This commit is contained in:
yml2213
2026-10-30 11:46:10 +08:00
commit 8d9481c251
689 changed files with 138703 additions and 0 deletions
Executable
+764
View File
@@ -0,0 +1,764 @@
#!/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"
FRONTEND_DIR="${ROOT_DIR}/frontend"
FRONTEND_PACKAGE_JSON="${FRONTEND_DIR}/package.json"
FRONTEND_PACKAGE_LOCK="${FRONTEND_DIR}/package-lock.json"
FRONTEND_NODE_MODULES="${FRONTEND_DIR}/node_modules"
FRONTEND_NODE_MODULES_LOCK="${FRONTEND_NODE_MODULES}/.package-lock.json"
DISTRIBUTION_DIR="${ROOT_DIR}/distribution-admin"
DISTRIBUTION_PACKAGE_JSON="${DISTRIBUTION_DIR}/package.json"
DISTRIBUTION_PACKAGE_LOCK="${DISTRIBUTION_DIR}/package-lock.json"
DISTRIBUTION_NODE_MODULES="${DISTRIBUTION_DIR}/node_modules"
DISTRIBUTION_NODE_MODULES_LOCK="${DISTRIBUTION_NODE_MODULES}/.package-lock.json"
MIGRATIONS_DIR="${ROOT_DIR}/backend/migrations"
MYSQL_DATABASE="${DEV_MYSQL_DATABASE:-hfb_jysh}"
BACKEND_PORT="${DEV_BACKEND_PORT:-18089}"
FRONTEND_PORT="${DEV_FRONTEND_PORT:-5289}"
DISTRIBUTION_PORT="${DEV_DISTRIBUTION_PORT:-5290}"
DISTRIBUTION_STOREFRONT_PORT="${DEV_DISTRIBUTION_STOREFRONT_PORT:-5291}"
DISTRIBUTION_STOREFRONT_SITE_CODE="${DEV_DISTRIBUTION_STOREFRONT_SITE_CODE:-test_1}"
MYSQL_PORT="${DEV_MYSQL_PORT:-23306}"
REDIS_PORT="${DEV_REDIS_PORT:-26379}"
MINIO_API_PORT="${DEV_MINIO_API_PORT:-29090}"
MINIO_CONSOLE_PORT="${DEV_MINIO_CONSOLE_PORT:-29091}"
BACKEND_HEALTH_URL="${DEV_BACKEND_HEALTH_URL:-http://127.0.0.1:${BACKEND_PORT}/health}"
FRONTEND_HEALTH_URL="${DEV_FRONTEND_HEALTH_URL:-http://127.0.0.1:${FRONTEND_PORT}/}"
DISTRIBUTION_HEALTH_URL="${DEV_DISTRIBUTION_HEALTH_URL:-http://127.0.0.1:${DISTRIBUTION_PORT}/}"
DISTRIBUTION_STOREFRONT_HEALTH_URL="${DEV_DISTRIBUTION_STOREFRONT_HEALTH_URL:-http://127.0.0.1:${DISTRIBUTION_STOREFRONT_PORT}/}"
HTTP_READY_TIMEOUT="${DEV_HTTP_READY_TIMEOUT:-120}"
AUTO_KILL_PORTS="${DEV_AUTO_KILL_PORTS:-1}"
FORCE_NPM_CI="${DEV_FORCE_NPM_CI:-0}"
GOOSE_VERSION="${GOOSE_VERSION:-v3.27.1}"
GOOSE_BIN="${GOOSE_BIN:-}"
BACKEND_PID=""
FRONTEND_PID=""
DISTRIBUTION_PID=""
DISTRIBUTION_STOREFRONT_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 重置数据库(删除并重建)
--no-migrate 不执行数据库迁移
--no-frontend 不启动主站前端
--no-backend 不启动后端
--no-distribution 不启动分销后台
--no-distribution-storefront
不启动分销前台商城
-h, --help 显示帮助信息
环境变量:
DEV_RESET_DB=1 等同于 --reset-db
DEV_MYSQL_DATABASE=hfb_jysh
开发数据库名
DEV_BACKEND_PORT=18089
后端启动端口
DEV_FRONTEND_PORT=5289
主站前端启动端口
DEV_DISTRIBUTION_PORT=5290
分销后台启动端口
DEV_MYSQL_PORT=23306
MySQL 宿主机映射端口
DEV_REDIS_PORT=26379
Redis 宿主机映射端口
DEV_MINIO_API_PORT=29090
MinIO API 宿主机映射端口
DEV_MINIO_CONSOLE_PORT=29091
MinIO 控制台宿主机映射端口
DEV_BACKEND_HEALTH_URL=http://127.0.0.1:18089/health
后端健康检查地址
DEV_FRONTEND_HEALTH_URL=http://127.0.0.1:5289/
主站前端健康检查地址
DEV_DISTRIBUTION_HEALTH_URL=http://127.0.0.1:5290/
分销后台健康检查地址
DEV_HTTP_READY_TIMEOUT=120
HTTP 服务就绪等待秒数
DEV_AUTO_KILL_PORTS=1
前后端端口被占用时自动停止占用的 Docker 容器或本机进程
DEV_FORCE_NPM_CI=0
强制重新执行前端 npm ci
示例:
./scripts/dev.sh # 正常启动(后端 + 主前后台 + 分销前台)
./scripts/dev.sh --reset-db # 重置数据库后启动(推荐迁移后使用)
EOF
exit 0
}
# 参数解析
RESET_DB="${DEV_RESET_DB:-0}"
NO_MIGRATE=0
NO_FRONTEND=0
NO_BACKEND=0
NO_DISTRIBUTION=0
NO_DISTRIBUTION_STOREFRONT=0
while [[ $# -gt 0 ]]; do
case "$1" in
--reset-db)
RESET_DB=1
shift
;;
--no-migrate)
NO_MIGRATE=1
shift
;;
--no-frontend)
NO_FRONTEND=1
shift
;;
--no-backend)
NO_BACKEND=1
shift
;;
--no-distribution)
NO_DISTRIBUTION=1
shift
;;
--no-distribution-storefront)
NO_DISTRIBUTION_STOREFRONT=1
shift
;;
-h|--help)
show_help
;;
*)
log_error "未知选项: $1"
show_help
;;
esac
done
cleanup() {
if [[ "${CLEANED_UP}" == "1" ]]; then
return
fi
CLEANED_UP=1
if [[ -z "${FRONTEND_PID}" && -z "${BACKEND_PID}" && -z "${DISTRIBUTION_PID}" && -z "${DISTRIBUTION_STOREFRONT_PID}" ]]; then
return
fi
log "正在停止前后端开发进程..."
if [[ -n "${DISTRIBUTION_STOREFRONT_PID}" ]] && kill -0 "${DISTRIBUTION_STOREFRONT_PID}" >/dev/null 2>&1; then
kill "${DISTRIBUTION_STOREFRONT_PID}" >/dev/null 2>&1 || true
fi
if [[ -n "${DISTRIBUTION_PID}" ]] && kill -0 "${DISTRIBUTION_PID}" >/dev/null 2>&1; then
kill "${DISTRIBUTION_PID}" >/dev/null 2>&1 || true
fi
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
}
wait_http_ready() {
local url="$1"
local label="$2"
local pid="${3:-}"
local attempt
log "等待 ${label} HTTP 服务就绪..."
for ((attempt = 1; attempt <= HTTP_READY_TIMEOUT; attempt++)); do
if [[ -n "${pid}" ]] && ! kill -0 "${pid}" >/dev/null 2>&1; then
wait "${pid}" || true
log_error "${label} 进程已退出,请检查上方日志"
exit 1
fi
if curl -fsS --max-time 2 "${url}" >/dev/null 2>&1; then
log_success "${label} 已就绪:${url}"
return 0
fi
sleep 1
done
log_error "${label} HTTP 服务启动超时:${url}"
exit 1
}
port_in_use() {
local port="$1"
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
return $?
fi
if command -v nc >/dev/null 2>&1; then
nc -z 127.0.0.1 "${port}" >/dev/null 2>&1
return $?
fi
return 1
}
wait_port_released() {
local port="$1"
local timeout="${2:-5}"
local attempt
for ((attempt = 1; attempt <= timeout; attempt++)); do
if ! port_in_use "${port}"; then
return 0
fi
sleep 1
done
return 1
}
is_allowed_container() {
local container_name="$1"
shift
local allowed_name
for allowed_name in "$@"; do
if [[ "${container_name}" == "${allowed_name}" ]]; then
return 0
fi
done
return 1
}
docker_containers_on_host_port() {
local port="$1"
docker ps --format '{{.ID}}\t{{.Names}}\t{{.Ports}}' 2>/dev/null |
awk -F '\t' -v port="${port}" 'index($3, ":" port "->") || $3 ~ ("(^|[ ,])" port "->") { print $1 "\t" $2 }'
}
stop_docker_containers_on_port() {
local port="$1"
local label="$2"
shift 2
local containers container_id container_name allowed_found=0 stopped_found=0
containers="$(docker_containers_on_host_port "${port}" || true)"
if [[ -z "${containers}" ]]; then
return 1
fi
while IFS=$'\t' read -r container_id container_name; do
[[ -z "${container_id}" ]] && continue
if is_allowed_container "${container_name}" "$@"; then
allowed_found=1
log "${label}端口 ${port} 已由本项目容器 ${container_name} 使用,保留运行"
continue
fi
if [[ "${AUTO_KILL_PORTS}" != "1" ]]; then
log_error "${label}端口 ${port} 被 Docker 容器 ${container_name} 占用"
exit 1
fi
log_warn "${label}端口 ${port} 被 Docker 容器 ${container_name} 占用,正在停止..."
docker stop "${container_id}" >/dev/null
stopped_found=1
log_success "已停止容器 ${container_name}"
done <<< "${containers}"
if [[ "${allowed_found}" == "1" ]]; then
return 0
fi
if [[ "${stopped_found}" == "1" ]]; then
wait_port_released "${port}" 5 || true
fi
return 1
}
process_args() {
local pid="$1"
ps -p "${pid}" -o args= 2>/dev/null || true
}
is_docker_proxy_process() {
local args="$1"
[[ "${args}" == *"com.docker"* || "${args}" == *"Docker Desktop"* || "${args}" == *"vpnkit"* || "${args}" == *"OrbStack"* ]]
}
ensure_port_free() {
local port="$1"
local label="$2"
shift 2
if stop_docker_containers_on_port "${port}" "${label}" "$@"; then
return 0
fi
if command -v lsof >/dev/null 2>&1; then
local pids pid args
pids="$(lsof -nP -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sort -u || true)"
if [[ -z "${pids}" ]]; then
return 0
fi
if [[ "${AUTO_KILL_PORTS}" != "1" ]]; then
log_error "${label}端口 ${port} 已被占用,请先停止旧进程或调整端口"
exit 1
fi
log_warn "${label}端口 ${port} 已被占用,正在停止占用进程..."
while IFS= read -r pid; do
[[ -z "${pid}" ]] && continue
args="$(process_args "${pid}")"
if is_docker_proxy_process "${args}"; then
log_error "${label}端口 ${port} 仍由 Docker 代理进程占用,但未找到可自动停止的容器,请执行 docker ps 检查"
exit 1
fi
log_warn "停止 PID ${pid}: ${args:-未知进程}"
kill "${pid}" >/dev/null 2>&1 || true
done <<< "${pids}"
if ! wait_port_released "${port}" 5; then
log_warn "${label}端口 ${port} 未及时释放,尝试强制停止..."
pids="$(lsof -nP -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sort -u || true)"
while IFS= read -r pid; do
[[ -z "${pid}" ]] && continue
args="$(process_args "${pid}")"
if is_docker_proxy_process "${args}"; then
log_error "${label}端口 ${port} 仍由 Docker 代理进程占用,但未找到可自动停止的容器,请执行 docker ps 检查"
exit 1
fi
log_warn "强制停止 PID ${pid}: ${args:-未知进程}"
kill -9 "${pid}" >/dev/null 2>&1 || true
done <<< "${pids}"
if ! wait_port_released "${port}" 5; then
log_error "${label}端口 ${port} 释放失败,请手动检查占用进程"
exit 1
fi
fi
log_success "${label}端口 ${port} 已释放"
return 0
fi
if command -v nc >/dev/null 2>&1; then
if nc -z 127.0.0.1 "${port}" >/dev/null 2>&1; then
log_error "${label}端口 ${port} 已被占用,但未找到 lsof,无法自动定位并停止占用进程"
exit 1
fi
return 0
fi
log_warn "未找到 lsof 或 nc,跳过 ${label}端口 ${port} 占用检查"
}
mysql_root() {
docker exec -i -e MYSQL_PWD=rootsecret jysh-mysql mysql -uroot "$@"
}
validate_database_name() {
if [[ ! "${MYSQL_DATABASE}" =~ ^[A-Za-z0-9_]+$ ]]; then
log_error "DEV_MYSQL_DATABASE 只能包含字母、数字和下划线,当前值:${MYSQL_DATABASE}"
exit 1
fi
}
reset_database() {
if [[ "${RESET_DB}" != "1" ]]; then
return 0
fi
log_warn "重置数据库 ${MYSQL_DATABASE}..."
mysql_root -e \
"DROP DATABASE IF EXISTS \`${MYSQL_DATABASE}\`; CREATE DATABASE \`${MYSQL_DATABASE}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
log_success "数据库已重建"
}
ensure_goose() {
if [[ -z "${GOOSE_BIN}" ]]; then
GOOSE_BIN="$(command -v goose || true)"
fi
if [[ -z "${GOOSE_BIN}" ]]; then
need_cmd go
log "未找到 goose,正在安装 ${GOOSE_VERSION}..."
(cd "${ROOT_DIR}/backend" && go install "github.com/pressly/goose/v3/cmd/goose@${GOOSE_VERSION}")
GOOSE_BIN="$(go env GOPATH)/bin/goose"
fi
if [[ ! -x "${GOOSE_BIN}" ]]; then
log_error "goose 不可执行:${GOOSE_BIN}"
exit 1
fi
}
goose_dsn() {
local dsn="$1"
if [[ "${dsn}" != *"multiStatements="* ]]; then
if [[ "${dsn}" == *"?"* ]]; then
dsn="${dsn}&multiStatements=true"
else
dsn="${dsn}?multiStatements=true"
fi
fi
printf "%s" "${dsn}"
}
dev_goose_dsn() {
goose_dsn "hfb:secret@tcp(127.0.0.1:${MYSQL_PORT})/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local"
}
goose_cmd() {
"${GOOSE_BIN}" -dir "${MIGRATIONS_DIR}" mysql "$(dev_goose_dsn)" "$@"
}
run_migrations() {
if [[ "${NO_MIGRATE}" == "1" ]]; then
log_warn "已跳过数据库迁移"
return 0
fi
ensure_goose
log "使用 goose 执行数据库迁移..."
goose_cmd up
log_success "数据库迁移完成"
}
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:${BACKEND_PORT}"
(
cd "${ROOT_DIR}/backend"
load_env_file "${BACKEND_ENV}"
export APP_ADDR=":${BACKEND_PORT}"
export MYSQL_DSN="hfb:secret@tcp(127.0.0.1:${MYSQL_PORT})/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local"
export REDIS_ADDR="127.0.0.1:${REDIS_PORT}"
export STORAGE_ENDPOINT="http://localhost:${MINIO_API_PORT}"
go run ./cmd/api
) &
BACKEND_PID="$!"
}
frontend_dependencies_need_ci() {
if [[ "${FORCE_NPM_CI}" == "1" ]]; then
return 0
fi
if [[ ! -d "${FRONTEND_NODE_MODULES}" ]]; then
return 0
fi
if [[ ! -f "${FRONTEND_NODE_MODULES_LOCK}" ]]; then
return 0
fi
if [[ "${FRONTEND_PACKAGE_JSON}" -nt "${FRONTEND_NODE_MODULES_LOCK}" || "${FRONTEND_PACKAGE_LOCK}" -nt "${FRONTEND_NODE_MODULES_LOCK}" ]]; then
return 0
fi
(
cd "${FRONTEND_DIR}"
npm ls --depth=0 >/dev/null 2>&1
) || return 0
return 1
}
ensure_frontend_dependencies() {
if [[ ! -f "${FRONTEND_PACKAGE_LOCK}" ]]; then
log_error "缺少 frontend/package-lock.json,无法执行 npm ci"
exit 1
fi
if frontend_dependencies_need_ci; then
log "同步前端依赖(npm ci)..."
(
cd "${FRONTEND_DIR}"
npm ci
)
log_success "前端依赖已同步"
else
log "前端依赖已同步,跳过 npm ci"
fi
}
start_frontend() {
ensure_frontend_dependencies
log "启动前端:http://localhost:${FRONTEND_PORT}"
(
cd "${FRONTEND_DIR}"
export DEV_FRONTEND_PORT="${FRONTEND_PORT}"
export DEV_BACKEND_PORT="${BACKEND_PORT}"
export VITE_API_PROXY_TARGET="${VITE_API_PROXY_TARGET:-http://127.0.0.1:${BACKEND_PORT}}"
npm run dev
) &
FRONTEND_PID="$!"
}
distribution_dependencies_need_ci() {
if [[ "${FORCE_NPM_CI}" == "1" ]]; then
return 0
fi
if [[ ! -d "${DISTRIBUTION_NODE_MODULES}" ]]; then
return 0
fi
if [[ ! -f "${DISTRIBUTION_NODE_MODULES_LOCK}" ]]; then
return 0
fi
if [[ "${DISTRIBUTION_PACKAGE_JSON}" -nt "${DISTRIBUTION_NODE_MODULES_LOCK}" || "${DISTRIBUTION_PACKAGE_LOCK}" -nt "${DISTRIBUTION_NODE_MODULES_LOCK}" ]]; then
return 0
fi
(
cd "${DISTRIBUTION_DIR}"
npm ls --depth=0 >/dev/null 2>&1
) || return 0
return 1
}
ensure_distribution_dependencies() {
if [[ ! -f "${DISTRIBUTION_PACKAGE_LOCK}" ]]; then
log_error "缺少 distribution-admin/package-lock.json,无法执行 npm ci"
exit 1
fi
if distribution_dependencies_need_ci; then
log "同步分销后台依赖(npm ci)..."
(
cd "${DISTRIBUTION_DIR}"
npm ci
)
log_success "分销后台依赖已同步"
else
log "分销后台依赖已同步,跳过 npm ci"
fi
}
start_distribution() {
ensure_distribution_dependencies
log "启动分销后台:http://localhost:${DISTRIBUTION_PORT}"
(
cd "${DISTRIBUTION_DIR}"
export PORT="${DISTRIBUTION_PORT}"
export DEV_DISTRIBUTION_PORT="${DISTRIBUTION_PORT}"
export DEV_BACKEND_PORT="${BACKEND_PORT}"
export VITE_API_PROXY_TARGET="${VITE_API_PROXY_TARGET:-http://127.0.0.1:${BACKEND_PORT}}"
npm run dev
) &
DISTRIBUTION_PID="$!"
}
start_distribution_storefront() {
ensure_frontend_dependencies
log "启动分销前台:http://localhost:${DISTRIBUTION_STOREFRONT_PORT} (分销站: ${DISTRIBUTION_STOREFRONT_SITE_CODE})"
(
cd "${FRONTEND_DIR}"
export DEV_FRONTEND_PORT="${DISTRIBUTION_STOREFRONT_PORT}"
export DEV_BACKEND_PORT="${BACKEND_PORT}"
export VITE_API_PROXY_TARGET="${VITE_API_PROXY_TARGET:-http://127.0.0.1:${BACKEND_PORT}}"
export VITE_DISTRIBUTION_SITE_CODE="${DISTRIBUTION_STOREFRONT_SITE_CODE}"
npm run dev
) &
DISTRIBUTION_STOREFRONT_PID="$!"
}
watch_processes() {
while true; do
if [[ -n "${BACKEND_PID}" ]] && ! kill -0 "${BACKEND_PID}" >/dev/null 2>&1; then
wait "${BACKEND_PID}" || true
printf "后端进程已退出\n" >&2
exit 1
fi
if [[ -n "${FRONTEND_PID}" ]] && ! kill -0 "${FRONTEND_PID}" >/dev/null 2>&1; then
wait "${FRONTEND_PID}" || true
printf "主站前端进程已退出\n" >&2
exit 1
fi
if [[ -n "${DISTRIBUTION_PID}" ]] && ! kill -0 "${DISTRIBUTION_PID}" >/dev/null 2>&1; then
wait "${DISTRIBUTION_PID}" || true
printf "分销后台进程已退出\n" >&2
exit 1
fi
if [[ -n "${DISTRIBUTION_STOREFRONT_PID}" ]] && ! kill -0 "${DISTRIBUTION_STOREFRONT_PID}" >/dev/null 2>&1; then
wait "${DISTRIBUTION_STOREFRONT_PID}" || true
printf "分销前台商城进程已退出\n" >&2
exit 1
fi
sleep 2
done
}
main() {
need_cmd docker
if [[ "${NO_BACKEND}" == "0" ]]; then
need_cmd go
fi
if [[ "${NO_FRONTEND}" == "0" || "${NO_DISTRIBUTION}" == "0" ]]; then
need_cmd npm
fi
if [[ "${NO_BACKEND}" == "0" || "${NO_FRONTEND}" == "0" || "${NO_DISTRIBUTION}" == "0" ]]; then
need_cmd curl
fi
trap cleanup EXIT
trap 'cleanup; exit 130' INT TERM
validate_database_name
if [[ "${RESET_DB}" == "1" && "${NO_MIGRATE}" == "1" ]]; then
log_error "--reset-db 会清空数据库,不能和 --no-migrate 同时使用"
exit 1
fi
ensure_port_free "${MYSQL_PORT}" "MySQL" jysh-mysql
ensure_port_free "${REDIS_PORT}" "Redis" jysh-redis
ensure_port_free "${MINIO_API_PORT}" "MinIO API" jysh-minio
ensure_port_free "${MINIO_CONSOLE_PORT}" "MinIO 控制台" jysh-minio
log "启动开发依赖:MySQL、Redis、MinIO"
export DEV_MYSQL_PORT="${MYSQL_PORT}"
export DEV_REDIS_PORT="${REDIS_PORT}"
export DEV_MINIO_API_PORT="${MINIO_API_PORT}"
export DEV_MINIO_CONSOLE_PORT="${MINIO_CONSOLE_PORT}"
docker compose -f "${COMPOSE_FILE}" up -d
wait_container_healthy jysh-mysql "MySQL"
wait_container_healthy jysh-redis "Redis"
wait_container_healthy jysh-minio "MinIO"
reset_database
run_migrations
if [[ "${NO_BACKEND}" == "0" ]]; then
ensure_port_free "${BACKEND_PORT}" "后端"
start_backend
wait_http_ready "${BACKEND_HEALTH_URL}" "后端" "${BACKEND_PID}"
fi
if [[ "${NO_FRONTEND}" == "0" ]]; then
ensure_port_free "${FRONTEND_PORT}" "主站前端"
start_frontend
wait_http_ready "${FRONTEND_HEALTH_URL}" "主站前端" "${FRONTEND_PID}"
fi
if [[ "${NO_DISTRIBUTION}" == "0" && -d "${DISTRIBUTION_DIR}" ]]; then
ensure_port_free "${DISTRIBUTION_PORT}" "分销后台"
start_distribution
wait_http_ready "${DISTRIBUTION_HEALTH_URL}" "分销后台" "${DISTRIBUTION_PID}"
fi
if [[ "${NO_DISTRIBUTION_STOREFRONT}" == "0" && "${NO_FRONTEND}" == "0" ]]; then
ensure_port_free "${DISTRIBUTION_STOREFRONT_PORT}" "分销前台"
start_distribution_storefront
wait_http_ready "${DISTRIBUTION_STOREFRONT_HEALTH_URL}" "分销前台" "${DISTRIBUTION_STOREFRONT_PID}"
fi
if [[ -z "${BACKEND_PID}" && -z "${FRONTEND_PID}" && -z "${DISTRIBUTION_PID}" && -z "${DISTRIBUTION_STOREFRONT_PID}" ]]; then
log "仅启动 Docker 开发依赖,前后端与分销端未启动"
return 0
fi
log_success "开发环境已就绪"
if [[ "${NO_FRONTEND}" == "0" ]]; then
local admin_base_path="${VITE_ADMIN_BASE_PATH:-/admin}"
log "主站前台:http://localhost:${FRONTEND_PORT}"
log "主站后台:http://localhost:${FRONTEND_PORT}${admin_base_path%/}/login"
fi
if [[ "${NO_DISTRIBUTION}" == "0" && -d "${DISTRIBUTION_DIR}" ]]; then
log "分销后台:http://localhost:${DISTRIBUTION_PORT}"
fi
if [[ "${NO_DISTRIBUTION_STOREFRONT}" == "0" && "${NO_FRONTEND}" == "0" ]]; then
log "分销前台:http://localhost:${DISTRIBUTION_STOREFRONT_PORT} (直连分销站: ${DISTRIBUTION_STOREFRONT_SITE_CODE})"
fi
log "按 Ctrl+C 停止所有服务;Docker 依赖会保留运行"
watch_processes
}
main "$@"