Files
wangjia 05203a9b5b
Design Source Checks / design-source (push) Failing after 14m23s
refactor: 原型迁移 .superpowers/prototype → design/prototype(与 CONTRACT 同目录)
- git mv 全目录(历史保留);.gitignore 收敛为整个 .superpowers/ 忽略
- 全仓 16 处引用同步(CI checks.yml / l1-sync / screens.mjs / hooks /
  CLAUDE.md / CONTRACT / SSR 模板注释 / docs / web 注释)
- 修 pre-commit 路径正则残留;5180 评审服务已切新路径
- 验证:check-ds 12 道 / l1-sync 6 道 / fidelity 抽查全过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-07 18:47:31 +08:00

25 KiB
Raw Permalink Blame History

酒库管理系统 — 项目上下文

所有 Agent 在开始工作前必须读取此文件,了解项目全貌。

项目简介

面向酒水门店的仓库管理系统。核心特点:

  • 多租户:每个账号对应一个门店,数据通过 shop_id 完全隔离
  • 付费授权:许可证绑定设备 ID,支持试用/年付/买断
  • 跨端客户端:Flutter 单一代码库,支持 Windows / Web / macOS / iOS / Android

技术栈

后端:
  语言: 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/                    # 运维操作手册

核心数据模型

关键语义: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.gonextProductCodemax+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 + cost_price 进价·单瓶 + cost_amount 总进价 + batch_no/production_date
stock_out_orders:    # 出库单(同状态机;sale_total 应收合计 + profit_total 总利润,建单落库、确认售价/进价联动重算)
stock_out_items:     # 出库明细(cost_price 成本快照 + sale_price 售价 + cost_amount/sale_amount 两侧小计;成本/利润仅管理员可见,operator 响应被服务端抹零)
                     #   ※ 2026-07 定价消歧:旧列 unit_price/total_price/total_amount 弃用(详见 CLAUDE.md「定价字段口径」)
inventory_logs:      # 库存流水(每次变动自动记录 in/out + qty_before/after
inventory_checks / inventory_check_items:  # 盘点单 / 盘点明细(FIFO 盘盈盘亏)

显示策略:库存列表 inventory.goCOALESCE(NULLIF(p.code,''), 快照列)——优先 productproduct 为空/被删才回退快照。前端 lineOrProduct 同理。历史导入的出入库明细 product_id 多指向一个占位 productHIST-PLACEHOLDER),真实信息存快照列。

其它主表

shops:               # 门店(租户根节点)
users:               # 用户(含 shop_idrole: 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 + 图片)

关键业务规则

  1. 多租户隔离shop_id 只从 JWT 提取(middleware.GetShopID(c)),绝不从请求参数读取
  2. product = 序列号:入库每行新建独立 product(不按名称复用);product 是商品信息单一来源,库存/明细只是指向它的引用 + 历史快照(见上节)
  3. 库存变更事务:入库/出库审核时,同一事务内更新 inventories + 写 inventory_logs
  4. 出库前校验:审核出库时校验库存充足,不足返回错误并回滚
  5. 单据状态机与撤回draft→submit→pending→approve/reject;审核中(pending)可 withdraw 回 draft 再改再提交——管理员/超管撤回任意单,操作员限本人单(OperatorID==本人handler 内判权);已审核(approved)只读
  6. 单号生成:通过 number_rules 表事务安全生成,格式 {前缀}{YYYYMMDD}{6位序号}
  7. 权限角色superadmin(超管,可清空数据/看反馈)> admin(管理员,管用户/店铺信息)> operator(操作员)> readonly(只读,所有写操作 403)。middleware.ReadOnly() 全局挂载、AdminOnly()(admin+superadmin) / SuperAdminOnly() 按需挂载
  8. 并发安全
    • 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 + _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 统一管理,支持构建时注入:

// 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 规范(ds 真相源,2026-07 起)

列表屏统一 DsTableclient/lib/widgets/ds/ds_table.dart):toolbar + 表格 + pager 连成一卡, 镜像原型 .toolbar/.table/.pager。列定义 DsColumn;可筛选列走列头漏斗(filtered 态主色高亮); 可隐藏列走列设置菜单;分页 total 带控件 / pagerInfoText 仅文案。行 hover、表头底色等 全部来自 context.tokens(禁硬编码色,闸:node client/tool/check_ds_code.mjs)。 其余原子(按钮/输入/徽章/chip/分段/KPI/菜单/toast/柱状图)见 client/lib/widgets/ds/ 一对一镜像 design/prototype/atoms.css。旧 DataTableCard/FilterableColumnHeader 体系已废弃(2026-07-03),勿在新屏使用。

移动端 / 响应式 UI 规范

客户端同一套代码跑桌面/Web/平板/手机。窄屏(手机)自动切换为移动布局,宽屏保持桌面布局不变

断点判定

统一用 client/lib/core/responsive/responsive.dart 的扩展,不要散落魔法数:

context.isMobile          // 宽度 < 600kMobileBreakpoint
context.dialogWidth(720)  // 固定宽度弹窗的安全宽度,≤ 屏宽 92%

列表 → 卡片

列表屏用 DataTableCard 时同时传 mobileCards,窄屏渲染卡片流、宽屏仍表格:

DataTableCard(
  columns: [...], rows: [...],          // 宽屏表格
  mobileCards: items.map(_buildCard).toList(),  // 窄屏卡片(MobileListCard
)

卡片复用 MobileListCard(标题/徽章/字段/操作)+ MobileCardFieldwidgets/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→pushCI(Forgejo) 按 tag 前缀触发对应 workflow 自动编译/测试/发 Release/部署 ali/Telegram 通知。测试未过禁止发版

生产环境与运维

  • 生产:阿里云 ECSssh ali,北京;2026-07-02 从 EC2 割接)。入口 https://jiu.51yanmei.comnginx 443 ssl+http2certbot webroot 自动续期),反代 127.0.0.1:8081 的 systemd jiu.serviceMySQL 在容器 jiu_mysql127.0.0.1:3306)。配置 /opt/jiu/config/production.envDATABASE_DSN、SERVER_PORT=8081、STORAGE_PUBLIC_URL)。每日 DB 备份 → 开发机 ~/jiu-db-backups
  • CI runnermac runner=开发者本机(launchd + relay 绕 Shadowrocket,有看门狗自愈)、windows runner=另一台机(nssm 服务 forgejo-runner)、ubuntu=NAS docker。Forgejo 在 NASgit.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/index.html 全部文档单一入口
用户手册 docs/manual/user-manual.html 酒行员工操作手册(HTML 主版本;官网帮助页源=web/content/docs.md
开发手册 docs/manual/dev-manual.html 架构/后端/前端/数据库/API/运维/发版 全景
DB 列级文档 docs/db-schema.html 数据库 Schema 可视化
用户手册(旧) docs/user-manual.md 已迁移至 HTML 版,停止更新
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 安装、平板布局等)