# 开发环境配置指南 > 适用平台:macOS(Apple Silicon / Intel) --- ## 一、必备工具安装 ### 1. Homebrew ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` 安装后确保 PATH 生效(Apple Silicon 路径为 `/opt/homebrew/bin`): ```bash echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc source ~/.zshrc ``` ### 2. Go(后端) ```bash brew install go go version # 期望:go1.26.x ``` ### 3. Flutter(前端) ```bash brew install --cask flutter brew install cocoapods # macOS 桌面版必须 flutter doctor # 验证环境 ``` 期望输出(Android 可以有警告,不影响 macOS/Web 开发): ``` [✓] Flutter [✓] Xcode - develop for iOS and macOS [✓] Chrome - develop for the web [✓] Connected device ``` ### 4. Docker(数据库容器) ```bash brew install --cask docker # 安装后打开 Docker Desktop 完成初始化 ``` --- ## 二、项目初始化 ```bash # 后端依赖 cd backend && go mod download # 前端依赖 cd client && flutter pub get ``` --- ## 三、开发脚本(scripts/dev.sh) 所有日常操作统一通过 `sh scripts/dev.sh <命令>` 执行,脚本内部已处理 PATH 和容器交互。 ### 启动命令 | 命令 | 说明 | |------|------| | `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 S001` | 清空并写入 S001 测试数据 | | `sh scripts/dev.sh reset` | 删表重建(AutoMigrate) | | `sh scripts/dev.sh clear` | 清空所有业务数据(保留表结构) | 执行完成后会打印测试账号: ``` 门店编号: S001 admin / password123 (管理员,可审核单据) operator / password123 (操作员,可创建/提交单据) test / password123 (只读,仅查看) ``` `seed` 命令工作原理: 1. 从 `backend/config/config.yaml` 解析数据库连接信息 2. 检查 MySQL 容器(`jiu_mysql`)是否运行 3. 通过 `docker exec` 将 `backend/seeds/.sql` 注入容器内的 mysql 执行 4. SQL 文件顶部包含 TRUNCATE,每次执行完整重建数据 ### 新增门店测试数据 参照 `backend/seeds/S001.sql` 创建新文件 `backend/seeds/S002.sql`,然后执行: ```bash sh scripts/dev.sh seed S002 ``` --- ## 四、完整启动流程 ```bash # 第一步:启动数据库容器 docker compose -f deploy/docker-compose.yml up -d # 第二步:启动前后端 sh scripts/dev.sh run # 后端: http://localhost:8080 # Flutter 窗口自动弹出 # 第三步(另开终端):写入测试数据 sh scripts/dev.sh seed S001 # 数据库管理界面(可选) # http://localhost:8888 服务器:mysql 用户:root 密码:password 库:jiu_db ``` ### Flutter 热重载 在运行 `flutter run` 的终端中: | 按键 | 效果 | |------|------| | `r` | 热重载(Hot Reload)— 保留状态,秒级生效,适合 UI 调整 | | `R` | 热重启(Hot Restart)— 重置状态,完整重建,适合逻辑变更 | | `q` | 退出 | ### 仅开发后端时 ```bash sh scripts/dev.sh --backend-only # 后端日志: tail -f scripts/.logs/backend.log ``` ### macOS 桌面端已知问题 | 问题 | 原因 | 解决方式 | |------|------|---------| | Impeller 着色器编译 OOM | macOS 26 内存压力 | `flutter run -d macos --no-enable-impeller` | | KeyDownEvent 断言失败 | Flutter 3.41 + macOS 26 已知 bug | 改用 Chrome 运行 | | mouse_tracker 断言失败(debug)| Flutter debug 断言 + hover 事件 | 用 `--profile` 模式运行 | ### 移动端构建 ```bash # Android(本地):无 client/android/key.properties 时自动回退 debug 签名 cd client && flutter build apk --release # iOS(本地验证编译,不签名) cd client && flutter build ios --release --no-codesign ``` - 正式签名/发布由 CI 完成:Android 见 `docs/android-signing.md`,iOS(TestFlight)见 `docs/ios-signing.md`。 - 移动端 UI 自动适配:窄屏走移动布局(Drawer 抽屉 + 列表卡片),规范见 `docs/context/project.md` 的「移动端 / 响应式 UI 规范」。 --- ## 五、后端测试架构 ### 测试分层 ``` backend/ ├── testutil/setup.go # 共享测试基础设施 ├── internal/service/*_test.go # 服务层单元测试 └── internal/handler/*_test.go # Handler 集成测试 ``` ### 测试数据库:SQLite In-Memory 所有测试使用 SQLite 内存数据库,**不依赖 MySQL 容器**,可在任何环境直接运行。 `testutil.SetupTestDB()` 负责创建 SQLite 库并建表(手写 DDL 替代 GORM AutoMigrate,因 SQLite 不支持 ENUM)。 ### testutil 工具包(`backend/testutil/setup.go`) ```go // 初始化测试配置(JWT secret 等) testutil.InitConfig() // 创建 SQLite in-memory 测试库(含全部建表) db := testutil.SetupTestDB() // 创建测试数据 shop := testutil.CreateTestShop(db, "TEST001") user := testutil.CreateTestUser(db, shop.ID, "admin", "password123", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "主仓库") product := testutil.CreateTestProduct(db, shop.ID, "飞天茅台") // 生成带 shop_id/user_id/role 的测试 JWT token token := testutil.GetAuthToken(user.ID, shop.ID, "admin") ``` ### Handler 测试辅助函数(`internal/handler/testhelper_test.go`) ```go // 创建带 JWT 中间件的完整测试路由(包含所有 handler) r := setupProtectedRouter(db) // 发起带认证的 HTTP 请求 w := makeRequest(r, "POST", "/api/v1/products", token, body) // 从响应提取 ID id := extractID(w) // 构建请求 body body := jsonBody("name", "飞天茅台", "unit", "瓶", "warehouse_id", wh.ID) ``` ### 典型测试写法 ```go func TestStockIn_Create(t *testing.T) { db := testutil.SetupTestDB() shop := testutil.CreateTestShop(db, "SHOP001") user := testutil.CreateTestUser(db, shop.ID, "admin", "pw", "admin") wh := testutil.CreateTestWarehouse(db, shop.ID, "主仓库") product := testutil.CreateTestProduct(db, shop.ID, "茅台") token := testutil.GetAuthToken(user.ID, shop.ID, "admin") r := setupProtectedRouter(db) w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, jsonBody( "warehouse_id", wh.ID, "order_date", "2026-04-07", "items", []map[string]any{ {"product_id": product.ID, "quantity": 10, "unit_price": 2350}, }, )) assert.Equal(t, 200, w.Code) assert.NotZero(t, extractID(w)) } ``` ### 运行测试 ```bash cd backend export PATH="/opt/homebrew/bin:$PATH" # 运行全部测试 go test ./... # 带覆盖率 go test ./... -cover # 运行单个包 go test ./internal/handler/ -v # 运行单个测试 go test ./internal/handler/ -run TestStockIn_Approve -v ``` ### 测试规范 - **每个 handler 方法**对应至少一个正向测试 + 一个异常测试(无权限 / 参数错误 / 业务规则违反) - **多租户隔离**必须有专项测试:验证 shop A 无法访问 shop B 的数据 - **库存变更**测试需验证 `inventories` 和 `inventory_logs` 同时更新 - 测试文件与被测文件同包,命名 `xxx_test.go`,放在同一目录 --- ## 六、配置文件 `backend/config/config.yaml`(本地开发,不提交 git): ```yaml server: port: "8080" mode: "debug" database: dsn: "root:password@tcp(127.0.0.1:3306)/jiu_db?charset=utf8mb4&parseTime=True&loc=Local" jwt: secret: "change-this-to-a-random-secret-in-production" access_expire_min: 60 refresh_expire_h: 168 license: hmac_secret: "change-this-license-secret-in-production" ``` **注意**:`config.go` 中的结构体字段必须有 `mapstructure` 标签,否则 Viper 无法正确映射 snake_case YAML 键到 PascalCase Go 字段(例如 `access_expire_min` 无法映射到 `AccessExpireMin`)。