docs: 更新项目文档与开发规范

修正 hotel_id→shop_id 错误引用,补充 dev.sh 脚本用法、
测试架构说明、种子数据规范,整理目录结构为当前实际状态。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-07 23:07:16 +08:00
parent 39fe1da800
commit 098fba38f2
3 changed files with 364 additions and 282 deletions
+30 -13
View File
@@ -1,4 +1,4 @@
# 酒店仓库管理系统 — 项目规则与 Orchestrator
# 酒库管理系统 — 项目规则与 Orchestrator
## 必读
@@ -24,9 +24,6 @@
并行:backend-coder + flutter-coder(都依赖 API 设计,互不依赖)
串行:test-engineer → linter → code-reviewer → security-auditor
→ 上线前:/design-review 验收还原度
→ 上线后:/archive-design 存档最终设计
```
### 仅后端修改
@@ -89,15 +86,15 @@ doc-writer
| requirements-analyst | docs/requirements/ | 任何代码 |
| architect | docs/architecture/ | 任何代码 |
| api-designer | docs/api/ | 任何代码 |
| db-designer | backend/migrations/, backend/schema/ | 业务代码、测试代码 |
| backend-coder | backend/internal/, backend/main.go, backend/internal/router/ | *_test.go, migrations/ |
| ui-designer | docs/design/(非 archive/ | 任何代码 |
| db-designer | backend/schema/ | 业务代码、测试代码 |
| backend-coder | backend/internal/, backend/main.go | *_test.go, schema/ |
| ui-designer | docs/design/ | 任何代码 |
| flutter-coder | client/ | backend/ |
| test-engineer | *_test.go, docs/review/*-bugs.md | 业务代码 |
| linter | 格式自动修复(gofmt/dart format),docs/review/lint-report.md | 逻辑代码 |
| code-reviewer | docs/review/*-review.md | 任何代码 |
| security-auditor | docs/security/ | 任何代码 |
| devops | deploy/, .github/workflows/, backend/Dockerfile | 业务代码 |
| devops | deploy/, .github/workflows/ | 业务代码 |
| sre | docs/runbooks/ | 任何代码 |
| doc-writer | docs/(除 context/ 外) | 任何代码 |
@@ -163,9 +160,29 @@ security: 修复入库单多租户隔离漏洞
---
## 项目特殊规则
## 项目强制规则
- `hotel_id` 永远从 JWT token 中提取(`middleware.GetHotelID(c)`),绝不从请求参数读取
- 所有库存变更操作必须在数据库事务中执行,并同时写 `inventory_logs`
- `custom_fields` JSON 列用于动态扩展,不为每个新业务字段修改表结构
- Go 命令需要 `export PATH="/opt/homebrew/bin:$PATH"`Go 安装在 Homebrew
### 多租户隔离
- `shop_id` **永远**从 JWT token 中提取,使用 `middleware.GetShopID(c)`
- **绝不**从请求参数、URL 或请求体中读取 `shop_id`
- 所有数据库查询必须带 `WHERE shop_id = ?` 条件
### 库存变更
- 入库/出库审核通过时,必须在同一事务中同时完成:
1. 更新 `inventories` 表数量
2. 写入 `inventory_logs` 流水记录
- 出库前必须校验库存充足,不足时返回错误并回滚
### Schema 管理
- 表结构变更:修改 `backend/schema/schema.sql` + `backend/internal/model/` 对应 model
- 启动时自动 AutoMigrate,无单独迁移文件
- **禁止**为每个新业务字段修改表结构,使用 `custom_fields JSON` 动态扩展
### 种子数据
- 测试数据以 SQL 文件形式维护:`backend/seeds/<shop_code>.sql`
- 通过 `sh scripts/dev.sh seed <shop_code>` 执行(内部用 `docker exec` 注入 MySQL 容器)
- 每个门店一个文件,SQL 文件顶部包含 TRUNCATE,每次执行完整重建
### Go 命令
- 所有 Go 命令前须确保 PATH`export PATH="/opt/homebrew/bin:$PATH"`
- 或直接使用 `sh scripts/dev.sh` 脚本(已内置 PATH
+147 -124
View File
@@ -1,11 +1,11 @@
# 酒店仓库管理系统 — 项目上下文
# 酒库管理系统 — 项目上下文
> 所有 Agent 在开始工作前必须读取此文件,了解项目全貌。
## 项目简介
面向酒水门店的仓库管理系统。核心特点:
- **多租户**个账号对应一个门店,数据完全隔离(通过 `shop_id` 字段)
- **多租户**个账号对应一个门店,数据通过 `shop_id` 完全隔离
- **付费授权**:许可证绑定设备 ID,支持试用/年付/买断
- **跨端客户端**:Flutter 单一代码库,支持 Windows / Web / macOS / iOS / Android
@@ -15,21 +15,21 @@
后端:
语言: Go 1.26
框架: GinHTTP+ GORMORM
数据库: MySQL 8.0
数据库: MySQL 8.0Docker 容器 jiu_mysql
认证: JWTHS256),Access Token 60分钟,Refresh Token 7天
模块路径: github.com/wangjia/jiu/backend
前端:
框架: Flutter 3.x
状态管理: Riverpod
HTTP客户端: Dio
路由: go_router
HTTP客户端: Dio(含 401 自动刷新 token 拦截器)
路由: go_router(含登录重定向)
目录: client/
数据库迁移:
工具: golang-migrate
迁移文件: backend/migrations/
完整schema: backend/schema/schema.sql
数据库管理:
Schema: backend/schema/schema.sql(完整建表 SQL,容器启动时自动执行)
ORM 迁移: GORM AutoMigrate(后端启动时自动同步)
种子数据: backend/seeds/<shop>.sql(通过 dev.sh seed 命令执行)
```
## 项目目录结构
@@ -37,162 +37,185 @@
```
jiu/
├── backend/
│ ├── main.go # 入口
│ ├── config/config.yaml # 配置(本地开发)
│ ├── main.go # 入口,启动时执行 AutoMigrate
│ ├── config/
│ │ ├── config.go # 配置结构体(含 mapstructure 标签)
│ │ └── config.yaml # 本地开发配置(不提交 git)
│ ├── internal/
│ │ ├── handler/ # HTTP 处理器(每模块一文件)
│ │ ├── service/ # 业务逻辑层
│ │ ├── model/ # GORM 数据模型
│ │ ├── middleware/auth.go # JWT 验证 + 租户注入
│ │ ── router/router.go # 路由注册
├── migrations/ # 版本迁移 SQL
└── schema/schema.sql # 完整建表 SQL
├── client/ # Flutter 跨端客户端
│ │ ├── handler/ # HTTP 处理器(每模块一文件)
│ │ │ └── *_test.go # Handler 集成测试(SQLite in-memory
│ │ ├── service/ # 业务逻辑层
│ │ │ └── *_test.go # Service 单元测试
│ │ ── model/ # GORM 数据模型
│ │ ├── base.go # Base/TenantBase/Date/JSON 公共类型
│ │ └── *.go # 各业务模型
│ │ ├── middleware/auth.go # JWT 验证 + shop_id 注入(GetShopID/GetUserID
│ │ └── router/router.go # 路由注册
│ ├── testutil/setup.go # 测试工具包(SQLite DB、CreateTestXxx、GetAuthToken
│ ├── cmd/seed/main.go # 数据库工具(--reset 删表重建,--clear 清空数据)
│ ├── seeds/
│ │ └── H001.sql # 门店 H001 测试种子数据(SQL 命令形式)
│ └── schema/schema.sql # 完整建表 SQLMySQL
├── client/ # Flutter 跨端客户端
├── deploy/
│ └── docker-compose.yml # 本地开发:MySQL + Adminer
│ └── docker-compose.yml # 本地开发:MySQL(3306) + Adminer(8888)
├── scripts/
│ └── dev.sh # 一站式开发脚本(见下方用法)
└── docs/
├── context/project.md # 本文件
├── requirements/ # 需求文档
├── architecture/ # 架构设计
├── api/ # API 接口规范
├── review/ # 代码审查报告 + bug 报告
├── security/ # 安全审计报告
── runbooks/ # 运维操作手册
├── context/project.md # 本文件(项目全貌)
├── dev-setup.md # 环境搭建指南
├── requirements/ # 需求文档
├── architecture/ # 架构设计
├── api/ # API 接口规范
├── review/ # 代码审查报告 + bug 报告
── security/ # 安全审计报告
└── runbooks/ # 运维操作手册
```
## 核心数据模型
```yaml
主要表:
shops: # 酒店(租户)
users: # 用户(含 shop_id
licenses: # 许可证(含 device_id 绑定)
products: # 商品(含 custom_fields JSON 动态扩展)
warehouses: # 仓库
partners: # 往来单位(supplier/customer
stock_in_orders: # 入库单(draft→pending→approved/rejected
stock_in_items: # 入库单明细
stock_out_orders: # 出库单
stock_out_items: # 出库单明细
inventory: # 实时库存(唯一键shop_id+warehouse_id+product_id
inventory_logs: # 库存流水(每次变动记录)
inventory_checks: # 盘点单
finance_records: # 财务流水
number_rules: # 单号生成规则
shops: # 门店(租户根节点)
users: # 用户(含 shop_idrole: admin/operator/readonly
licenses: # 许可证(含 device_id 绑定,type: trial/monthly/annual/lifetime
product_categories: # 商品分类
products: # 商品(含 custom_fields JSON 动态扩展)
warehouses: # 仓库
partners: # 往来单位(type: supplier/customer
stock_in_orders: # 入库单(status: draft→pending→approved/rejected
stock_in_items: # 入库单明细
stock_out_orders: # 出库单
stock_out_items: # 出库单明细
inventories: # 实时库存(唯一键: shop_id+warehouse_id+product_id
inventory_logs: # 库存流水(每次变动自动记录)
inventory_checks: # 盘点单
inventory_check_items: # 盘点明细
finance_records: # 财务流水(receivable/payable/receipt/payment
number_rules: # 单号生成规则(前缀+日期+6位序号)
```
## 关键业务规则
1. **多租户**所有查询必须带 `shop_id` 条件,`shop_id` 只从 JWT 获取,不信任请求参数
2. **库存变更**:入库/出库审核通过时,同一事务更新 `inventory` + 写 `inventory_logs`
3. **出库前检查**出库审核时必须校验库存充足,不足返回错误并回滚事务
4. **单号生成**:通过 `number_rules` 表事务安全生成,格式 `{前缀}{日期}{6位序号}`
5. **扩展字段**:所有业务主表含 `custom_fields JSON`,用于存储动态业务字段
1. **多租户隔离**`shop_id` 只从 JWT 提取(`middleware.GetShopID(c)`),绝不从请求参数读取
2. **库存变更事务**:入库/出库审核时,同一事务更新 `inventories` + 写 `inventory_logs`
3. **出库前校验**审核出库时校验库存充足,不足返回错误并回滚
4. **单号生成**:通过 `number_rules` 表事务安全生成,格式 `{前缀}{YYYYMMDD}{6位序号}`
5. **扩展字段**:所有业务主表含 `custom_fields JSON`,用于存储动态业务字段,避免频繁改表
## 开发脚本(scripts/dev.sh
所有常用操作都通过 `sh scripts/dev.sh <命令>` 执行:
```bash
# 启动
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 测试数据(执行 seeds/H001.sql
sh scripts/dev.sh reset # 删表重建(AutoMigrate
sh scripts/dev.sh clear # 清空所有业务数据(保留表结构)
```
## 本地开发快速启动
```bash
# 1. 启动 MySQL 容器(首次或容器未运行时)
docker compose -f deploy/docker-compose.yml up -d
# 2. 一键启动前后端
sh scripts/dev.sh run
# 3. 写入测试数据(另开终端)
sh scripts/dev.sh seed H001
# 4. 数据库管理界面(可选)
# 浏览器打开 http://localhost:8888
# 服务器: mysql,用户: root,密码: password,数据库: jiu_db
```
## 已实现的 API 接口
```yaml
认证:
- POST /api/v1/auth/login
- POST /api/v1/auth/refresh
认证(无需 JWT:
POST /api/v1/auth/login
POST /api/v1/auth/refresh
许可证:
- POST /api/v1/license/activate
- GET /api/v1/license/verify
- POST /api/v1/license/deactivate
POST /api/v1/license/activate
GET /api/v1/license/verify
POST /api/v1/license/deactivate
商品:
- GET/POST /api/v1/products
- PUT/DELETE /api/v1/products/:id
GET/POST /api/v1/products
PUT/DELETE /api/v1/products/:id
仓库:
- GET/POST /api/v1/warehouses
- PUT/DELETE /api/v1/warehouses/:id
GET/POST /api/v1/warehouses
PUT/DELETE /api/v1/warehouses/:id
往来单位:
- GET/POST /api/v1/partners
- PUT/DELETE /api/v1/partners/:id
GET/POST /api/v1/partners
PUT/DELETE /api/v1/partners/:id
入库:
- GET/POST /api/v1/stock-in/orders
- GET /api/v1/stock-in/orders/:id
- PUT /api/v1/stock-in/orders/:id/submit
- PUT /api/v1/stock-in/orders/:id/approve
- PUT /api/v1/stock-in/orders/:id/reject
GET/POST /api/v1/stock-in/orders
GET /api/v1/stock-in/orders/:id
PUT /api/v1/stock-in/orders/:id/submit
PUT /api/v1/stock-in/orders/:id/approve
PUT /api/v1/stock-in/orders/:id/reject
出库:
- GET/POST /api/v1/stock-out/orders
- GET /api/v1/stock-out/orders/:id
- PUT /api/v1/stock-out/orders/:id/submit
- PUT /api/v1/stock-out/orders/:id/approve
- PUT /api/v1/stock-out/orders/:id/reject
GET/POST /api/v1/stock-out/orders
GET /api/v1/stock-out/orders/:id
PUT /api/v1/stock-out/orders/:id/submit
PUT /api/v1/stock-out/orders/:id/approve
PUT /api/v1/stock-out/orders/:id/reject
库存:
- GET /api/v1/inventory
- GET /api/v1/inventory/logs
- POST/GET /api/v1/inventory/checks
GET /api/v1/inventory
GET /api/v1/inventory/logs
POST/GET /api/v1/inventory/checks
数据导入:
- POST /api/v1/import/products (Excel/CSV)
- POST /api/v1/import/partners (Excel/CSV)
POST /api/v1/import/products (Excel/CSV)
POST /api/v1/import/partners (Excel/CSV)
```
## Flutter 客户端结构
```
client/
├── pubspec.yaml # 依赖:riverpod, go_router, dio, intl
├── lib/
│ ├── main.dart # 入口,ProviderScope + MaterialApp.router
│ ├── core/
│ ├── theme/app_theme.dart # 颜色常量 + ThemeData
│ ├── auth/auth_state.dart # AuthUser, AuthState, AuthNotifier
│ │ ├── api/api_client.dart # Dio 封装
│ └── router/app_router.dart # go_router 路由定义(含登录重定向)
│ ├── screens/
│ ├── auth/login_screen.dart # 登录页(蓝色背景 + 居中卡片)
│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏 + 状态栏)
│ ├── stock_in/ # 入库单列表 + 新建表单
│ ├── stock_out/ # 出库单列表
│ ├── inventory/ # 库存查询 + 盘点
│ ├── partners/ # 往来单位
│ ├── finance/ # 财务管理
│ │ ├── products/ # 商品管理
│ │ ── settings/ # 系统设置(用户/仓库/编号规则)
── widgets/
├── page_scaffold.dart # Tab 页面封装
── data_table_card.dart # 含工具栏+分页的数据表格
│ ├── status_badge.dart # 状态标签(草稿/待审/已审/拒绝)
│ └── form_dialog.dart # 弹窗表单封装
```
**启动 Flutter 客户端**(需先安装 Flutter):
```bash
cd client
flutter pub get
flutter run -d macos # macOS 桌面
flutter run -d chrome # Web
```
## 本地开发启动
```bash
# 1. 启动数据库
cd deploy && docker compose up -d
# 2. 启动后端(会自动 AutoMigrate
cd backend && export PATH="/opt/homebrew/bin:$PATH" && go run main.go
# 3. 运行测试
cd backend && go test ./... -cover
# 4. 数据库管理界面
# 打开 http://localhost:8888,服务器:mysql, 用户:root, 密码:password, 数据库:jiu_db
client/lib/
├── main.dart # 入口:ProviderScope + _AppBootstrapauth restore
├── core/
│ ├── theme/app_theme.dart # 颜色常量 + ThemeData
│ ├── auth/auth_state.dart # AuthUser, AuthState, AuthNotifiershared_preferences 持久化)
│ ├── api/api_client.dart # Dio 封装(401 自动刷新,_disposed 防悬空回调)
└── router/app_router.dart # go_router_RouterNotifier + refreshListenable
├── screens/
├── auth/login_screen.dart # 登录页
│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏 + 状态栏)
│ ├── stock_in/ # 入库管理
│ ├── stock_out/ # 出库管理
│ ├── inventory/ # 库存管理
│ ├── partners/ # 往来单位
│ ├── finance/ # 财务管理
│ ├── products/ # 商品管理(含分类)
└── settings/ # 系统设置(用户/仓库/编号规则)
└── widgets/
── page_scaffold.dart # Tab 页面封装
── data_table_card.dart # 含工具栏+分页的数据表格
├── status_badge.dart # 状态标签(草稿/待审/已审/拒绝)
── form_dialog.dart # 弹窗表单封装
```
## 文档索引
| 文档 | 路径 | 说明 |
|------|------|------|
| 本文件 | docs/context/project.md | 项目全貌,所有 Agent 必读 |
| 项目上下文 | docs/context/project.md | 本文件,所有 Agent 必读 |
| 环境搭建 | docs/dev-setup.md | 工具安装 + 开发脚本详解 + 测试架构 |
| Schema | backend/schema/schema.sql | 完整数据库建表 SQL |
| H001 种子 | backend/seeds/H001.sql | 门店 H001 测试数据 |
+187 -145
View File
@@ -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 安装 CocoaPodsmacOS 桌面版必须)
```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 26beta)下 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`)。