480ce836bb
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-windows (push) Successful in 1m48s
Deploy / build-macos (push) Successful in 1m17s
Deploy / build-android (push) Successful in 4m13s
Deploy / build-ios (push) Successful in 9s
Deploy / release-deploy (push) Successful in 1m37s
移动端响应式适配(抽屉导航/列表卡片/弹窗自适应)、Android 正式签名与 APK 发布、 iOS(TestFlight) 工程与 CI、多平台构建流水线、相关文档同步。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7.9 KiB
7.9 KiB
开发环境配置指南
适用平台:macOS(Apple Silicon / Intel)
一、必备工具安装
1. Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
安装后确保 PATH 生效(Apple Silicon 路径为 /opt/homebrew/bin):
echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
2. Go(后端)
brew install go
go version # 期望:go1.26.x
3. Flutter(前端)
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(数据库容器)
brew install --cask docker
# 安装后打开 Docker Desktop 完成初始化
二、项目初始化
# 后端依赖
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 命令工作原理:
- 从
backend/config/config.yaml解析数据库连接信息 - 检查 MySQL 容器(
jiu_mysql)是否运行 - 通过
docker exec将backend/seeds/<shop>.sql注入容器内的 mysql 执行 - SQL 文件顶部包含 TRUNCATE,每次执行完整重建数据
新增门店测试数据
参照 backend/seeds/S001.sql 创建新文件 backend/seeds/S002.sql,然后执行:
sh scripts/dev.sh seed S002
四、完整启动流程
# 第一步:启动数据库容器
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 |
退出 |
仅开发后端时
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 模式运行 |
移动端构建
# 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)
// 初始化测试配置(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)
// 创建带 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)
典型测试写法
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))
}
运行测试
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):
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)。