487 lines
16 KiB
Bash
Executable File
487 lines
16 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 如果用 sh 调用,自动切换到 bash
|
||
if [ -z "$BASH_VERSION" ]; then
|
||
exec bash "$0" "$@"
|
||
fi
|
||
# dev.sh — 本地开发环境管理
|
||
#
|
||
# 启动:
|
||
# sh scripts/dev.sh run # 启动前后端(macOS 桌面端)
|
||
# sh scripts/dev.sh run --force # 强制重启后端
|
||
# sh scripts/dev.sh run --web # 启动前后端(Chrome Web 端)
|
||
# sh scripts/dev.sh run --all # 启动前后端(macOS + Web 同时)
|
||
# sh scripts/dev.sh --backend-only # 仅启动后端
|
||
# sh scripts/dev.sh --frontend-only # 仅启动前端(macOS)
|
||
# sh scripts/dev.sh --web-only # 仅启动前端(Web)
|
||
#
|
||
# 数据库:
|
||
# sh scripts/dev.sh seed H001 # 清空并写入 H001 测试数据(SQL 文件)
|
||
# sh scripts/dev.sh reset # 删表重建(AutoMigrate)
|
||
# sh scripts/dev.sh clear # 清空所有业务数据
|
||
|
||
set -e
|
||
|
||
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"
|
||
|
||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||
BACKEND_DIR="$ROOT/backend"
|
||
CLIENT_DIR="$ROOT/client"
|
||
LOG_DIR="$ROOT/scripts/.logs"
|
||
BACKEND_LOG="$LOG_DIR/backend.log"
|
||
BACKEND_PID_FILE="$LOG_DIR/backend.pid"
|
||
BACKEND_HASH_FILE="$LOG_DIR/backend.hash"
|
||
WEB_LOG="$LOG_DIR/web.log"
|
||
WEB_PID_FILE="$LOG_DIR/web.pid"
|
||
ELEVENTY_LOG="$LOG_DIR/eleventy.log"
|
||
ELEVENTY_PID_FILE="$LOG_DIR/eleventy.pid"
|
||
PROXY_PID_FILE="$LOG_DIR/proxy.pid"
|
||
|
||
mkdir -p "$LOG_DIR"
|
||
|
||
# ── seed 命令 ────────────────────────────────────────────────
|
||
# 用法: cmd_seed [--clear] <shop_code>
|
||
# 从 docker-compose.yml 读取容器信息,执行 backend/seeds/<shop>.sql
|
||
# --clear: 写入前先清空该门店所有数据
|
||
cmd_seed() {
|
||
local clear=false
|
||
local shop=""
|
||
while [ $# -gt 0 ]; do
|
||
case "$1" in
|
||
--clear) clear=true ;;
|
||
*) shop="$1" ;;
|
||
esac
|
||
shift
|
||
done
|
||
|
||
if [ -z "$shop" ]; then
|
||
error "用法: sh scripts/dev.sh seed [--clear] <shop_code> 示例: sh scripts/dev.sh seed S001"
|
||
exit 1
|
||
fi
|
||
|
||
local sql_file="$ROOT/backend/seeds/${shop}.sql"
|
||
if [ ! -f "$sql_file" ]; then
|
||
error "种子文件不存在: $sql_file"
|
||
exit 1
|
||
fi
|
||
|
||
# 从 docker-compose.yml 读取容器名和数据库信息
|
||
local compose_file="$ROOT/deploy/docker-compose.yml"
|
||
local container db_pass db_name
|
||
container=$(grep 'container_name:' "$compose_file" | grep -i mysql | head -1 | sed 's/.*container_name: *//')
|
||
db_pass=$(grep 'MYSQL_ROOT_PASSWORD:' "$compose_file" | head -1 | sed 's/.*MYSQL_ROOT_PASSWORD: *//')
|
||
db_name=$(grep 'MYSQL_DATABASE:' "$compose_file" | head -1 | sed 's/.*MYSQL_DATABASE: *//')
|
||
|
||
# 检查容器是否运行
|
||
if ! docker ps --format '{{.Names}}' | grep -q "^${container}$"; then
|
||
error "MySQL 容器 '${container}' 未运行,请先执行: docker compose -f deploy/docker-compose.yml up -d"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$clear" = "true" ]; then
|
||
local clear_file="$ROOT/backend/seeds/clear_shop.sql"
|
||
if [ ! -f "$clear_file" ]; then
|
||
error "清空脚本不存在: $clear_file"
|
||
exit 1
|
||
fi
|
||
info "清空 ${shop} 数据 → 容器 ${container} / ${db_name}"
|
||
{ printf "SET @shop_code='%s';\n" "$shop"; cat "$clear_file"; } \
|
||
| docker exec -i "$container" mysql --default-character-set=utf8mb4 -uroot -p"$db_pass" "$db_name"
|
||
fi
|
||
|
||
info "写入 ${shop} 种子数据 → 容器 ${container} / ${db_name}"
|
||
docker exec -i "$container" mysql --default-character-set=utf8mb4 -uroot -p"$db_pass" "$db_name" < "$sql_file"
|
||
success "✅ ${shop} 种子数据写入完成"
|
||
echo ""
|
||
echo " 登录信息:"
|
||
echo " 门店编号: ${shop}"
|
||
echo " admin / password123"
|
||
echo " operator / password123"
|
||
echo " test / password123"
|
||
}
|
||
|
||
# ── 颜色输出 ──────────────────────────────────────────────
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
RED='\033[0;31m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
|
||
info() { echo -e "${CYAN}[dev]${NC} $*"; }
|
||
success() { echo -e "${GREEN}[dev]${NC} $*"; }
|
||
warn() { echo -e "${YELLOW}[dev]${NC} $*"; }
|
||
error() { echo -e "${RED}[dev]${NC} $*"; }
|
||
|
||
# ── 参数解析 ──────────────────────────────────────────────
|
||
COMMAND="${1:-run}"
|
||
FORCE=false
|
||
RUN_BACKEND=true
|
||
RUN_FRONTEND=true # macOS
|
||
RUN_WEB=false
|
||
|
||
cmd_marketing() {
|
||
local WEB_DIR="$ROOT/web"
|
||
local FLUTTER_WEB_PORT=9090
|
||
local ELEVENTY_PORT=3001
|
||
local PROXY_PORT=3000
|
||
|
||
# 检查依赖
|
||
if ! command -v node &>/dev/null; then
|
||
error "未找到 node,请安装 Node.js"
|
||
exit 1
|
||
fi
|
||
if ! command -v flutter &>/dev/null; then
|
||
error "未找到 flutter"
|
||
exit 1
|
||
fi
|
||
|
||
# 清理旧进程
|
||
for pid_file in "$ELEVENTY_PID_FILE" "$PROXY_PID_FILE" "$WEB_PID_FILE"; do
|
||
if [ -f "$pid_file" ]; then
|
||
pid=$(cat "$pid_file")
|
||
kill "$pid" 2>/dev/null || true
|
||
rm -f "$pid_file"
|
||
fi
|
||
done
|
||
for port in $ELEVENTY_PORT $FLUTTER_WEB_PORT $PROXY_PORT; do
|
||
pids=$(lsof -ti:"$port" 2>/dev/null || true)
|
||
[ -n "$pids" ] && echo "$pids" | xargs kill -9 2>/dev/null || true
|
||
done
|
||
sleep 0.5
|
||
|
||
# 启动后端(如已运行则跳过)
|
||
if ! curl -s -o /dev/null http://localhost:8080/health 2>/dev/null; then
|
||
info "启动后端..."
|
||
cd "$BACKEND_DIR"
|
||
go run main.go >>"$BACKEND_LOG" 2>&1 &
|
||
local bp=$!
|
||
echo "$bp" > "$BACKEND_PID_FILE"
|
||
wait_backend
|
||
cd "$ROOT"
|
||
else
|
||
success "后端已在运行 → http://localhost:8080"
|
||
fi
|
||
|
||
# 启动 Eleventy(宣传站)在后台,3001 口
|
||
info "启动 Eleventy(宣传站后台)→ :${ELEVENTY_PORT}..."
|
||
cd "$WEB_DIR"
|
||
npx eleventy --serve --port=$ELEVENTY_PORT >>"$ELEVENTY_LOG" 2>&1 &
|
||
echo "$!" > "$ELEVENTY_PID_FILE"
|
||
cd "$ROOT"
|
||
|
||
# 启动代理(后台)
|
||
info "启动代理(后台)→ http://localhost:${PROXY_PORT}..."
|
||
PROXY_PORT=$PROXY_PORT ELEVENTY_PORT=$ELEVENTY_PORT FLUTTER_PORT=$FLUTTER_WEB_PORT \
|
||
node "$ROOT/scripts/dev-proxy.js" >>"$LOG_DIR/proxy.log" 2>&1 &
|
||
echo "$!" > "$PROXY_PID_FILE"
|
||
sleep 1
|
||
|
||
echo ""
|
||
success "代理已就绪:http://localhost:${PROXY_PORT}(宣传站)/ http://localhost:${PROXY_PORT}/app/(Flutter)"
|
||
echo -e " ${CYAN}Eleventy 日志:tail -f $ELEVENTY_LOG${NC}"
|
||
echo ""
|
||
info "在前台启动 Flutter Web(支持热重载,按 r 重载,按 q 退出)..."
|
||
echo ""
|
||
|
||
# Flutter 在前台运行,支持交互热重载
|
||
cd "$CLIENT_DIR"
|
||
flutter pub get
|
||
flutter run -d chrome --web-port "$FLUTTER_WEB_PORT" \
|
||
"--dart-define=BASE_URL=http://localhost:8080" \
|
||
"--dart-define=PUBLIC_URL=http://localhost:${PROXY_PORT}" \
|
||
"--dart-define=APP_VERSION=dev"
|
||
}
|
||
|
||
show_help() {
|
||
echo "用法: sh scripts/dev.sh <命令> [选项]"
|
||
echo ""
|
||
echo "启动命令:"
|
||
echo " run 启动前后端(macOS 桌面端)"
|
||
echo " run --force 强制重启后端"
|
||
echo " run --web 启动前后端(Chrome Web 端)"
|
||
echo " run --all 启动前后端(macOS + Web 同时)"
|
||
echo " --backend-only 仅启动后端"
|
||
echo " --frontend-only 仅启动前端(macOS)"
|
||
echo " --web-only 仅启动前端(Web)"
|
||
echo " marketing 宣传站 + Flutter Web + 代理(localhost:3000,/app/ 路由到 Flutter)"
|
||
echo ""
|
||
echo "数据库命令:"
|
||
echo " seed <shop_code> 写入指定门店测试数据(如 S001)"
|
||
echo " seed --clear <shop> 先清空再写入(单店)"
|
||
echo " reset 删表重建(AutoMigrate)"
|
||
echo " clear 清空所有业务数据"
|
||
echo ""
|
||
echo "其他:"
|
||
echo " stop 停止后端服务"
|
||
echo " help / --help / -h 显示此帮助"
|
||
}
|
||
|
||
case "$COMMAND" in
|
||
marketing)
|
||
;; # 在底部所有函数定义完后执行
|
||
run)
|
||
case "${2:-}" in
|
||
--force) FORCE=true ;;
|
||
--web) RUN_FRONTEND=false; RUN_WEB=true ;;
|
||
--all) RUN_WEB=true ;;
|
||
--help|-h|help)
|
||
show_help
|
||
exit 0
|
||
;;
|
||
"") ;;
|
||
*)
|
||
error "run: 未知选项 '${2}'"
|
||
echo "可用选项: --force | --web | --all"
|
||
exit 1
|
||
;;
|
||
esac
|
||
;;
|
||
seed)
|
||
cmd_seed "${2:-}" "${3:-}"
|
||
exit 0
|
||
;;
|
||
reset)
|
||
info "删表重建(AutoMigrate)..."
|
||
cd "$BACKEND_DIR"
|
||
go run cmd/seed/main.go --reset
|
||
exit 0
|
||
;;
|
||
clear)
|
||
info "清空所有业务数据..."
|
||
cd "$BACKEND_DIR"
|
||
go run cmd/seed/main.go --clear
|
||
exit 0
|
||
;;
|
||
--backend-only)
|
||
COMMAND=run
|
||
RUN_FRONTEND=false
|
||
[ "${2:-}" = "--force" ] && FORCE=true
|
||
;;
|
||
--frontend-only)
|
||
COMMAND=run
|
||
RUN_BACKEND=false
|
||
;;
|
||
--web-only)
|
||
COMMAND=run
|
||
RUN_BACKEND=false
|
||
RUN_FRONTEND=false
|
||
RUN_WEB=true
|
||
;;
|
||
stop)
|
||
if [ -f "$BACKEND_PID_FILE" ]; then
|
||
pid=$(cat "$BACKEND_PID_FILE")
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
rm -f "$BACKEND_PID_FILE"
|
||
fi
|
||
if [ -f "$WEB_PID_FILE" ]; then
|
||
pid=$(cat "$WEB_PID_FILE")
|
||
kill -9 "$pid" 2>/dev/null || true
|
||
rm -f "$WEB_PID_FILE"
|
||
fi
|
||
lsof -ti TCP:8080 -s TCP:LISTEN 2>/dev/null | xargs kill -9 2>/dev/null || true
|
||
sleep 0.5
|
||
if lsof -ti TCP:8080 -s TCP:LISTEN >/dev/null 2>&1; then
|
||
error "端口 8080 仍被占用,请手动处理"
|
||
else
|
||
success "后端已停止,端口 8080 已释放"
|
||
fi
|
||
exit 0
|
||
;;
|
||
help|--help|-h)
|
||
show_help
|
||
exit 0
|
||
;;
|
||
*)
|
||
error "未知命令: $COMMAND"
|
||
echo ""
|
||
show_help
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
# ── 清理函数 ──────────────────────────────────────────────
|
||
cleanup() {
|
||
echo ""
|
||
info "正在关闭服务..."
|
||
for pid_file in "$WEB_PID_FILE" "$ELEVENTY_PID_FILE" "$PROXY_PID_FILE"; do
|
||
if [ -f "$pid_file" ]; then
|
||
pid=$(cat "$pid_file")
|
||
kill "$pid" 2>/dev/null || true
|
||
rm -f "$pid_file"
|
||
fi
|
||
done
|
||
info "后端继续在后台运行。如需停止:sh scripts/dev.sh stop"
|
||
exit 0
|
||
}
|
||
trap cleanup INT TERM
|
||
|
||
# ── 依赖检查 ──────────────────────────────────────────────
|
||
check_deps() {
|
||
local missing=0
|
||
if [ "$RUN_BACKEND" = true ] && ! command -v go &>/dev/null; then
|
||
error "未找到 go,请确认 PATH 包含 Homebrew bin"
|
||
missing=1
|
||
fi
|
||
if [ "$RUN_FRONTEND" = true ] && ! command -v flutter &>/dev/null; then
|
||
error "未找到 flutter,请安装 Flutter SDK"
|
||
missing=1
|
||
fi
|
||
[ "$missing" -eq 1 ] && exit 1
|
||
return 0
|
||
}
|
||
|
||
# ── 计算后端源码 hash ──────────────────────────────────────
|
||
backend_hash() {
|
||
# 对所有 .go 文件 + go.mod 内容做 hash
|
||
find "$BACKEND_DIR" -name "*.go" -o -name "go.mod" -o -name "go.sum" \
|
||
| sort \
|
||
| xargs shasum 2>/dev/null \
|
||
| shasum \
|
||
| awk '{print $1}'
|
||
}
|
||
|
||
# ── 检查后端进程是否还活着 ───────────────────────────────
|
||
backend_running() {
|
||
if [ ! -f "$BACKEND_PID_FILE" ]; then return 1; fi
|
||
local pid
|
||
pid=$(cat "$BACKEND_PID_FILE")
|
||
kill -0 "$pid" 2>/dev/null
|
||
}
|
||
|
||
# ── 杀掉占用 8080 的进程 ──────────────────────────────────
|
||
kill_port_8080() {
|
||
local pids
|
||
pids=$(lsof -ti:8080 2>/dev/null || true)
|
||
if [ -n "$pids" ]; then
|
||
warn "端口 8080 被占用,正在清理..."
|
||
echo "$pids" | xargs kill -9 2>/dev/null || true
|
||
sleep 1
|
||
success "端口 8080 已释放"
|
||
fi
|
||
}
|
||
|
||
# ── 等待后端就绪 ──────────────────────────────────────────
|
||
wait_backend() {
|
||
info "等待后端启动..."
|
||
local i=0
|
||
while [ $i -lt 30 ]; do
|
||
if curl -s -o /dev/null http://localhost:8080/health 2>/dev/null; then
|
||
success "后端已就绪 → http://localhost:8080"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
i=$((i + 1))
|
||
done
|
||
warn "后端启动超时,请查看日志:tail -f $BACKEND_LOG"
|
||
}
|
||
|
||
# ── 启动后端 ──────────────────────────────────────────────
|
||
start_backend() {
|
||
local current_hash
|
||
current_hash=$(backend_hash)
|
||
local saved_hash=""
|
||
[ -f "$BACKEND_HASH_FILE" ] && saved_hash=$(cat "$BACKEND_HASH_FILE")
|
||
|
||
# 判断是否需要重启
|
||
if [ "$FORCE" = false ] && backend_running && [ "$current_hash" = "$saved_hash" ]; then
|
||
local pid
|
||
pid=$(cat "$BACKEND_PID_FILE")
|
||
success "后端无改动,跳过重启 (PID $pid) → http://localhost:8080"
|
||
return 0
|
||
fi
|
||
|
||
if backend_running; then
|
||
local pid
|
||
pid=$(cat "$BACKEND_PID_FILE")
|
||
warn "后端代码已变更,重启中 (旧 PID $pid)..."
|
||
kill "$pid" 2>/dev/null || true
|
||
sleep 1
|
||
fi
|
||
|
||
kill_port_8080
|
||
|
||
info "编译并启动后端 (Go)..."
|
||
cd "$BACKEND_DIR"
|
||
go run main.go >>"$BACKEND_LOG" 2>&1 &
|
||
local new_pid=$!
|
||
echo "$new_pid" > "$BACKEND_PID_FILE"
|
||
echo "$current_hash" > "$BACKEND_HASH_FILE"
|
||
info "后端进程 PID=$new_pid,日志:tail -f $BACKEND_LOG"
|
||
wait_backend
|
||
}
|
||
|
||
# ── 启动 Web 前端(后台) ─────────────────────────────────
|
||
start_web_bg() {
|
||
# 清理占用 8081 的旧进程
|
||
local old_pid
|
||
old_pid=$(lsof -ti:8081 2>/dev/null | head -1)
|
||
if [ -n "$old_pid" ]; then
|
||
kill -9 "$old_pid" 2>/dev/null || true
|
||
sleep 1
|
||
fi
|
||
info "启动 Flutter Web (Chrome,后台)..."
|
||
flutter run -d chrome --web-port 8081 >>"$WEB_LOG" 2>&1 &
|
||
local pid=$!
|
||
echo "$pid" > "$WEB_PID_FILE"
|
||
# 等待 Chrome 打开(最多 15 秒)
|
||
local i=0
|
||
while [ $i -lt 15 ]; do
|
||
if grep -q "To hot reload\|Launching lib/main.dart" "$WEB_LOG" 2>/dev/null; then
|
||
success "Web 已启动 → http://localhost:8081 (日志: tail -f $WEB_LOG)"
|
||
return 0
|
||
fi
|
||
sleep 1
|
||
i=$((i + 1))
|
||
done
|
||
warn "Web 启动较慢,继续等待... (日志: tail -f $WEB_LOG)"
|
||
}
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
if [ "$COMMAND" = "marketing" ]; then
|
||
cmd_marketing
|
||
exit 0
|
||
fi
|
||
|
||
check_deps
|
||
|
||
if [ "$RUN_BACKEND" = true ]; then
|
||
start_backend
|
||
fi
|
||
|
||
# --all 模式:先在后台启动 Web,再在前台启动 macOS
|
||
if [ "$RUN_WEB" = true ] && [ "$RUN_FRONTEND" = true ]; then
|
||
cd "$CLIENT_DIR" && flutter pub get
|
||
start_web_bg
|
||
echo ""
|
||
success "Flutter macOS 启动中..."
|
||
echo -e "${CYAN}提示:macOS 窗口 + Chrome 同时运行,Ctrl+C 退出时 Web 同步停止${NC}"
|
||
echo ""
|
||
flutter run -d macos
|
||
|
||
elif [ "$RUN_WEB" = true ]; then
|
||
cd "$CLIENT_DIR" && flutter pub get
|
||
# 清理占用 8081 的旧进程
|
||
_old=$(lsof -ti:8081 2>/dev/null | head -1)
|
||
[ -n "$_old" ] && kill -9 "$_old" 2>/dev/null && sleep 1
|
||
echo ""
|
||
success "Chrome 启动中,稍候自动打开..."
|
||
echo -e "${CYAN}提示:按 r 热重载,按 q 退出,后端继续在后台运行${NC}"
|
||
echo ""
|
||
flutter run -d chrome --web-port 8081
|
||
|
||
elif [ "$RUN_FRONTEND" = true ]; then
|
||
cd "$CLIENT_DIR" && flutter pub get
|
||
echo ""
|
||
success "Flutter macOS 启动中,窗口将自动弹出..."
|
||
echo -e "${CYAN}提示:按 q 退出前端,后端继续运行${NC}"
|
||
echo -e "${CYAN} 再次执行 sh scripts/dev.sh run 可复用已有后端${NC}"
|
||
echo ""
|
||
flutter run -d macos
|
||
|
||
else
|
||
echo ""
|
||
success "后端运行中,按 Ctrl+C 退出此脚本(后端仍在后台)"
|
||
echo -e " 查看日志:tail -f $BACKEND_LOG"
|
||
wait "$(cat "$BACKEND_PID_FILE")" 2>/dev/null || true
|
||
fi
|