Files
jiu/scripts/dev.sh
T
wangjia 393e227de5 feat: 商品详情页、XLS导入修复、分页选择器、导出功能
后端:
- 新增 product_images 表,支持每商品最多5张图(服务端压缩至1200px/JPEG85%)
- products 表新增 public_id(UUID)、description 字段
- 新增商品详情接口、二维码接口、公开商品接口(无鉴权)
- 修复 XLS 导入:OLE2 magic bytes 检测 + 临时文件解析,兼容 extrame/xls
- 修复商品/名称/系列/规格三张表导入数据为0(LastCol()=0 bug)
- 所有导入接口返回 total/imported/skipped 统计
- config 新增 StorageConfig,支持 STORAGE_* 环境变量覆盖
- 种子数据修复:products 补 public_id、新增 product_images TRUNCATE、schema.sql 表名修正

前端:
- 商品详情页:图片上传/删除、描述内联编辑、二维码弹窗、公开链接复制
- 公开商品页:无鉴权路由 /product/:public_id,Flutter Web SPA
- 商品详情列表(批次追踪)商品名超链接跳转详情页
- 导航「商品管理」改名「商品详情」
- 所有列表表格新增每页条数选择(10/20/50/100)
- 表格列头内嵌筛选(FilterableColumnHeader)
- 导出 Excel 功能(入库/出库/库存/财务/批次/往来单位)
- 网络恢复自动刷新 + 离线缓存展示

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

332 lines
11 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 # 启动前后端(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"
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 # macOS
RUN_WEB=false
case "$COMMAND" in
run)
case "${2:-}" in
--force) FORCE=true ;;
--web) RUN_FRONTEND=false; RUN_WEB=true ;;
--all) RUN_WEB=true ;;
esac
;;
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
;;
--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
;;
*)
error "未知命令: $COMMAND"
echo "用法: sh scripts/dev.sh [run [--force|--web|--all] | seed <shop> | reset | clear | stop | --backend-only | --frontend-only | --web-only]"
exit 1
;;
esac
# ── 清理函数 ──────────────────────────────────────────────
cleanup() {
echo ""
info "正在关闭服务..."
if [ -f "$WEB_PID_FILE" ]; then
pid=$(cat "$WEB_PID_FILE")
kill "$pid" 2>/dev/null || true
rm -f "$WEB_PID_FILE"
info "Web 服务已停止"
fi
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/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
}
# ── 启动 Web 前端(后台) ─────────────────────────────────
start_web_bg() {
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)"
}
# ════════════════════════════════════════════════════════
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
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