Files
jiu/docs/dev-setup.md
wangjia 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
chore: release v1.0.18
移动端响应式适配(抽屉导航/列表卡片/弹窗自适应)、Android 正式签名与 APK 发布、
iOS(TestFlight) 工程与 CI、多平台构建流水线、相关文档同步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:55:33 +08:00

7.9 KiB
Raw Permalink Blame History

开发环境配置指南

适用平台:macOSApple 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 命令工作原理:

  1. backend/config/config.yaml 解析数据库连接信息
  2. 检查 MySQL 容器(jiu_mysql)是否运行
  3. 通过 docker execbackend/seeds/<shop>.sql 注入容器内的 mysql 执行
  4. 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.mdiOSTestFlight)见 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 的数据
  • 库存变更测试需验证 inventoriesinventory_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)。