05203a9b5b
Design Source Checks / design-source (push) Failing after 14m23s
- 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
409 lines
25 KiB
Markdown
409 lines
25 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)
|
||
│ │ └── 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 的引用 + 历史快照)
|
||
|
||
```yaml
|
||
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.go` 用 `COALESCE(NULLIF(p.code,''), 快照列)`——**优先 product,product 为空/被删才回退快照**。前端 `lineOrProduct` 同理。历史导入的出入库明细 `product_id` 多指向一个占位 product(`HIST-PLACEHOLDER`),真实信息存快照列。
|
||
|
||
### 其它主表
|
||
|
||
```yaml
|
||
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 + 图片)
|
||
```
|
||
|
||
## 关键业务规则
|
||
|
||
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 <命令>` 执行:
|
||
|
||
```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 接口(以 `router/router.go` 为准)
|
||
|
||
```yaml
|
||
无需 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` 统一管理,支持构建时注入:
|
||
|
||
```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 规范(ds 真相源,2026-07 起)
|
||
|
||
列表屏统一 `DsTable`(`client/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` 的扩展,不要散落魔法数:
|
||
```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) |
|
||
|
||
版本清单 `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/部署 ali/Telegram 通知。**测试未过禁止发版**。
|
||
|
||
### 生产环境与运维
|
||
|
||
- 生产:阿里云 ECS(`ssh ali`,北京;2026-07-02 从 EC2 割接)。入口 `https://jiu.51yanmei.com`(nginx 443 ssl+http2,certbot webroot 自动续期),反代 `127.0.0.1:8081` 的 systemd `jiu.service`;MySQL 在容器 `jiu_mysql`(127.0.0.1:3306)。配置 `/opt/jiu/config/production.env`(DATABASE_DSN、SERVER_PORT=8081、STORAGE_PUBLIC_URL)。每日 DB 备份 → 开发机 `~/jiu-db-backups`。
|
||
- 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/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 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 安装、平板布局等) |
|