Files
jiu/scripts/dev.sh
T
wangjia bb4f17cf7a feat: 自动更新、系统设置、安全修复
后端:
- 新增 GET /version 版本检查端点(version.go + version.yaml)
- 新增 GET /license/info 接口,返回门店授权信息
- 修复 GenerateOrderNo 并发重复单号:事务内加 FOR UPDATE 行锁
- 修复 ApproveStockOut 超卖竞态:预检和库存更新均加 FOR UPDATE
- 修复 Product Create 并发 code 冲突:加重试逻辑,schema 加 UNIQUE KEY
- 修复 Product Update 全字段覆盖:改用 selective Updates()
- 挂载 ReadOnly 中间件(全局)+ AdminOnly(用户管理路由)
- version.go 配置缺失时返回 500 而非静默降级

前端:
- 新增自动更新检测(update_provider.dart)+ shell 更新 banner/弹窗
- 新增系统设置"关于"标签页:版本、授权、开发信息、意见反馈
- 新增离线缓存:所有 AsyncNotifierProvider 支持断网浏览历史数据
- 新增门店信息弹窗(点击左上角 logo 或右上角门店号触发)
- 提取 AppConfig 统一管理 BASE_URL,支持 --dart-define 注入
- update_provider.dart 加 kIsWeb 保护,修复 Web 平台崩溃
- dev.sh 新增 stop 命令,修复 stop 误杀前端进程问题

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 00:28:08 +08:00

272 lines
9.0 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
# 如果用 sh 调用,自动切换到 bash
if [ -z "$BASH_VERSION" ]; then
exec bash "$0" "$@"
fi
# dev.sh — 本地开发环境管理
#
# 启动:
# sh scripts/dev.sh run # 启动前后端(后端无改动则跳过重启)
# sh scripts/dev.sh run --force # 强制重启后端
# sh scripts/dev.sh --backend-only # 仅启动后端
# sh scripts/dev.sh --frontend-only # 仅启动前端
#
# 数据库:
# 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"
mkdir -p "$LOG_DIR"
# ── seed 命令 ────────────────────────────────────────────────
# 用法: cmd_seed <shop_code>
# 从 backend/config/config.yaml 读取 DSN,执行 backend/seeds/<shop>.sql
cmd_seed() {
local shop="${1:-}"
if [ -z "$shop" ]; then
error "用法: sh scripts/dev.sh seed <shop_code> 示例: sh scripts/dev.sh seed H001"
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
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
case "$COMMAND" in
run)
[ "${2:-}" = "--force" ] && FORCE=true
;;
seed)
cmd_seed "${2:-}"
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
;;
stop)
# 优先用 PID 文件
if [ -f "$BACKEND_PID_FILE" ]; then
pid=$(cat "$BACKEND_PID_FILE")
kill -9 "$pid" 2>/dev/null || true
rm -f "$BACKEND_PID_FILE"
fi
# 无论如何,再用 lsof 兜底清理端口(只杀监听 8080 的进程,不杀连接客户端)
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
;;
*)
error "未知命令: $COMMAND"
echo "用法: sh scripts/dev.sh [run [--force] | seed <shop> | reset | clear | stop | --backend-only | --frontend-only]"
exit 1
;;
esac
# ── 清理函数 ──────────────────────────────────────────────
cleanup() {
echo ""
info "正在关闭服务..."
# 不杀后端:run 模式下后端是持久进程,Ctrl+C 只退出 flutter
info "后端继续在后台运行。如需停止:bash 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/api/v1/auth/login 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
}
# ════════════════════════════════════════════════════════
check_deps
if [ "$RUN_BACKEND" = true ]; then
start_backend
fi
if [ "$RUN_FRONTEND" = true ]; then
info "启动 Flutter (macOS)..."
cd "$CLIENT_DIR"
flutter pub get
echo ""
success "Flutter 启动中,窗口将自动弹出..."
echo -e "${CYAN}提示:关闭 Flutter 窗口或按 q 退出前端,后端继续运行${NC}"
echo -e "${CYAN} 再次执行 bash 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