docs: 更新项目文档与开发规范
修正 hotel_id→shop_id 错误引用,补充 dev.sh 脚本用法、 测试架构说明、种子数据规范,整理目录结构为当前实际状态。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+187
-145
@@ -6,7 +6,7 @@
|
||||
|
||||
## 一、必备工具安装
|
||||
|
||||
### 1. Homebrew(包管理器)
|
||||
### 1. Homebrew
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
@@ -19,56 +19,22 @@ echo 'export PATH="/opt/homebrew/bin:$PATH"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Go(后端语言)
|
||||
### 2. Go(后端)
|
||||
|
||||
```bash
|
||||
brew install go
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
go version # 期望:go1.26.x
|
||||
```
|
||||
|
||||
**注意**:每次运行 Go 命令前须确保 PATH 包含 Homebrew 路径:
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
go build ./...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Flutter(前端框架)
|
||||
### 3. Flutter(前端)
|
||||
|
||||
```bash
|
||||
brew install --cask flutter
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
flutter --version # 期望:Flutter 3.41.x
|
||||
```
|
||||
|
||||
#### 3.1 安装 CocoaPods(macOS 桌面版必须)
|
||||
|
||||
```bash
|
||||
brew install cocoapods
|
||||
```
|
||||
|
||||
#### 3.2 验证全部环境
|
||||
|
||||
```bash
|
||||
flutter doctor
|
||||
brew install cocoapods # macOS 桌面版必须
|
||||
flutter doctor # 验证环境
|
||||
```
|
||||
|
||||
期望输出(Android 可以有警告,不影响 macOS/Web 开发):
|
||||
|
||||
```
|
||||
[✓] Flutter
|
||||
[✓] Xcode - develop for iOS and macOS
|
||||
@@ -76,146 +42,222 @@ flutter doctor
|
||||
[✓] Connected device
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Docker(本地数据库)
|
||||
### 4. Docker(数据库容器)
|
||||
|
||||
```bash
|
||||
brew install --cask docker
|
||||
```
|
||||
|
||||
安装后打开 Docker Desktop 应用完成初始化。
|
||||
|
||||
---
|
||||
|
||||
### 5. 其他工具(可选)
|
||||
|
||||
```bash
|
||||
brew install git # 版本管理
|
||||
brew install --cask vscode # 代码编辑器(推荐安装 Go 和 Flutter 插件)
|
||||
# 安装后打开 Docker Desktop 完成初始化
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、项目初始化
|
||||
|
||||
### 克隆 / 进入项目
|
||||
|
||||
```bash
|
||||
cd /Users/wangjia/code/jiu
|
||||
```
|
||||
# 后端依赖
|
||||
cd backend && go mod download
|
||||
|
||||
### 后端依赖
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
go mod download
|
||||
```
|
||||
|
||||
### 前端依赖
|
||||
|
||||
```bash
|
||||
cd client
|
||||
flutter pub get
|
||||
# 前端依赖
|
||||
cd client && flutter pub get
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、本地开发启动
|
||||
## 三、开发脚本(scripts/dev.sh)
|
||||
|
||||
### 1. 启动数据库
|
||||
所有日常操作统一通过 `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 H001` | 清空并写入 H001 测试数据 |
|
||||
| `sh scripts/dev.sh reset` | 删表重建(AutoMigrate) |
|
||||
| `sh scripts/dev.sh clear` | 清空所有业务数据(保留表结构) |
|
||||
|
||||
`seed` 命令工作原理:
|
||||
1. 从 `backend/config/config.yaml` 解析数据库连接信息
|
||||
2. 检查 MySQL 容器(`jiu_mysql`)是否运行
|
||||
3. 通过 `docker exec` 将 `backend/seeds/<shop>.sql` 注入容器内的 mysql 执行
|
||||
4. SQL 文件顶部包含 TRUNCATE,每次执行完整重建数据
|
||||
|
||||
### 新增门店测试数据
|
||||
|
||||
参照 `backend/seeds/H001.sql` 创建新文件 `backend/seeds/H002.sql`,然后执行:
|
||||
|
||||
```bash
|
||||
cd deploy
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
- MySQL 端口:`3306`
|
||||
- Adminer(数据库管理界面):http://localhost:8888
|
||||
- 服务器:`mysql`,用户:`root`,密码:`password`,数据库:`jiu_db`
|
||||
|
||||
### 2. 启动后端
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
go run main.go
|
||||
```
|
||||
|
||||
后端启动后监听 `http://localhost:8080`
|
||||
|
||||
### 3. 启动 Flutter 前端
|
||||
|
||||
#### Web(推荐开发时使用,稳定)
|
||||
|
||||
```bash
|
||||
cd client
|
||||
flutter run -d chrome
|
||||
```
|
||||
|
||||
> macOS 26(beta)下 debug 模式可能有 Flutter 框架断言 bug,加 `--profile` 规避:
|
||||
> ```bash
|
||||
> flutter run -d chrome --profile
|
||||
> ```
|
||||
|
||||
#### macOS 桌面版
|
||||
|
||||
```bash
|
||||
cd client
|
||||
flutter run -d macos --no-enable-impeller
|
||||
```
|
||||
|
||||
> `--no-enable-impeller`:绕过 macOS 26 上 Impeller 着色器编译 OOM 问题
|
||||
|
||||
---
|
||||
|
||||
## 四、运行测试
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
go test ./... -cover
|
||||
sh scripts/dev.sh seed H002
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、已知问题(macOS 26 beta 环境)
|
||||
## 四、完整启动流程
|
||||
|
||||
```bash
|
||||
# 第一步:启动数据库容器
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
|
||||
# 第二步:启动前后端
|
||||
sh scripts/dev.sh run
|
||||
# 后端: http://localhost:8080
|
||||
# Flutter 窗口自动弹出
|
||||
|
||||
# 第三步(另开终端):写入测试数据
|
||||
sh scripts/dev.sh seed H001
|
||||
|
||||
# 数据库管理界面(可选)
|
||||
# http://localhost:8888 服务器:mysql 用户:root 密码:password 库:jiu_db
|
||||
```
|
||||
|
||||
### 仅开发后端时
|
||||
|
||||
```bash
|
||||
sh scripts/dev.sh --backend-only
|
||||
# 后端日志: tail -f scripts/.logs/backend.log
|
||||
```
|
||||
|
||||
### macOS 桌面端已知问题
|
||||
|
||||
| 问题 | 原因 | 解决方式 |
|
||||
|------|------|---------|
|
||||
| Impeller 着色器编译 exit code -9 | macOS 26 内存压力 + OOM killer | `flutter run --no-enable-impeller` |
|
||||
| KeyDownEvent 断言失败(桌面版) | Flutter 3.41 + macOS 26 已知 bug | 改用 Chrome 运行,或等 Flutter 修复 |
|
||||
| mouse_tracker 断言失败(debug) | Flutter debug 断言 + hover 事件 | 用 `--profile` 模式运行 |
|
||||
| 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` 模式运行 |
|
||||
|
||||
---
|
||||
|
||||
## 六、环境变量说明
|
||||
## 五、后端测试架构
|
||||
|
||||
敏感配置放在 `~/.env`,通过 `.zshrc` 自动加载(无需手动 source)。
|
||||
### 测试分层
|
||||
|
||||
后端本地开发配置在 `backend/config/config.yaml`(已被 `.gitignore` 排除):
|
||||
```
|
||||
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(localhost:3306)/jiu_db?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
dsn: "root:password@tcp(127.0.0.1:3306)/jiu_db?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
|
||||
jwt:
|
||||
secret: "dev-secret-change-in-prod"
|
||||
secret: "change-this-to-a-random-secret-in-production"
|
||||
access_expire_min: 60
|
||||
refresh_expire_h: 168
|
||||
|
||||
license:
|
||||
hmac_secret: "dev-hmac-secret-change-in-prod"
|
||||
hmac_secret: "change-this-license-secret-in-production"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、flutter doctor 完整检查(参考输出)
|
||||
|
||||
```
|
||||
[✓] Flutter (Channel stable, 3.41.6, on macOS 26.x darwin-arm64)
|
||||
[!] Android toolchain ← 不影响 macOS/Web 开发,可忽略
|
||||
[✓] Xcode - develop for iOS and macOS (Xcode 26.x)
|
||||
[✓] Chrome - develop for the web
|
||||
[✓] Connected device (2 available)
|
||||
• macOS (desktop)
|
||||
• Chrome (web)
|
||||
[✓] Network resources
|
||||
```
|
||||
**注意**:`config.go` 中的结构体字段必须有 `mapstructure` 标签,否则 Viper 无法正确映射 snake_case YAML 键到 PascalCase Go 字段(例如 `access_expire_min` 无法映射到 `AccessExpireMin`)。
|
||||
|
||||
Reference in New Issue
Block a user