# 酒库管理系统 — 项目上下文 > 所有 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/.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 # 完整建表 SQL(MySQL) ├── client/ # Flutter 跨端客户端 │ └── android/ ios/ macos/ web/ windows/ # 五个平台目录(均已初始化) ├── deploy/ │ └── docker-compose.yml # 本地开发:MySQL(3306) + Adminer(8888) ├── .gitea/workflows/ # Forgejo Actions(deploy.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_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 /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 + _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,支持候选词自动填充) │ ├── 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 _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` 实现行级 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 // 宽度 < 600(kMobileBreakpoint) 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 secrets,CI 给 APK 正式签名 | | iOS 分发 | docs/ios-signing.md | 证书/Profile/API Key + secrets,CI 构建并上传 TestFlight | | 部署(NAS/Gitea) | docs/deployment-nas-gitea.md | 自建 Forgejo + runner 部署说明 | | 待办 | docs/TODO.md | 后续迭代事项(iOS 上架、应用内 APK 安装、平板布局等) |