docs: 更新项目文档与开发规范
修正 hotel_id→shop_id 错误引用,补充 dev.sh 脚本用法、 测试架构说明、种子数据规范,整理目录结构为当前实际状态。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
@@ -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/<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 # 完整建表 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 测试数据 |
|
||||
|
||||
+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