Files
wangjia 75e3b934bc
Deploy / build-windows (push) Failing after 9s
Deploy / build-linux-web (push) Successful in 51s
Deploy / release-deploy (push) Has been skipped
chore: release v1.0.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:39:04 +08:00

487 lines
16 KiB
Bash
Executable File
Raw Permalink 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
# 如果用 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