c1ed81dfab
后端 - 新增 shop handler:GET/PUT /shop/info(管理员权限) - 新增 finance CloseByRef:按单据 ref_type+ref_id 结清账款 - 新增 inventory UpdateRemark:PUT /inventory/:id/remark - 入库/出库审批自动生成财务应付/应收记录(去除金额>0限制) - 种子数据 S001-S003 补充真实门店信息 前端 - 设置页新增「酒行信息」Tab,管理员可编辑门店名称/地址/电话/负责人 - 入库单列表新增结清按钮(含确认弹窗),出库单同步 - 入库表单:规格、系列、生产日期、供应商、商品名称改为提交必填 - 入库/出库列表新增入库时间、出库时间、创建时间列 - 商品标签标题改为读取 shop 表门店名,扫码文案改为「扫码溯源 · TRACE」 - 标签页脚显示门店地址和电话(从 API 读取,不再依赖编译时 dart-define) - 库存备注支持点击编辑,超4字截断显示+Hover展示全文 - ApiClient 新增 patch() 方法(已改用 PUT 规避 CORS) 文档 - 新增 docs/user-manual.md 完整用户操作手册(12章) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
340 lines
15 KiB
Markdown
340 lines
15 KiB
Markdown
# 酒库管理系统 — 项目上下文
|
||
|
||
> 所有 Agent 在开始工作前必须读取此文件,了解项目全貌。
|
||
|
||
## 项目简介
|
||
|
||
面向酒水门店的仓库管理系统。核心特点:
|
||
- **多租户**:每个账号对应一个门店,数据通过 `shop_id` 完全隔离
|
||
- **付费授权**:许可证绑定设备 ID,支持试用/年付/买断
|
||
- **跨端客户端**:Flutter 单一代码库,支持 Windows / Web / macOS / iOS / Android
|
||
|
||
## 技术栈
|
||
|
||
```yaml
|
||
后端:
|
||
语言: Go 1.26
|
||
框架: Gin(HTTP)+ GORM(ORM)
|
||
数据库: 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(含 401 自动刷新 token 拦截器)
|
||
路由: go_router(含登录重定向)
|
||
目录: client/
|
||
|
||
数据库管理:
|
||
Schema: backend/schema/schema.sql(完整建表 SQL,容器启动时自动执行)
|
||
ORM 迁移: GORM AutoMigrate(后端启动时自动同步)
|
||
种子数据: backend/seeds/<shop>.sql(通过 dev.sh seed 命令执行)
|
||
```
|
||
|
||
## 项目目录结构
|
||
|
||
```
|
||
jiu/
|
||
├── backend/
|
||
│ ├── main.go # 入口,启动时执行 AutoMigrate
|
||
│ ├── config/
|
||
│ │ ├── config.go # 配置结构体(含 mapstructure 标签)
|
||
│ │ └── config.yaml # 本地开发配置(不提交 git)
|
||
│ ├── internal/
|
||
│ │ ├── 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)
|
||
│ │ │ # ReadOnly():只读用户禁止写操作(全局挂载)
|
||
│ │ │ # AdminOnly():仅管理员可访问(挂载在 /users 路由组)
|
||
│ │ └── router/router.go # 路由注册
|
||
│ ├── testutil/setup.go # 测试工具包(SQLite DB、CreateTestXxx、GetAuthToken)
|
||
│ ├── cmd/seed/main.go # 数据库工具(--reset 删表重建,--clear 清空数据)
|
||
│ ├── seeds/
|
||
│ │ └── S001.sql # 门店 S001 测试种子数据(SQL 命令形式)
|
||
│ └── schema/schema.sql # 完整建表 SQL(MySQL)
|
||
├── client/ # Flutter 跨端客户端
|
||
├── deploy/
|
||
│ └── docker-compose.yml # 本地开发:MySQL(3306) + Adminer(8888)
|
||
├── scripts/
|
||
│ ├── dev.sh # 一站式开发脚本(见下方用法)
|
||
│ └── .logs/ # 脚本运行日志(已加入 .gitignore,不提交)
|
||
└── docs/
|
||
├── context/project.md # 本文件(项目全貌)
|
||
├── dev-setup.md # 环境搭建指南
|
||
├── requirements/ # 需求文档
|
||
├── architecture/ # 架构设计
|
||
├── api/ # API 接口规范
|
||
├── review/ # 代码审查报告 + bug 报告
|
||
├── security/ # 安全审计报告
|
||
└── runbooks/ # 运维操作手册
|
||
```
|
||
|
||
## 核心数据模型
|
||
|
||
```yaml
|
||
shops: # 门店(租户根节点)
|
||
users: # 用户(含 shop_id,role: admin/operator/readonly)
|
||
licenses: # 许可证(含 device_id 绑定,type: trial/monthly/annual/lifetime)
|
||
product_categories: # 商品分类
|
||
products: # 商品(含 custom_fields JSON 动态扩展)
|
||
# UNIQUE KEY uk_product_code (shop_id, code) — 防并发重复
|
||
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` 只从 JWT 提取(`middleware.GetShopID(c)`),绝不从请求参数读取
|
||
2. **库存变更事务**:入库/出库审核时,同一事务内更新 `inventories` + 写 `inventory_logs`
|
||
3. **出库前校验**:审核出库时校验库存充足,不足返回错误并回滚
|
||
4. **单号生成**:通过 `number_rules` 表事务安全生成,格式 `{前缀}{YYYYMMDD}{6位序号}`
|
||
5. **扩展字段**:所有业务主表含 `custom_fields JSON`,用于存储动态业务字段,避免频繁改表
|
||
6. **并发安全**:
|
||
- `GenerateOrderNo` 在事务内用 `FOR UPDATE` 锁住 `number_rules` 行,防止并发生成重复单号
|
||
- `ApproveStockOut`/`updateInventory` 在事务内用 `FOR UPDATE` 锁住库存行,防止超卖竞态
|
||
- 商品编码 `products.code` 有 UNIQUE 约束,Create 时重试最多 5 次
|
||
|
||
## 开发脚本(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 stop # 停止所有服务
|
||
|
||
# 数据库
|
||
sh scripts/dev.sh seed S001 # 清空并写入 S001 测试数据(执行 seeds/S001.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 S001
|
||
# 写入后可用以下账号登录(门店编号 S001):
|
||
# admin / password123 管理员
|
||
# operator / password123 操作员
|
||
# test / password123 只读
|
||
|
||
# 4. 数据库管理界面(可选)
|
||
# 浏览器打开 http://localhost:8888
|
||
# 服务器: mysql,用户: root,密码: password,数据库: jiu_db
|
||
```
|
||
|
||
## 已实现的 API 接口
|
||
|
||
```yaml
|
||
认证(无需 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
|
||
GET /api/v1/license/info # 返回当前许可证详情(含到期日、类型)
|
||
|
||
版本(无需 JWT):
|
||
GET /version # 返回最新版本信息(读取 version.yaml,缺失返回 500)
|
||
|
||
商品:
|
||
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/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-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
|
||
GET /api/v1/inventory/batch-tracking # 批次追踪(已审核入库商品含销售状态)
|
||
POST/GET /api/v1/inventory/checks
|
||
|
||
用户管理(仅 admin 角色):
|
||
GET/POST /api/v1/users
|
||
PUT/DELETE /api/v1/users/:id
|
||
|
||
数据导入:
|
||
POST /api/v1/import/products (Excel/CSV)
|
||
POST /api/v1/import/partners (Excel/CSV)
|
||
```
|
||
|
||
## Flutter 客户端结构
|
||
|
||
```
|
||
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 防悬空回调)
|
||
│ ├── config/app_config.dart # 全局 URL 配置(通过 --dart-define=BASE_URL=... 注入)
|
||
│ ├── router/app_router.dart # go_router(_RouterNotifier + refreshListenable)
|
||
│ ├── storage/login_history.dart # 登录历史(shared_preferences,支持候选词自动填充)
|
||
│ ├── models/page_result.dart # 通用分页结果模型
|
||
│ └── exceptions.dart # 自定义异常类型
|
||
├── models/ # 数据模型(与后端 JSON 对应)
|
||
├── repositories/ # 数据访问层(封装 API 调用)
|
||
│ └── auth_repository.dart # 登录/刷新 token(含历史记录保存)
|
||
├── providers/ # Riverpod 状态管理
|
||
│ ├── license_provider.dart # 许可证状态(licenseInfoProvider)
|
||
│ ├── connectivity_provider.dart # 网络连通性探测(connectivityProvider)
|
||
│ ├── update_provider.dart # 版本更新检查(updateCheckProvider)
|
||
│ │ # 平台判断用 kIsWeb,避免 Web 上调用 dart:io
|
||
│ ├── product_provider.dart # 商品列表(AsyncNotifierProvider,含缓存)
|
||
│ ├── stock_in_provider.dart # 入库单列表
|
||
│ ├── stock_out_provider.dart # 出库单列表
|
||
│ ├── inventory_provider.dart # 库存列表
|
||
│ ├── finance_provider.dart # 财务记录
|
||
│ ├── partner_provider.dart # 往来单位
|
||
│ ├── warehouse_provider.dart # 仓库列表
|
||
│ ├── user_provider.dart # 用户管理
|
||
│ └── number_rule_provider.dart # 编号规则
|
||
├── screens/
|
||
│ ├── auth/login_screen.dart # 登录页(含候选词下拉,150ms 延迟防止覆盖 onTap)
|
||
│ ├── shell/app_shell.dart # 主框架(顶栏 + 侧边栏 + 状态栏 + 更新提示 banner)
|
||
│ ├── stock_in/ # 入库管理
|
||
│ ├── stock_out/ # 出库管理
|
||
│ ├── inventory/ # 库存管理(含批次追踪 batch_tracking_screen.dart)
|
||
│ ├── partners/ # 往来单位
|
||
│ ├── finance/ # 财务管理
|
||
│ ├── products/ # 商品管理(含分类)
|
||
│ └── settings/ # 系统设置(用户/仓库/编号规则/关于)
|
||
└── widgets/
|
||
├── page_scaffold.dart # Tab 页面封装
|
||
├── data_table_card.dart # 含工具栏+分页的数据表格
|
||
│ # StatefulWidget,用 Table + IntrinsicColumnWidth 实现
|
||
│ # ValueNotifier<int> _hoveredRow 驱动行 hover 高亮
|
||
├── multi_select_dropdown.dart # 多选筛选组件(多个导出):
|
||
│ # MultiSelectDropdown — 独立多选按钮(toolbar 使用)
|
||
│ # FilterableColumnHeader — 列头内嵌筛选图标
|
||
│ # hover 显示,active 常显;图标 16px 靠右对齐
|
||
│ # ColDef — 列定义(key, label, required, minWidth)
|
||
│ # ColumnToggleButton — 列显示/隐藏切换按钮
|
||
├── status_badge.dart # 状态标签(草稿/待审/已审/拒绝)
|
||
└── form_dialog.dart # 弹窗表单封装
|
||
```
|
||
|
||
## 前端配置管理
|
||
|
||
后端 URL 通过 `AppConfig` 统一管理,支持构建时注入:
|
||
|
||
```dart
|
||
// client/lib/core/config/app_config.dart
|
||
class AppConfig {
|
||
static const _baseUrl = String.fromEnvironment(
|
||
'BASE_URL',
|
||
defaultValue: 'http://localhost:8080',
|
||
);
|
||
static String get baseUrl => _baseUrl;
|
||
static String get apiBaseUrl => '$_baseUrl/api/v1';
|
||
static String get healthUrl => '$_baseUrl/health';
|
||
static String get versionUrl => '$_baseUrl/version';
|
||
}
|
||
```
|
||
|
||
**构建/运行时注入**:
|
||
```bash
|
||
flutter run --dart-define=BASE_URL=http://192.168.1.100:8080
|
||
flutter build web --dart-define=BASE_URL=https://api.example.com
|
||
```
|
||
|
||
**规则**:所有硬编码 `localhost:8080` 的 URL 必须改用 `AppConfig` 对应属性,不得直接拼接字符串。
|
||
|
||
## 前端表格 UI 规范
|
||
|
||
所有数据表格使用 `DataTableCard` widget,遵循以下规范:
|
||
|
||
### 行 Hover 效果
|
||
`data_table_card.dart` 用 `Table` + `MouseRegion` + `ValueNotifier<int>` 实现行级 hover:
|
||
- hover 时行背景色:`Color(0xFFF5F7FF)`
|
||
- 列头背景色:`Color(0xFFF0F4FF)`
|
||
|
||
### 列头内嵌筛选(FilterableColumnHeader)
|
||
可筛选的列头使用 `FilterableColumnHeader`,作为 `DataColumn.label` 传入:
|
||
```dart
|
||
DataColumn(
|
||
label: FilterableColumnHeader(
|
||
text: '仓库',
|
||
options: warehouseOptions,
|
||
selected: _filterWarehouse,
|
||
onChanged: (v) => setState(() => _filterWarehouse = v),
|
||
),
|
||
)
|
||
```
|
||
- 鼠标 hover 时显示筛选图标(`filter_alt_outlined`,16px,右对齐)
|
||
- 已激活筛选时图标常驻(`filter_alt`,蓝色)+ 取消 icon
|
||
- 点击图标弹出多选对话框
|
||
|
||
### 客户端筛选模式
|
||
部分表格(库存、商品、批次追踪、财务)采用客户端筛选:加载全量数据,在内存中过滤,派生 options:
|
||
```dart
|
||
final warehouseOptions = _records
|
||
.map((r) => r.warehouseName ?? '')
|
||
.where((s) => s.isNotEmpty)
|
||
.toSet().toList()..sort();
|
||
```
|
||
|
||
### 列显示/隐藏(ColDef + ColumnToggleButton)
|
||
使用 `ColDef` 定义列,支持:
|
||
- `required: true` — 不可隐藏
|
||
- `minWidth: 1000` — 屏幕宽度不足时自动隐藏
|
||
|
||
## 文档索引
|
||
|
||
| 文档 | 路径 | 说明 |
|
||
|------|------|------|
|
||
| 项目上下文 | docs/context/project.md | 本文件,所有 Agent 必读 |
|
||
| 环境搭建 | docs/dev-setup.md | 工具安装 + 开发脚本详解 + 测试架构 |
|
||
| Schema | backend/schema/schema.sql | 完整数据库建表 SQL |
|
||
| S001 种子 | backend/seeds/S001.sql | 门店 S001 测试数据(含完整入库/出库/库存历史) |
|
||
| S002 种子 | backend/seeds/S002.sql | 门店 S002 测试数据(基础数据相同,无库存/单据,模拟新门店) |
|
||
| 用户手册 | docs/user-manual.md | 酒行员工操作手册(登录/入库/出库/库存/财务/设置) |
|