Files
jiu/docs/context/project.md
T
wangjia 480ce836bb
Deploy / build-linux-web (push) Successful in 53s
Deploy / build-windows (push) Successful in 1m48s
Deploy / build-macos (push) Successful in 1m17s
Deploy / build-android (push) Successful in 4m13s
Deploy / build-ios (push) Successful in 9s
Deploy / release-deploy (push) Successful in 1m37s
chore: release v1.0.18
移动端响应式适配(抽屉导航/列表卡片/弹窗自适应)、Android 正式签名与 APK 发布、
iOS(TestFlight) 工程与 CI、多平台构建流水线、相关文档同步。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 07:55:33 +08:00

405 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 酒库管理系统 — 项目上下文
> 所有 Agent 在开始工作前必须读取此文件,了解项目全貌。
## 项目简介
面向酒水门店的仓库管理系统。核心特点:
- **多租户**:每个账号对应一个门店,数据通过 `shop_id` 完全隔离
- **付费授权**:许可证绑定设备 ID,支持试用/年付/买断
- **跨端客户端**:Flutter 单一代码库,支持 Windows / Web / macOS / iOS / Android
## 技术栈
```yaml
后端:
语言: Go 1.26
框架: GinHTTP+ GORMORM
数据库: MySQL 8.0Docker 容器 jiu_mysql
认证: JWTHS256),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)
│ │ └── version.yaml # 版本清单(version + download_urls,发版时由 release.sh 更新)
│ ├── 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 # 完整建表 SQLMySQL
├── client/ # Flutter 跨端客户端
│ └── android/ ios/ macos/ web/ windows/ # 五个平台目录(均已初始化)
├── deploy/
│ └── docker-compose.yml # 本地开发:MySQL(3306) + Adminer(8888)
├── .gitea/workflows/ # Forgejo Actionsdeploy.yml=tag 发版,及备份/重置等)
├── scripts/
│ ├── dev.sh # 一站式开发脚本(见下方用法)
│ ├── ci/ # CI 编译/发布脚本(compile-*.sh / release.sh / deploy.sh / provision-*.sh
│ └── .logs/ # 脚本运行日志(已加入 .gitignore,不提交)
└── docs/
├── context/project.md # 本文件(项目全貌)
├── dev-setup.md # 环境搭建指南
├── requirements/ # 需求文档
├── architecture/ # 架构设计
├── api/ # API 接口规范
├── review/ # 代码审查报告 + bug 报告
├── security/ # 安全审计报告
└── runbooks/ # 运维操作手册
```
## 核心数据模型
```yaml
shops: # 门店(租户根节点)
users: # 用户(含 shop_idrole: 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 /api/v1/public/release # 版本 + download_urls(驱动客户端更新提示与下载页)
商品:
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)
意见反馈:
POST /api/v1/feedback # 提交反馈(bug/suggestion,文字+图片,认证)
POST /api/v1/feedback/images # 上传反馈附图(认证)
GET /api/v1/admin/feedback # 反馈列表(仅 superadmin
PATCH /api/v1/admin/feedback/:id # 标记处理状态(仅 superadmin
```
## Flutter 客户端结构
```
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 防悬空回调)
│ ├── config/app_config.dart # 全局 URL 配置(通过 --dart-define=BASE_URL=... 注入)
│ ├── router/app_router.dart # go_router_RouterNotifier + refreshListenable
│ ├── storage/login_history.dart # 登录历史(shared_preferences,支持候选词自动填充)
│ ├── responsive/responsive.dart # 响应式:context.isMobile<600)、context.dialogWidth(X)
│ ├── update/app_updater*.dart # 应用内更新(_io: Win/macOS 装;_web: 刷新;移动端/兜底开下载链接)
│ ├── errors/error_reporter.dart # 异常上报(reportError
│ ├── 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 # 主框架(顶栏 + 侧边栏/窄屏 Drawer 抽屉 + 状态栏 + 更新 banner
│ ├── stock_in/ # 入库管理
│ ├── stock_out/ # 出库管理
│ ├── inventory/ # 库存管理(含批次追踪 batch_tracking_screen.dart
│ ├── partners/ # 往来单位
│ ├── finance/ # 财务管理
│ ├── products/ # 商品管理(含分类)
│ ├── about/ # 关于我们(含意见反馈:文字+附图直接提交后台)
│ ├── public/ # 商品扫码公开展示页(无需登录)
│ └── settings/ # 系统设置(用户/仓库/编号规则)
└── widgets/
├── page_scaffold.dart # Tab 页面封装
├── data_table_card.dart # 含工具栏+分页的数据表格
│ # StatefulWidget,用 Table + IntrinsicColumnWidth 实现
│ # ValueNotifier<int> _hoveredRow 驱动行 hover 高亮
│ # mobileCards 入参:窄屏渲染卡片流,宽屏仍表格
├── mobile_list_card.dart # 移动端列表卡片:MobileListCard / MobileCardField
│ # 标题 + 右上角徽章 + 字段竖排 + 底部操作,替代窄屏表格行
├── 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` — 屏幕宽度不足时自动隐藏
## 移动端 / 响应式 UI 规范
客户端同一套代码跑桌面/Web/平板/手机。**窄屏(手机)自动切换为移动布局,宽屏保持桌面布局不变**。
### 断点判定
统一用 `client/lib/core/responsive/responsive.dart` 的扩展,不要散落魔法数:
```dart
context.isMobile // 宽度 < 600kMobileBreakpoint
context.dialogWidth(720) // 固定宽度弹窗的安全宽度,≤ 屏宽 92%
```
### 列表 → 卡片
列表屏用 `DataTableCard` 时同时传 `mobileCards`,窄屏渲染卡片流、宽屏仍表格:
```dart
DataTableCard(
columns: [...], rows: [...], // 宽屏表格
mobileCards: items.map(_buildCard).toList(), // 窄屏卡片(MobileListCard
)
```
卡片复用 `MobileListCard`(标题/徽章/字段/操作)+ `MobileCardField``widgets/mobile_list_card.dart`)。
顶部并排汇总卡片在窄屏改为横向滚动。
### 弹窗宽度
固定宽度弹窗一律 `width: context.dialogWidth(X)`**禁止**裸写 `width: <固定值>`(窄屏会溢出)。
表单字段(`_FormField`)窄屏占满整行。
### 导航
窄屏用 Drawer 抽屉(`app_shell.dart``_buildDrawer`,汉堡按钮打开),隐藏底部状态栏;宽屏保持侧边栏。
新增页面挂在 shell 下即可,自动适配,无需单独处理。
## 跨端分发
| 平台 | 构建产物 | 分发方式 |
|------|----------|----------|
| Web | `flutter build web` | 部署到 `jiu.51yanmei.com/app` |
| Windows | Inno Setup `setup.exe` | 下载页 `/downloads/` + 应用内更新 |
| macOS | `.app` zip | 下载页 `/downloads/` + 应用内更新 |
| Android | 签名 `jiu-android.apk` | 下载页 `/downloads/`(见 docs/android-signing.md |
| iOS | 签名 IPA | TestFlight(见 docs/ios-signing.md |
版本清单:`backend/config/version.yaml``download_urls`(含各平台下载地址);客户端 `GET /api/v1/public/release` 读取,驱动更新提示与下载页。
## 文档索引
| 文档 | 路径 | 说明 |
|------|------|------|
| 项目上下文 | 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 | 酒行员工操作手册(登录/入库/出库/库存/财务/设置) |
| Android 签名 | docs/android-signing.md | 生成 keystore + 配置 Forgejo secretsCI 给 APK 正式签名 |
| iOS 分发 | docs/ios-signing.md | 证书/Profile/API Key + secretsCI 构建并上传 TestFlight |
| 部署(NAS/Gitea | docs/deployment-nas-gitea.md | 自建 Forgejo + runner 部署说明 |
| 待办 | docs/TODO.md | 后续迭代事项(iOS 上架、应用内 APK 安装、平板布局等) |