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

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