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>
296 lines
7.9 KiB
Markdown
296 lines
7.9 KiB
Markdown
# 开发环境配置指南
|
||
|
||
> 适用平台: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/<shop>.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`)。
|