Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
24 KiB
酒库管理系统 — 项目上下文
所有 Agent 在开始工作前必须读取此文件,了解项目全貌。
项目简介
面向酒水门店的仓库管理系统。核心特点:
- 多租户:每个账号对应一个门店,数据通过
shop_id完全隔离 - 付费授权:许可证绑定设备 ID,支持试用/年付/买断
- 跨端客户端:Flutter 单一代码库,支持 Windows / Web / macOS / iOS / Android
技术栈
后端:
语言: 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)
│ │ └── 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/ # 运维操作手册
核心数据模型
关键语义:product = 「特有产品 / 序列号」(非 SKU)
products 表的每一行 = 一个特有产品,以序列号(= 商品编码 code,如 ZXZ###### / P####)唯一标识,每个产品独一份、不复用(像手机序列号,哪怕同款也各不相同)。一个产品的全部信息以 product 为单一来源:
code序列号(同店唯一,UNIQUE KEY uk_shop_code (shop_id, code)防并发/重复)name=品牌、series=型号、spec=版本/规格、unit单位production_date生产日期、batch_no批次、purchase_price进价、sale_price售价public_id(公开页/二维码)、product_images(图片)、name_pinyin/name_initials(拼音搜索索引,写入时由util.ToPinyin自动生成)
入库每录一行 = 新建一个独立 product(发新序列号),不按名称复用(旧 findOrCreate 复用逻辑已废弃);同名同型号录两次 = 两个独立序列号。handler/product.go 的 nextProductCode(max+1 自增)+ createIndependentProduct 负责。
基础数据字典(「基础数据」页 products_screen.dart 管理)
入库选择商品时从字典选取文本,再据此建 product:
product_name_options(商品名=品牌)、product_series_options(系列=型号)、product_spec_options(规格=版本)- 商品属性字典:
product_origin_options(产地)、product_shelf_life_options(保质期)、product_storage_options(储存方式)、product_description_docs(介绍文档)——公开页展示用,product 以可空外键引用
库存与单据(库存/明细 = 指向 product 的引用 + 历史快照)
inventories: # 库存批次:每行 = 某 product 在某仓库的一批(product_id + warehouse_id + quantity)
# + stock_in_item_id(来源入库明细,盘盈则 inventory_check_id)
# 含「快照列」product_code/product_name/series/spec/unit/unit_price/
# production_date/batch_no/supplier_name/warehouse_name —— 导入/审核时
# 从源/product 拷贝,作历史保真 + 显示兜底
stock_in_orders: # 入库单(status: draft→pending→approved/rejected;可由本人/管理员 withdraw 回 draft)
stock_in_items: # 入库明细(product_id + 快照列 + quantity/unit_price/total_price/batch_no/production_date)
stock_out_orders: # 出库单(同状态机)
stock_out_items: # 出库明细(同上)
inventory_logs: # 库存流水(每次变动自动记录 in/out + qty_before/after)
inventory_checks / inventory_check_items: # 盘点单 / 盘点明细(FIFO 盘盈盘亏)
显示策略:库存列表
inventory.go用COALESCE(NULLIF(p.code,''), 快照列)——优先 product,product 为空/被删才回退快照。前端lineOrProduct同理。历史导入的出入库明细product_id多指向一个占位 product(HIST-PLACEHOLDER),真实信息存快照列。
其它主表
shops: # 门店(租户根节点)
users: # 用户(含 shop_id,role: superadmin/admin/operator/readonly)
licenses / license_codes / license_devices: # 授权(时长兑换券短码 + 设备绑定)
product_categories: # 商品分类(可空)
warehouses: # 仓库
partners: # 往来单位(type: supplier/customer)
finance_records: # 财务流水(receivable/payable/receipt/payment)
number_rules: # 单号生成规则(前缀+日期+6位序号)
feedbacks: # 意见反馈(bug/suggestion + 图片)
关键业务规则
- 多租户隔离:
shop_id只从 JWT 提取(middleware.GetShopID(c)),绝不从请求参数读取 - product = 序列号:入库每行新建独立 product(不按名称复用);product 是商品信息单一来源,库存/明细只是指向它的引用 + 历史快照(见上节)
- 库存变更事务:入库/出库审核时,同一事务内更新
inventories+ 写inventory_logs - 出库前校验:审核出库时校验库存充足,不足返回错误并回滚
- 单据状态机与撤回:
draft→submit→pending→approve/reject;审核中(pending)可 withdraw 回 draft 再改再提交——管理员/超管撤回任意单,操作员限本人单(OperatorID==本人,handler 内判权);已审核(approved)只读 - 单号生成:通过
number_rules表事务安全生成,格式{前缀}{YYYYMMDD}{6位序号} - 权限角色:
superadmin(超管,可清空数据/看反馈)>admin(管理员,管用户/店铺信息)>operator(操作员)>readonly(只读,所有写操作 403)。middleware.ReadOnly()全局挂载、AdminOnly()(admin+superadmin) /SuperAdminOnly()按需挂载 - 并发安全:
GenerateOrderNo在事务内用FOR UPDATE锁住number_rules行,防并发重复单号ApproveStockOut/updateInventory在事务内用FOR UPDATE锁库存行,防超卖竞态products.code有(shop_id,code)UNIQUE 约束,建 product 按最大序号+1、ErrDuplicatedKey 重试最多 5 次(不复用软删号)
开发脚本(scripts/dev.sh)
所有常用操作都通过 sh scripts/dev.sh <命令> 执行:
# 启动
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 # 清空所有业务数据(保留表结构)
本地开发快速启动
# 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 接口(以 router/router.go 为准)
无需 JWT:
GET /health /version
POST /api/v1/auth/login /api/v1/auth/refresh /api/v1/public/register
GET /api/v1/public/products/:public_id # 公开商品详情(扫码页)
GET /api/v1/public/shops/:shop_code/products # 店铺公开商品列表(仅有库存 + 数量 + code)
GET /api/v1/public/release # 版本 + download_urls
GET /product/:public_id # 注入 OG 标签的分享页
POST /api/v1/public/errors # 客户端异常上报
会话/许可证:
POST /api/v1/auth/logout /api/v1/auth/ping
GET /api/v1/sessions ; DELETE /api/v1/sessions/:id (AdminOnly)
GET /api/v1/license/info|verify|devices ; POST /api/v1/license/activate|deactivate
商品(product=序列号):
GET/POST /api/v1/products ; PUT/DELETE /api/v1/products/:id
POST /api/v1/products/find-or-create
GET /api/v1/products/:id/detail /:id/qrcode
POST /api/v1/products/:id/images ; DELETE /api/v1/products/:id/images/:image_id
基础数据字典 /api/v1/product-options:
names | series | specs | origins | shelf-lives | storages | description-docs
每组 GET(支持 ?keyword 服务端搜索)/ POST / PUT/:id / DELETE/:id
仓库 /warehouses · 往来单位 /partners(?type&keyword): GET/POST/PUT/DELETE
入库 /stock-in/orders · 出库 /stock-out/orders:
GET(列表,?status&keyword) /POST ; GET/PUT/DELETE/:id
PUT /:id/submit | approve | reject | withdraw # withdraw=审核中撤回回草稿
库存 /inventory:
GET ""(?warehouse_id&keyword&series&spec&in_stock) ; GET /logs
PUT /:id/remark ; POST /checks ; GET /checks/:id ; PUT /checks/:id/complete
财务 /finance: GET /records /summary ; POST /records ; PUT /records/:id/close · /close-by-ref
用户 /users(AdminOnly): GET/POST ; PUT/:id ; PUT /:id/reset-password
店铺 /shop: GET /info ; PUT /info、POST /logo (AdminOnly)
编号规则 /number-rules: GET ""; PUT /:id
意见反馈 /feedback: POST ""、/images
数据导入 /import: products | partners | product-names|series|specs|codes | stock-in | stock-out | inventory (Excel)
超管 /admin(SuperAdminOnly): POST /clear-data ; GET /reconcile /errors /feedback ; PATCH /feedback/:id
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<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 统一管理,支持构建时注入:
// 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';
}
构建/运行时注入:
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 传入:
DataColumn(
label: FilterableColumnHeader(
text: '仓库',
options: warehouseOptions,
selected: _filterWarehouse,
onChanged: (v) => setState(() => _filterWarehouse = v),
),
)
- 鼠标 hover 时显示筛选图标(
filter_alt_outlined,16px,右对齐) - 已激活筛选时图标常驻(
filter_alt,蓝色)+ 取消 icon - 点击图标弹出多选对话框
客户端筛选模式
部分表格(库存、商品、批次追踪、财务)采用客户端筛选:加载全量数据,在内存中过滤,派生 options:
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 的扩展,不要散落魔法数:
context.isMobile // 宽度 < 600(kMobileBreakpoint)
context.dialogWidth(720) // 固定宽度弹窗的安全宽度,≤ 屏宽 92%
列表 → 卡片
列表屏用 DataTableCard 时同时传 mobileCards,窄屏渲染卡片流、宽屏仍表格:
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) |
版本清单 version.yaml(归 client,写 version/build_number/release_notes/下载链接/changelog);后端 GET /version 与 /api/v1/public/release 每请求实时读取,client 部署后立即生效,无需重启后端、无需重建官网。
三条独立发版流水线(互不影响,各有 tag 前缀 / CHANGELOG / workflow)
| part | 范围 | tag 前缀 | CHANGELOG | workflow |
|---|---|---|---|---|
| client | client/ Flutter 全平台 + version.yaml |
client-v* |
CHANGELOG-client.md |
deploy-client.yml |
| site | web/ Eleventy 营销宣传站(不含 Web 版 app) |
site-v* |
CHANGELOG-site.md |
deploy-site.yml |
| server | backend/ Go 服务 + 共享基建(nginx/systemd) |
server-v* |
CHANGELOG-server.md |
deploy-server.yml |
用 /release <part> [version] slash command:本地 build→test→更新 CHANGELOG→commit→tag→push;CI(Forgejo) 按 tag 前缀触发对应 workflow 自动编译/测试/发 Release/部署 EC2/Telegram 通知。测试未过禁止发版。
生产环境与运维
- 生产:EC2(
ec2-user@18.136.60.128),jiu 为宿主 systemd 服务,MySQL 在容器jiu_mysql(映射 127.0.0.1:3306)。配置/opt/jiu/config/production.env(DATABASE_DSN)。 - CI runner:mac runner=开发者本机(launchd + relay 绕 Shadowrocket,有看门狗自愈)、windows runner=另一台机(nssm 服务
forgejo-runner)、ubuntu=NAS docker。Forgejo 在 NAS(git.51yanmei.com)。 - 一次性数据工具:
cmd/import-history(旧系统进销存批量导入)、cmd/fix-inventory-products(修复历史库存 product_id 错指)。
文档索引
| 文档 | 路径 | 说明 |
|---|---|---|
| 项目上下文 | 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 安装、平板布局等) |