diff --git a/CLAUDE.md b/CLAUDE.md index f79eff3..fc6b57d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/.sql` +- 通过 `sh scripts/dev.sh seed ` 执行(内部用 `docker exec` 注入 MySQL 容器) +- 每个门店一个文件,SQL 文件顶部包含 TRUNCATE,每次执行完整重建 + +### Go 命令 +- 所有 Go 命令前须确保 PATH:`export PATH="/opt/homebrew/bin:$PATH"` +- 或直接使用 `sh scripts/dev.sh` 脚本(已内置 PATH) diff --git a/docs/context/project.md b/docs/context/project.md index e0e5c11..08175ba 100644 --- a/docs/context/project.md +++ b/docs/context/project.md @@ -1,11 +1,11 @@ -# 酒店仓库管理系统 — 项目上下文 +# 酒库管理系统 — 项目上下文 > 所有 Agent 在开始工作前必须读取此文件,了解项目全貌。 ## 项目简介 面向酒水门店的仓库管理系统。核心特点: -- **多租户**:一个账号对应一个门店,数据完全隔离(通过 `shop_id` 字段) +- **多租户**:每个账号对应一个门店,数据通过 `shop_id` 完全隔离 - **付费授权**:许可证绑定设备 ID,支持试用/年付/买断 - **跨端客户端**:Flutter 单一代码库,支持 Windows / Web / macOS / iOS / Android @@ -15,21 +15,21 @@ 后端: 语言: Go 1.26 框架: Gin(HTTP)+ GORM(ORM) - 数据库: MySQL 8.0 + 数据库: MySQL 8.0(Docker 容器 jiu_mysql) 认证: JWT(HS256),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/.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 # 完整建表 SQL(MySQL) +├── 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_id,role: 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 + _AppBootstrap(auth restore) +├── core/ +│ ├── theme/app_theme.dart # 颜色常量 + ThemeData +│ ├── auth/auth_state.dart # AuthUser, AuthState, AuthNotifier(shared_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 测试数据 | diff --git a/docs/dev-setup.md b/docs/dev-setup.md index 8b7fcc2..af3fc42 100644 --- a/docs/dev-setup.md +++ b/docs/dev-setup.md @@ -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/.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`)。