diff --git a/CLAUDE.md b/CLAUDE.md index 6d1e731..6bea0d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,12 @@ **开始任何任务前,先读 `docs/context/project.md`** 了解项目全貌。 +**文档地图**(入口 `docs/index.html`): +- 用户使用手册:`docs/manual/user-manual.html`(官网帮助页精简版源=`web/content/docs.md`,两者同步改) +- 开发手册(架构/后端/前端/数据库/API/运维/发版):`docs/manual/dev-manual.html` +- 数据库列级文档:`docs/db-schema.html`(schema 变更时同步) +- 设计契约与像素闸:`design/CONTRACT.md` + `tools/screens.mjs` + --- ## Orchestrator:如何自动选择 Agent @@ -248,10 +254,10 @@ cd client && flutter test - 使用 `dart:io` 的 `Platform` 类前,必须先检查 `kIsWeb`(`import 'package:flutter/foundation.dart'`) - Web 平台不支持 `dart:io`,直接使用会在 Web 构建时崩溃 -### 表格列头筛选 -- 可筛选列使用 `FilterableColumnHeader`(来自 `widgets/multi_select_dropdown.dart`) -- 不要在 toolbar 放独立的筛选按钮,筛选入口应内嵌在列头 -- 列定义使用 `ColDef`,可隐藏列用 `ColumnToggleButton` 控制 +### 表格与列头筛选(ds 真相源) +- 列表屏统一 `DsTable`(`widgets/ds/ds_table.dart`:toolbar+表格+pager 连成一卡),列定义 `DsColumn`,可筛选列走其列头漏斗(`filtered` 态高亮),可隐藏列走列设置菜单 +- 不要在 toolbar 放独立筛选按钮,筛选入口内嵌列头或工具栏 `DsChip` +- 旧 `FilterableColumnHeader`/`multi_select_dropdown.dart` 仅存量引用,勿在新屏使用 ### 响应式 / 移动端适配 - 断点判定统一用 `context.isMobile`(`client/lib/core/responsive/responsive.dart`,宽度 < 600 为窄屏/手机),**禁止**在各处散落 `MediaQuery.width < 600` 这类魔法数 @@ -290,7 +296,7 @@ cd client && flutter test ``` 执行顺序:本地 build → test(按 part)→ 更新对应 CHANGELOG → git commit → tag `-v` → push main+tag。 -CI/CD(Forgejo)按 tag 前缀触发对应 workflow,自动:编译 → 测试 → 创建 Release → 部署 EC2 → Telegram 通知。 +CI/CD(Forgejo)按 tag 前缀触发对应 workflow,自动:编译 → 测试 → 创建 Release → 部署 ali(jiu.51yanmei.com,443)→ Telegram 通知。 **归属与解耦**(关键): - `version.yaml` 归 **client**(写 version/build_number/release_notes/下载链接/changelog);nginx/systemd 归 **server**。 diff --git a/docs/context/project.md b/docs/context/project.md index 86f9796..e876be8 100644 --- a/docs/context/project.md +++ b/docs/context/project.md @@ -107,9 +107,10 @@ inventories: # 库存批次:每行 = 某 product 在某仓库的一批 # 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: # 出库明细(同上) +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 盘盈盘亏) ``` @@ -319,44 +320,15 @@ flutter build web --dart-define=BASE_URL=https://api.example.com **规则**:所有硬编码 `localhost:8080` 的 URL 必须改用 `AppConfig` 对应属性,不得直接拼接字符串。 -## 前端表格 UI 规范 +## 前端表格 UI 规范(ds 真相源,2026-07 起) -所有数据表格使用 `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` — 屏幕宽度不足时自动隐藏 +列表屏统一 `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/`, +一对一镜像 `.superpowers/prototype/atoms.css`。旧 `DataTableCard`/`FilterableColumnHeader` +体系已废弃(2026-07-03),勿在新屏使用。 ## 移动端 / 响应式 UI 规范 @@ -408,11 +380,11 @@ DataTableCard( | **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 [version]` slash command:本地 build→test→更新 CHANGELOG→commit→tag→push;CI(Forgejo) 按 tag 前缀触发对应 workflow 自动编译/测试/发 Release/部署 EC2/Telegram 通知。**测试未过禁止发版**。 +用 `/release [version]` slash command:本地 build→test→更新 CHANGELOG→commit→tag→push;CI(Forgejo) 按 tag 前缀触发对应 workflow 自动编译/测试/发 Release/部署 ali/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)。 +- 生产:阿里云 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 错指)。 @@ -425,7 +397,11 @@ DataTableCard( | Schema | backend/schema/schema.sql | 完整数据库建表 SQL | | S001 种子 | backend/seeds/S001.sql | 门店 S001 测试数据(含完整入库/出库/库存历史) | | S002 种子 | backend/seeds/S002.sql | 门店 S002 测试数据(基础数据相同,无库存/单据,模拟新门店) | -| 用户手册 | docs/user-manual.md | 酒行员工操作手册(登录/入库/出库/库存/财务/设置) | +| 文档索引 | 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 部署说明 | diff --git a/docs/db-schema.html b/docs/db-schema.html index 21cb6ee..0cc6902 100644 --- a/docs/db-schema.html +++ b/docs/db-schema.html @@ -58,7 +58,7 @@
+

license_purchases 在线购买/续费记录

+
pay 收款中枢:契约见 ~/code/pay-contract(v1.0.0)。out_trade_no = pay 订单号,兼作对账键与幂等键(同一单只续期一次);amount 为 pay 下单响应回传的权威金额,webhook 回调时逐分核对。
+ + + + + + + + + + + + + + + +
列名类型可空默认说明
id PKBIGINT UNSIGNEDAUTO_INC
shop_id 租户BIGINT UNSIGNED
user_idBIGINT UNSIGNED下单管理员
product_biz_codeVARCHAR(64)套餐稳定码:monthly_standard / annual_standard / monthly_pro / annual_pro
amountVARCHAR(16)NULLpay 下单回传金额,如 2999.00
out_trade_noVARCHAR(64)NULLpay 订单号(对账+幂等键)
statusENUM('pending','paid','failed')pending
trade_noVARCHAR(64)NULL渠道交易号(支付宝/微信)
channelVARCHAR(16)NULL
paid_atDATETIMENULL
created_atDATETIMECURRENT_TIMESTAMP
updated_atDATETIMECURRENT_TIMESTAMP
deleted_atDATETIMENULL软删除
+
PRIMARY (`id`)
UNIQUE uk_out_trade_no (`out_trade_no`)
INDEX idx_shop (`shop_id`)
INDEX idx_status (`status`)

商品主数据

products 每行 = 一个特有产品/序列号(非 SKU),是商品信息的单一来源。基础数据字典为入库选文本服务。

products 商品

单一来源铁律:批次号 batch_no、生产日期 production_date、进价、图片、public_id 均以此表为准。每行 = 一个序列号(uk_product_code 同店唯一)。
diff --git a/docs/index.html b/docs/index.html index 23ca352..ff6d2d0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -27,34 +27,53 @@

酒库管理系统 · 文档索引

项目所有文档的单一入口。新增文档须同时登记到此处。
-

设计方案 / 原型

+

📖 手册

+ +

🏗 设计方案

+ + +

📚 知识库 · 调研

+ + +

🔧 Runbook · 排障

+ + +

🧪 测试 · 审计报告

+

实现计划

  • (暂无)
- -

排障 / 运维 Runbook

- - -

知识库 / 调研 / 测试

- - -

历史 Markdown 文档(逐步迁移)

- diff --git a/docs/manual/dev-manual.html b/docs/manual/dev-manual.html new file mode 100644 index 0000000..b33bacd --- /dev/null +++ b/docs/manual/dev-manual.html @@ -0,0 +1,669 @@ + + + + + +开发手册 — 岩美酒库 + + + +
+

岩美酒库 · 开发手册

+
面向后续开发者/维护者 · server v1.1.1 / client v1.1.1 · 更新于 2026-07-03 · 来源以仓库代码为准(backend/ client/ deploy/ scripts/
+ + + + +

1. 系统架构总览

+

五端 Flutter 客户端统一访问 https://jiu.51yanmei.com。阿里云主机上 nginx 1.24 终结 TLS(443 ssl+http2,certbot webroot 自动续期,80 → 443 跳转),按 location 分发:API/健康检查/版本反代到本机 8081 的 Go 后端(systemd jiu.service),静态资源(Flutter Web /app、商品图 /images、安装包 /downloads、营销站根路径)直接由 nginx 服务;/product/:id/app/product/:id(扫码分享页)回代后端注入 OG 标签。后端连本机 Docker 容器 jiu_mysql(MySQL 8,127.0.0.1:3306)。未鉴权的 /api/v1/(public|auth)/ 在 nginx 层再加一道 per-IP 限流(10r/s burst 20)。源:deploy/nginx-jiu-ali.confdeploy/jiu.service

+ +
+ + + + + + + + + Windows + macOS + Web (/app) + Android + iOS + + + + + + + https://jiu.51yanmei.com + + + ali · nginx 1.24 — 443 ssl+http2(certbot webroot 自动续期)· 80 → 443 + + + 静态:/app(Flutter Web) + /images · /downloads · / 营销站 + + 反代:/api /health /version + → 127.0.0.1:8081 + + 限流反代:/api/v1/(public|auth) + 10r/s · burst 20(per-IP) + + /product · /app/product + OG 注入 → 回代后端 + 证书 /etc/letsencrypt/live/jiu.51yanmei.com/ · client_max_body_size 20m · /import 超时 300s · 安全头 HSTS/XFO/nosniff + + + proxy_pass 127.0.0.1:8081 + + + jiu-server(Go / Gin)— systemd jiu.service + /opt/jiu/backend/jiu-server · EnvironmentFile=/opt/jiu/config/production.env(SERVER_PORT=8081) + + + + MySQL 8 — docker 容器 jiu_mysql + 127.0.0.1:3306 · 库 jiu_db · 每日备份见 §6 + +
+ +
阿里机 8080 已被 pay 项目 payd 占用,后端端口固定 8081;本地开发默认仍是 8080。改 nginx 时只动 jiu 的 server block(/etc/nginx/conf.d/jiu.conf),别碰同机其他站点。
+ + +

2. 后端(Go / Gin)

+ +

2.1 目录分层

+
+ + + + + + + + + + + + + + + +
目录 / 文件职责
main.go入口:配置加载、生产前置检查(CORS/JWT 密钥)、连库、AutoMigrate、启动回填、SetTrustedProxies、挂路由
cmd/一次性/运维工具(seed、gencode、import-history、fix-inventory-products、backfill-pinyin,详见 §6.4)
config/Viper 配置结构体 + config.yaml(本地,不入 git)+ version.yaml(发版清单,/version 实时读)
internal/handler/HTTP 处理器,每业务模块一个文件;参数校验 + 组织响应,*_test.go 为 SQLite in-memory 集成测试
internal/service/业务逻辑层:auth(登录/失败锁定)、license(兑换券)、stock(审核/库存事务)、migrate(启动回填)、session_cleanup
internal/model/GORM 模型;base.go 提供 Base/TenantBase/Date/JSON 公共类型
internal/middleware/JWT 鉴权 + 会话校验、角色拦截(ReadOnly/AdminOnly/SuperAdminOnly)、LicenseGuard、限流
internal/util/通用工具(pinyin.go 拼音搜索列生成等)
internal/router/router.go全部路由注册与中间件编排(§5 的唯一真相源)
schema/schema.sql完整建表 DDL(新装用;线上结构演进靠 AutoMigrate)
seeds/门店种子数据 SQL(S001 全量测试数据、S002 空门店)
testutil/setup.go测试工具:SQLite in-memory DB、CreateTestXxx 工厂、GetAuthToken
+ +

2.2 JWT 与多租户隔离

+

源:backend/internal/middleware/auth.go。JWT(HS256,Access 60 分钟 / Refresh 7 天)Claims 携带 user_id / shop_id / role / sid / lic_exp。带 sid 的 token 每请求校验 user_sessions(撤销/禁用即时下线,last_seen_at 30s 节流刷新)。

+
铁律:shop_id 永远用 middleware.GetShopID(c) 从 JWT 取,绝不从请求参数/URL/请求体读;所有查询必须带 WHERE shop_id = ?。违反即多租户越权。
+ +

2.3 角色中间件

+
+ + + + + + +
中间件挂载位置行为
ReadOnly()业务路由组全局 + /licenserole=readonly 的非 GET 请求 → 403(code: READONLY_USER
AdminOnly()/users 组、/shop 写、/sessions/:id 删admin / superadmin 放行
SuperAdminOnly()/admin 组superadmin 放行(清数据/对账/错误报告/反馈后台)
+

角色顺序:superadmin > admin > operator > readonly。新增写路由默认已受 ReadOnly 保护;仅管理员可用的要再包 AdminOnly 子组。

+ +

2.4 LicenseGuard(授权过期降级)

+

源:middleware/license_guard.go。按当前 DB 的有效授权实时算 phase(每店 30s 缓存,激活/续费调用 InvalidateLicensePhase 即时生效),不信任登录时 token 快照:

+
+ + + + + + + +
phase条件效果
normal未过期 / 永久授权正常读写
grace过期 0–7 天可写,前端展示提醒横幅
readonly过期 7–15 天非 GET → 403(phase: readonly
locked过期 15+ 天,或授权全部被停用(吊销)所有业务请求 403(phase: locked
+

/license/* 与 登出/心跳/在线会话 挂在 LicenseGuard 之外——锁定时仍能查看授权状态并激活新码。

+ +

2.5 限流

+

源:middleware/ratelimit.go(进程内令牌桶 + janitor 清理,config.C.RateLimit.Enabled=false 可整体关闭)。两个维度:

+
    +
  • per-IP(未鉴权接口逐条挂):login / refresh / register / errors / 公开商品读 / 店铺商品列表各自独立配额,IP 取自 nginx 写入的 X-Real-IP(main.go 只信任 127.0.0.1 代理,不可伪造)。
  • +
  • per-Shop(挂在 JWT 之后,作用于全部已鉴权路由):按 shop_id 限 RPS+burst,防单店打爆共享后端。
  • +
+

超限返回 429 + Retry-After: 60。nginx 层对 /api/v1/(public|auth)/ 还有最外层 10r/s 兜底(§1)。

+ +

2.6 单据审核流状态机

+

入库单 / 出库单同一状态机(service/stock.go + handler/stock_in.gostock_out.go)。审核通过时同一事务内:更新 inventories(FOR UPDATE 锁行)+ 写 inventory_logs + 生成财务应收/应付;出库先校验库存充足,不足回滚。

+
+ + + + + + + + draft 草稿 + pending 审核中 + approved 已审核 + rejected 已拒绝 + + + + + PUT /:id/submit + + + withdraw 撤回(管理员任意单 / 操作员限本人单) + + + approve 审核通过 + + + reject 拒绝 + + + + approved 后续动作(POST,service 内判权): + return 退单(冲库存+冲应收/应付) · confirm-cost 确认进价(入库) + confirm-sale 确认售价(出库,重算应收/利润) + + draft 可改/可删 + + +
+ +

2.7 AutoMigrate 与启动回填

+
    +
  • AutoMigrate(main.go):启动时对 31 个 model 只增不删地同步表结构;另幂等显式建 uk_shop_code(products) 唯一索引与 idx_so_items_shop_code(stock_out_items)(嵌入字段无法用 struct tag 表达)。
  • +
  • BackfillPricingColumnsservice/migrate.go):2026-07 定价字段消歧的一次性迁移,旧列(unit_price/total_price/total_amount)值拷入新列(cost_price/cost_amount/cost_total/sale_total/sale_amount/profit_total),幂等(新列为 0 才拷),旧列观察一版后手动 DROP;全新安装(无旧列)整体跳过。
  • +
  • backfillPartnerPinyin(main.go):为 name_pinyin 为空的存量往来单位补拼音搜索列;products 同机制由 handler 写入时生成 + 启动兜底。
  • +
+ +

2.8 拼音搜索

+

源:internal/util/pinyin.go(go-pinyin)。ToPinyin(name) 返回全拼 + 首字母,Create/Update 时自动写入 name_pinyin / name_initials 两列;搜索 SQL 同时 LIKE 匹配 name、code、name_pinyin、name_initials(汉字/全拼/首字母三种输入都命中)。新增需要拼音搜索的实体直接复用 ToPinyin()

+ +

2.9 客户端异常上报

+

客户端 POST /api/v1/public/errors(per-IP 限流)→ error_reports 表;超管在 GET /api/v1/admin/errors 查看。前端两处统一捕获(main.dart 的 runZonedGuarded + Dio 拦截器上报 5xx),业务代码只在技术性异常处手动 reportError(e, st),已知业务错误(AppException)不上报。

+ + +

3. 前端(Flutter)

+ +

3.1 目录分层

+
+ + + + + + + + + +
目录职责
lib/core/横切基建:api(Dio 封装,401 自动刷新)、auth(AuthNotifier + 持久化)、config(AppConfig,BASE_URL 注入)、router(go_router 登录重定向)、theme(token 三主题)、responsive、update(应用内更新)、errors(reportError)
lib/models/与后端 JSON 对应的数据模型
lib/providers/Riverpod 状态管理,每业务模块一个 provider(列表缓存/刷新/心跳等)
lib/repositories/数据访问层:封装 API 调用,供 provider 消费
lib/screens/页面(auth/shell/stock_in/stock_out/inventory/partners/finance/products/devices/settings/about/public/shared)
lib/widgets/共享组件;widgets/ds/ 为设计系统组件库(唯一样式来源,见 3.3)
+ +

3.2 数据流

+

单向:Screen 只组合 ds 组件并 watch provider;provider 调 repository;repository 走 Dio(ApiClient 统一注入 token、401 自动刷新、5xx 自动上报)。

+
+ + + + + + + + Screenscreens/ + ds 组件 + ProviderRiverpod 状态/缓存 + RepositoryAPI 调用封装 + Dio401 刷新 · 5xx 上报 + API + + + + + + +
+ +

3.3 ds 设计系统(单一真相源)

+

UI 的真相源是原型 .superpowers/prototype/(tokens + atoms.css);Flutter 侧一对一镜像,屏只组合 ds 组件、不自己堆样式。令牌层由 lib/core/theme/token_source/tokens.css codegen 生成 app_tokens.g.dart / app_dims.g.dart / app_chrome.g.dart——改样式改源后重跑脚本,禁止手改 .g 文件、禁止硬编码色值

+
+ + + + + + + + + + +
widgets/ds/ 文件镜像对象
ds_atoms.dartatoms.css 原子组件全集(按钮/输入/徽章/卡片…),尺寸引 AppDims、颜色引 context.tokens
ds_table.dart原型 .table + .toolbar + .pager(toolbar 与表连成一卡,pager 卡外透明)
ds_kpi.dart原型 .kpi 汇总卡(图标块 .ic 五种 tone)
ds_menu.dart原型 .menu 下拉(定位规则照抄 shell.js openMenu)
ds_toast.dart原型 .toast 1:1(单例顶替、2.2s 自动消失)
ds_bar_chart.dart原型 finance.html 收支趋势分组柱状图(度量逐像素照抄)
grid_combo_cell.dart表格内联可搜索下拉单元格(入库/出库编辑网格用)
+

闸机 1 — 代码端client/tool/check_ds_code.mjs 扫描 lib/ 禁 Color(0x..)Colors.x 硬编码(合理特例需 // ds-ignore: 理由),--changed --strict 供 pre-commit。

+ +

3.4 golden × 3 主题 + fidelity 像素闸

+
    +
  • goldenclient/test/golden/*_golden_test.dart 对每屏 × 三主题(a/b/c)出基准图 test/golden/goldens/<prefix>_<theme>.png;更新用 flutter test --update-goldens
  • +
  • 闸机 2 — fidelitytools/fidelity.mjs原型截图当基准(Playwright 截 .superpowers/prototype/screens/*.html,注入 Noto Sans SC 统一字体)与 Flutter golden 做 pixelmatch diff,超逐屏校准阈值即 exit 1;支持 zones 分区阈值抓局部错位。屏注册表在 tools/screens.mjs(fidelity 与人工目检 tools/ds-compare.mjs 共用)。
  • +
+
cd client && flutter test --update-goldens test/golden/inventory_list_golden_test.dart
+node tools/fidelity.mjs inventory          # 仓库根运行;--themes a,b,c;--update 刷新原型基准
+ +

3.5 响应式

+

断点统一 context.isMobile(宽 < 600,core/responsive/responsive.dart),弹窗宽度 context.dialogWidth(X)(≤ 屏宽 92%)。列表屏 DataTableCard 必传 mobileCards(窄屏卡片流),窄屏导航为 Drawer 抽屉。平台判断先 kIsWebdart:io Platform。后端 URL 一律 AppConfig--dart-define=BASE_URL=... 注入),禁止硬编码。

+ + +

4. 数据库

+

MySQL 8 · 所有业务表含 shop_id 租户隔离列。schema.sql 定义 26 张表;另有 5 张仅由 AutoMigrate 创建(4 张商品属性字典 + error_reports),共 31 张。完整列级文档见 db-schema.html

+ +

4.1 分组速览

+
+ + + + + + + + + + + + +
分组要点
租户与账号shops
users
user_sessions
login_attempts
shops=租户根(code 唯一);users 角色 ENUM 四档、uk_shop_username;user_sessions 支撑 sid 会话校验/踢人(revoked_at、last_seen_at);login_attempts 记失败登录供锁定
授权licenses
license_devices
license_codes
时长兑换券模型:license_codes 为平台码池(短码),激活兑入 licenses(expires_at 驱动 LicenseGuard),license_devices 记设备绑定
商品与字典products
product_categories
product_images
product_*_options ×6
product_description_docs
product = 特有产品/序列号(非 SKU):code 同店唯一(uk_shop_code),name/series/spec=品牌/型号/版本,含 name_pinyin/name_initials 搜索列、public_id 公开页;字典表(名称/系列/规格/产地/保质期/储存/介绍)供入库选取后建 product
仓库与往来warehouses
partners
partners.type=supplier/customer,含拼音搜索列
入库stock_in_orders
stock_in_items
单据状态机(§2.6)、uk_order_no(shop_id,order_no);明细含快照列 + cost_price/cost_amount + 批次/生产日期/有效期
出库stock_out_orders
stock_out_items
同状态机;单头 sale_total/profit_total;明细 cost_price(成本快照)+ sale_price/sale_amount;idx_so_items_shop_code 支撑按编码反查
库存inventories
inventory_logs
inventory_checks
inventory_check_items
inventories=批次行(每行一个入库批,stock_in_item_id 溯源,idx_fifo 支撑先进先出);logs 记每次变动 qty_before/after + ref;盘点单 draft→completed
财务finance_recordstype=receivable/payable/receipt/payment,balance=操作后余额,status=open/closed,ref_type/ref_id 关联单据
其他number_rules
feedbacks
error_reports
number_rules 每店每类型一行(uk_shop_type),单号=前缀+日期+6 位序号;feedbacks 意见反馈;error_reports 客户端异常
+ +

4.2 定价字段口径(2026-07 消歧)

+
+ + + + + + + + + + + + +
新列旧列(弃用)口径
stock_in_items.cost_priceunit_price进价 · 单瓶
stock_in_items.cost_amounttotal_price总进价 = quantity × cost_price
stock_in_orders.cost_totaltotal_amount应付合计 = Σ 明细 cost_amount
stock_out_items.cost_priceunit_price成本单价(入库成本快照)
stock_out_items.cost_amounttotal_price成本小计 = quantity × cost_price
stock_out_items.sale_price—(新增)实际销售单价,0 = 待定价
stock_out_items.sale_amount—(新增)售价小计 = quantity × sale_price(待定价 = 0)
stock_out_orders.sale_totaltotal_amount应收合计 = Σ 明细 sale_amount
stock_out_orders.profit_total—(新增)总利润 = Σ(sale_price>0 ? (sale_price−cost_price)×qty : 0),建单落库,确认售价/进价联动重算
+
行利润只统计已定价行(sale_price>0);待定价行不计利润也不计应收。旧列已停止读写,由启动回填拷值(§2.7),观察一版后手动 DROP——期间新代码引用旧列。
+ +

4.3 关键机制

+
    +
  • 批次行 + 快照列inventories 每行 = 某 product 在某仓的一个入库批;出入库明细与库存行都带 product_code/product_name/series/spec/... 快照列(导入/审核时拷贝),显示优先 product、回退快照(COALESCE / 前端 lineOrProduct)。快照列保留,勿删。
  • +
  • 生成列inventory_check_items.diff_qtyGENERATED ALWAYS AS (actual_qty - system_qty) STORED,不可写。
  • +
  • FOR UPDATE 并发锁:单号生成(number_rules)、库存扣减(inventories)等 check-then-act 一律事务内锁行;products.codeuk_shop_code 时 max+1 重试(最多 5 次)。
  • +
  • custom_fields JSON:新业务字段优先放 JSON 扩展列,不轻易加物理列。
  • +
+ + +

5. API 参考

+

逐条核对自 backend/internal/router/router.go。权限图例:公开 无需登录(多带 per-IP 限流);JWT 登录即可(readonly 角色写操作被 ReadOnly 拦);Admin = AdminOnly;Super = SuperAdminOnly;限流 = 独立 IP 限流。所有 JWT 路由另受 per-Shop 限流;业务组(商品及以下)还受 LicenseGuard(§2.4)。

+ +

5.1 根路由 / 认证 / 公开接口

+
+ + + + + + + + + + + + + +
MethodPath说明权限
GET/health健康检查(连通性探测)公开
GET/version版本清单(实时读 version.yaml)公开
GET/product/:public_id扫码分享页(OG 标签注入的 index.html)公开 限流
POST/api/v1/auth/login登录(失败锁定 + IP 限流双保险)公开 限流
POST/api/v1/auth/refresh刷新 token公开 限流
GET/api/v1/public/products/:public_id公开商品详情(扫码页数据)公开 限流
GET/api/v1/public/shops/:shop_code/products店铺公开商品列表(限流最紧)公开 限流
GET/api/v1/public/release版本 + 下载链接 + changelog(官网/更新用)公开 限流
POST/api/v1/public/errors客户端异常上报 → error_reports公开 限流
POST/api/v1/public/register门店注册公开 限流
+ +

5.2 会话 / 授权(豁免 LicenseGuard)

+
+ + + + + + + + + + + + + + + +
MethodPath说明权限
POST/api/v1/auth/logout登出(撤销会话)JWT
POST/api/v1/auth/ping在线心跳JWT
GET/api/v1/sessions在线会话列表JWT
DELETE/api/v1/sessions/:id强制下线Admin
GET/api/v1/license/info授权状态(与 LicenseGuard 同一取数口径)JWT
POST/api/v1/license/activate兑换/激活授权码(锁定期仍可用)JWT
GET/api/v1/license/verify校验当前授权JWT
POST/api/v1/license/deactivate解绑设备JWT
GET/api/v1/license/devices已绑定设备列表JWT
POST/api/v1/license/purchase在线购买/续费下单,返回 pay 收银台 pay_url(契约 pay-contract v1.0.0)Admin
GET/api/v1/license/purchase/:out_trade_no购买单状态(支付结果页轮询,本店可见)JWT
POST/api/v1/pay/callbackpay 支付成功 webhook(HMAC 验签+幂等+金额核对后续期)公开
+ +

5.3 商品 / 仓库 / 往来单位

+
+ + + + + + + + + + + + + + + + + + + + + +
MethodPath说明权限
GET/api/v1/products商品列表(汉字/全拼/首字母/编码搜索)JWT
POST/api/v1/products新建商品(独立序列号,code 自增 max+1)JWT
POST/api/v1/products/find-or-create按编号找/建 product(导入用;禁止按名称合并)JWT
GET/api/v1/products/:id/detail商品详情(含图片/字典属性)JWT
GET/api/v1/products/:id/price-history进价/售价历史JWT
GET/api/v1/products/:id/qrcode公开页二维码JWT
PUT/api/v1/products/:id更新商品JWT
DELETE/api/v1/products/:id删除商品(软删)JWT
POST/api/v1/products/:id/images上传商品图片JWT
DELETE/api/v1/products/:id/images/:image_id删除商品图片JWT
GET/api/v1/warehouses仓库列表JWT
POST/api/v1/warehouses新建仓库JWT
PUT/api/v1/warehouses/:id更新仓库JWT
DELETE/api/v1/warehouses/:id删除仓库JWT
GET/api/v1/partners往来单位列表(?type&keyword,拼音搜索)JWT
POST/api/v1/partners新建往来单位JWT
PUT/api/v1/partners/:id更新往来单位JWT
DELETE/api/v1/partners/:id删除往来单位JWT
+ +

5.4 入库 / 出库

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodPath说明权限
GET/api/v1/stock-in/orders入库单列表(?status&keyword,编码双路匹配)JWT
GET/api/v1/stock-in/summary入库汇总(KPI 卡)JWT
GET/api/v1/stock-in/orders/:id入库单详情JWT
POST/api/v1/stock-in/orders创建入库单(每明细行新建独立 product)JWT
PUT/api/v1/stock-in/orders/:id更新(仅 draft)JWT
DELETE/api/v1/stock-in/orders/:id删除(仅 draft)JWT
PUT/api/v1/stock-in/orders/:id/submit提交审核 draft→pendingJWT
PUT/api/v1/stock-in/orders/:id/approve审核通过(事务:+库存 +流水 +应付)JWT
PUT/api/v1/stock-in/orders/:id/reject拒绝JWT
PUT/api/v1/stock-in/orders/:id/withdraw撤回 pending→draft(管理员任意/操作员本人)JWT
POST/api/v1/stock-in/orders/:id/return退单(库存删除 + 冲应付;service 内判管理员)Admin
POST/api/v1/stock-in/orders/:id/confirm-cost确认进价(暂估 0 价→真实价,前向补偿)Admin
GET/api/v1/stock-out/orders出库单列表(?status&keyword)JWT
GET/api/v1/stock-out/summary出库汇总JWT
GET/api/v1/stock-out/orders/:id出库单详情JWT
POST/api/v1/stock-out/orders创建出库单JWT
PUT/api/v1/stock-out/orders/:id更新(仅 draft)JWT
DELETE/api/v1/stock-out/orders/:id删除(仅 draft)JWT
PUT/api/v1/stock-out/orders/:id/submit提交审核JWT
PUT/api/v1/stock-out/orders/:id/approve审核通过(校验库存充足,事务扣减 FIFO)JWT
PUT/api/v1/stock-out/orders/:id/reject拒绝JWT
PUT/api/v1/stock-out/orders/:id/withdraw撤回 pending→draftJWT
POST/api/v1/stock-out/orders/:id/return退单(库存加回 + 冲应收;管理员或本人)JWT
POST/api/v1/stock-out/orders/:id/confirm-sale确认售价(先出后定价,重算应收/利润)Admin
+ +

5.5 库存 / 财务 / 用户 / 店铺 / 反馈 / 编号规则

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
MethodPath说明权限
GET/api/v1/inventory库存列表(?warehouse_id&keyword&series&spec&in_stock)JWT
GET/api/v1/inventory/summary库存汇总JWT
GET/api/v1/inventory/logs库存流水JWT
PUT/api/v1/inventory/:id/remark更新库存行备注JWT
POST/api/v1/inventory/checks创建盘点单JWT
GET/api/v1/inventory/checks/:id盘点单详情JWT
PUT/api/v1/inventory/checks/:id/complete完成盘点(盘盈盘亏落库)JWT
GET/api/v1/finance/records财务流水列表(含日期区间)JWT
POST/api/v1/finance/records手工记账(收款/付款)JWT
PUT/api/v1/finance/records/:id/close结清单条应收/应付JWT
PUT/api/v1/finance/records/close-by-ref按关联单据结清JWT
GET/api/v1/finance/summary财务汇总JWT
GET/api/v1/finance/trend收支趋势(柱状图数据)JWT
GET/api/v1/users用户列表Admin
POST/api/v1/users新建用户Admin
PUT/api/v1/users/:id更新用户(角色/启用)Admin
PUT/api/v1/users/:id/reset-password重置密码Admin
GET/api/v1/shop/info店铺信息JWT
PUT/api/v1/shop/info更新店铺信息Admin
POST/api/v1/shop/logo上传店铺 logoAdmin
POST/api/v1/feedback提交意见反馈JWT
POST/api/v1/feedback/images上传反馈附图JWT
GET/api/v1/number-rules单号规则列表JWT
PUT/api/v1/number-rules/:id更新单号规则JWT
+ +

5.6 数据导入(Excel)

+
+ + + + + + + + + + + + +
MethodPath说明权限
POST/api/v1/import/products导入商品JWT
POST/api/v1/import/partners导入往来单位JWT
POST/api/v1/import/product-names导入名称字典JWT
POST/api/v1/import/product-series导入系列字典JWT
POST/api/v1/import/product-specs导入规格字典JWT
POST/api/v1/import/product-codes导入商品编码JWT
POST/api/v1/import/stock-in导入历史入库单JWT
POST/api/v1/import/stock-out导入历史出库单JWT
POST/api/v1/import/inventory导入库存(按编号建 product,禁按名称合并)JWT
+ +

5.7 基础数据字典(/api/v1/product-options)

+

八组字典同构:每组 GET(支持 ?keyword 服务端搜索)/ POST / PUT /:id / DELETE /:id,共 32 条路由,权限均为 JWT

+
+ + + + + + + + + + + +
子路径字典
/names商品名称(品牌)
/series系列(型号)
/specs规格(版本)
/categories香型 / 分类
/origins产地
/shelf-lives保质期
/storages储存方式
/description-docs商品介绍文档
+ +

5.8 超级管理员(/api/v1/admin)

+
+ + + + + + + + +
MethodPath说明权限
POST/api/v1/admin/clear-data清空本店业务数据Super
GET/api/v1/admin/reconcile库存对账(inventories vs logs)Super
GET/api/v1/admin/errors客户端异常报告列表Super
GET/api/v1/admin/feedback意见反馈列表Super
PATCH/api/v1/admin/feedback/:id更新反馈处理状态Super
+ + +

6. 数据操作与运维

+ +

6.1 Schema 管理

+

backend/schema/schema.sql = 新装环境的完整 DDL(docker 初始化时执行);线上结构演进只靠启动时 AutoMigrate(只增不删),没有独立迁移文件。表结构变更 = 同步改 schema.sql + 对应 model。改生产库数据前先备份

+ +

6.2 种子数据

+
sh scripts/dev.sh seed S001    # 清空并写入 S001 全量测试数据(docker exec 注入 MySQL 容器)
+sh scripts/dev.sh seed S002    # 空门店(基础数据相同,无单据/库存)
+

每店一个 backend/seeds/<shop_code>.sql,文件顶部含 TRUNCATE,每次完整重建。

+ +

6.3 每日备份

+

.gitea/workflows/backup.yml:每日北京时间 02:00(cron UTC 18:00)在 mac runner(开发者本机)跑 scripts/ci/backup-db.sh——SSH 到阿里主库机 dump MySQL,落到本机 ~/jiu-db-backups,Telegram 通知结果;也可 workflow_dispatch 手动触发(外网 nginx 挡 API 触发,需 Forgejo UI 操作)。

+ +

6.4 一次性工具(backend/cmd/)

+
+ + + + + + + + +
工具用途
cmd/seed初始化/重置测试数据(--reset 删表重建、--clear 清数据)
cmd/gencode平台方批量生成授权兑换券写入码池:go run ./cmd/gencode -type annual -days 365 -count 100 -batch 2026-summer
cmd/import-history旧系统历史进销存一次性迁入(历史只读、4 列快照直存、不回放库存)
cmd/fix-inventory-products修复历史库存导入按「名称|系列|规格」错误合并 product 的数据(按快照编号重指)
cmd/backfill-pinyin为存量 products 补 name_pinyin/name_initials(现启动已自动兜底)
+ +

6.5 生产环境配置

+

生产 env 在 /opt/jiu/config/production.env(systemd EnvironmentFile 加载):SERVER_PORT=8081DATABASE_DSNJWT_SECRET(生产必须非默认值否则拒绝启动)、CORS origin(生产禁 *)。密钥不入 git,需要时从 Bitwarden 取。

+ + +

7. 开发环境与工作流

+ +

7.1 本地开发

+
# 首次:起 MySQL 容器(127.0.0.1:3306 + Adminer :8888)
+docker compose -f deploy/docker-compose.yml up -d
+
+# 一键起前后端(后端代码未变则跳过重启;--force 强制)
+sh scripts/dev.sh run
+sh scripts/dev.sh stop
+
+# 测试数据 + 账号(门店编号 S001,密码均 password123)
+sh scripts/dev.sh seed S001    # admin=管理员 operator=操作员 test=只读
+

Go 命令前须 export PATH="/opt/homebrew/bin:$PATH"(dev.sh 已内置)。发版前自测「指向线上后端」的 macOS 包:sh scripts/local_test.sh(BASE_URL 固定生产域名,版本取最新 client tag patch+1)。

+ +

7.2 完成标准(DoD 门禁)

+
# 后端
+cd backend && go build ./... && go vet ./... && go test ./...
+
+# 前端
+cd client && flutter analyze --no-fatal-infos --no-fatal-warnings && flutter test
+
+# 设计系统闸(改了 UI 时)
+cd client && node tool/check_ds_code.mjs --changed    # 禁硬编码色
+node tools/fidelity.mjs                               # golden vs 原型像素闸(仓库根)
+
任一项失败禁止提交、禁止打 tag 发版。通用检查:多租户隔离未破坏(查询都带 shop_id)、schema.sql 与 model 同步、新接口有测试覆盖。
+ +

7.3 Git 提交规范

+
{类型}({模块}): {简短说明}     # 类型: feat|fix|test|docs|chore|security|refactor
+                              # 模块: backend|client|db|deploy|docs
+feat(backend): 新增财务报表接口
+docs(api): 财务报表 API 接口文档
+ +

7.4 三条发版流水线

+

client / site / server 三条互不影响的流水线,各有 tag 前缀、独立版本序列、独立 CHANGELOG,由 Forgejo CI 按 tag 前缀路由(.gitea/workflows/deploy-*.yml + scripts/ci/)。用 /release <part> [version]:本地 build → test → 更新 CHANGELOG → commit → tag → push;省略版本则自增最新 tag 的 patch。回滚用 manual.yml 输入带前缀 tag,从 Forgejo Release 下载产物重部署。

+ +
+ + + + + + + + + + client-v* + CHANGELOG-client + + site-v* + CHANGELOG-site + + server-v* + CHANGELOG-server + + + + deploy-client.yml — 五端构建矩阵 + + mac runner(本机,容量 1,串行链): + build-client-web + build-macos + build-android + build-ios + + + + + windows runner(并行): + build-windows (Inno) + + + release-deploy-client + 收齐产物 → Release → 部署 + + + Android 需 ANDROID_* / iOS TestFlight 需 IOS_*·APPSTORE_* secrets,未配置优雅跳过 + + + + + deploy-site.yml — Eleventy 构建 → rsync /opt/jiu/marketing(nginx 根路径) + + + + + deploy-server.yml — go build+test → Release → 部署 jiu-server + nginx/systemd + + + + + 阿里云主机 + /opt/jiu/* + Forgejo Release + + + + + + Telegram 通知 + + + +
+ +
+ + + + + + +
part范围tag 前缀要点
clientclient/ 全平台 + version.yamlclient-v*version.yaml 归 client;后端 /version 与 /public/release 每请求实时读——client 发版立即生效,不重启后端、不重建官网
siteweb/ 营销站(不含 Web 版 app)site-v*官网下载页运行时 fetch /public/release 动态刷新版本徽章/链接/更新日志
serverbackend/ + nginx/systemd 基建server-v*nginx conf、jiu.service 归 server 流水线
+
/release client 1.1.1     # 指定版本
+/release server           # 省略则自增最新 server-v* tag 的 patch
+

CI 脚本在 scripts/ci/lib-forgejo.sh(公共函数)+ compile-{client-web,site,backend,macos,android,ios,windows}.sh + release-*.sh + deploy-*.sh。runner:mac = 开发者本机(launchd 看门狗)、windows = 另一台机(nssm)、Forgejo 在 NAS(git.51yanmei.com)。

+ +
+ +
+ + diff --git a/docs/manual/user-manual.html b/docs/manual/user-manual.html new file mode 100644 index 0000000..92cbe42 --- /dev/null +++ b/docs/manual/user-manual.html @@ -0,0 +1,465 @@ + + + + + +岩美酒库 · 用户使用手册 + + + +
+

岩美酒库 · 用户使用手册

+
适用版本:客户端 v1.1.1 · 更新日期:2026-07-03 · 受众:酒行店员 / 店长
+

岩美酒库是为酒行门店设计的进销存管理系统:进货入库、卖酒出库、库存盘点、账款往来,一套系统全搞定。每瓶酒有自己的编号和二维码标签,卖出去、退回来都能查到来龙去脉。本手册按日常使用顺序编写,新员工建议先读第 1、2 章。

+ + + + +

1. 快速上手

+
谁用:所有人 · 入口:登录页 / 顶栏右上角
+ +

1.1 注册新门店

+
+

第一次使用,先给自己的酒行开一个门店账号。在登录页点「注册新门店」,按页面提示分三步走:

+
    +
  1. 填写门店信息:店名、地址、联系人。门店编号由系统自动分配,不用自己起。
  2. +
  3. 创建管理员账号:填登录账号和密码(密码至少 6 位),这个账号就是本店的管理员,以后由它来添加其他员工。
  4. +
  5. 激活授权:新门店自带 30 天免费试用,先用起来;有兑换券的话,登录后到「系统设置 → 授权兑换券」输入短码续期。
  6. +
+

提交成功后,记下系统分配的门店编号,登录时要用。

+
+ +

1.2 登录

+
+
    +
  1. 登录页依次输入门店编号登录账号密码
  2. +
  3. 勾选「记住我」(默认勾选):下次打开会自动填入最近登录的门店和账号,只需输密码。账号输入框还带历史下拉,多人共用一台电脑时点箭头切换。
  4. +
  5. 密码框右侧的小眼睛可以切换密码明文显示,方便核对。
  6. +
+
忘记密码?系统没有自助找回,请找本店管理员在「系统设置 → 用户管理」里帮你重置密码。管理员自己忘了密码,请联系技术支持。
+
+ +

1.3 五个平台都能用

+
+ + + + + + + + + +
平台说明
Windows官网下载安装包,双击安装。推荐收银台/办公电脑使用,支持针式打印机、标签打印机。
macOS官网下载 dmg 安装,已通过 Apple 公证,不会弹安全警告。
Web 网页版浏览器直接打开官网「立即使用」,免安装,临时用别人电脑也能登。
Android官网下载 APK 安装,手机上随时查库存、录单。
iOS通过 TestFlight 安装(官网下载页有指引链接)。
+

同一个账号可以多端同时登录,数据实时同步;有新版本时应用内会提示更新。

+
+ +

1.4 换个顺眼的主题

+
+

右上角的小衣服图标(登录页和主界面都有),三套配色随意换:

+
    +
  • A 经典蓝 — 浅色,默认主题
  • +
  • B 琥珀 — 深色,晚上看着不刺眼
  • +
  • C 酒窖 — 暖浅色
  • +
+

也可以在「系统设置 → 偏好设置」里选,选择会被记住。

+
+ + +

2. 核心概念(必读)

+
谁用:所有人。花三分钟读完,后面所有操作都会顺很多。
+ +

2.1 每瓶酒 = 一个独立编号

+
+

岩美酒库不是按「款」记账,而是按「件」记账——入库时每录一行,系统就生成一个新的商品编号。就算是同一款酒、同一个规格,录两行就是两个编号、两张标签。

+

录单时的三个商品字段,含义按这样理解:

+ + + + + + + +
字段实际含义示例
名称品牌茅台
系列型号 / 产品线飞天 53°
规格版本 / 包装500ml
+
好处:每件商品都能贴上带二维码的专属标签,卖出、退回、盘点都能精确到「这一件」,顾客扫码还能验真。所以库存页里同一款酒出现好几行是正常的——每行是不同批次/不同编号的货。
+
+ +

2.2 单据审核流:审核通过才动库存

+
+
草稿 → 提交 → 待审核 → 审核通过(已审核,此刻才增减库存、生成账款) / 拒绝(已拒绝,什么都不变)
+
    +
  • 草稿:随便存,可以反复改、可以删。
  • +
  • 待审核:提交后锁定,不能再编辑。发现录错了用「撤回」退回草稿,改完重新提交——管理员能撤回任何单,操作员只能撤回自己提交的单。
  • +
  • 已审核:库存和账款已经变动。这时想反悔,走「退单」(见第 3、4 章),不能直接改单。
  • +
+
+ +

2.3 四级角色,各看各的

+
+ + + + + + + + +
角色能做什么特别说明
超级管理员全部操作本店最高权限
管理员全部业务操作 + 用户管理、门店信息、强制下线设备能看到成本价与利润
操作员录单、提交、审核、查询看不到成本价和利润,出库时只见售价和小计;只能撤回自己的单
只读只能看所有新增/编辑/删除/审核按钮全部隐藏,顶栏显示「只读」标识
+
进价、成本、利润这些敏感数字只有管理员(含超管)能看到——不只是前端藏起来,服务器也不会把数字发给操作员账号,放心让店员用。
+
+ + +

3. 入库管理

+
谁用:操作员及以上 · 入口:左侧导航「入库管理」
+ +

3.1 页面一眼看懂

+
+

顶部四张统计卡(KPI 卡),后两张点一下就自动筛选列表

+ + + + + + + +
卡片含义
近30天入库笔数 / 金额最近 30 天的入库量概览
待审核单数点击 → 列表只显示待审核单,审核就从这里进
草稿单数点击 → 列表只显示草稿,接着没录完的单继续录
+
+ +

3.2 新建入库单

+
+
    +
  1. 点「新建入库单」,先选仓库供应商入库日期(下拉都能搜索,支持拼音首字母)。
  2. +
  3. 在明细表格里一行一行录货。记住:一行 = 一件独立编号的商品。每行字段: +
      +
    • 名称 / 系列 / 规格:可搜索下拉;字典里没有的直接输入文字,会提示「新增」,选它就自动加进字典。
    • +
    • 生产日期:点开用滚轮选年月日。
    • +
    • 批次号必填,没填会拦住不让提交。
    • +
    • 数量:这一行的瓶数。
    • +
    • 进价(单瓶)可以留空!调货时价格还没谈好就先留空,算「待定价」,之后用「确认进价」补(见 3.4)。
    • +
    • 参考售价:填上后会存到商品档案,出库时自动带出来,强烈建议填。
    • +
    +
  4. +
  5. 底部「总进价」自动合计,不用自己按计算器。
  6. +
  7. 录完点「保存草稿」(还想再改)或「提交审核」。
  8. +
+
键盘党提速Enter 逐格往后跳;⌘D / Ctrl+D 复制上一行(同款酒连续录超快);⌘Enter / Ctrl+Enter 直接提交。
+
+ +

3.3 提交、审核、撤回

+
+
    +
  • 提交:草稿单点「提交」进入待审核。
  • +
  • 审核:待审核单点「通过」→ 库存增加、自动生成一笔对供应商的应付账款;点「拒绝」→ 什么都不变。
  • +
  • 撤回:待审核单点「撤回」退回草稿,改完重新提交。操作员只能撤自己的单。
  • +
+
审核前请核对好数量和进价——审核一过库存和账就都动了,之后只能靠「退单」纠正。
+
+ +

3.4 退单与确认进价(已审核单的两个后悔药)

+
+
    +
  • 退单(仅管理员):已审核单点红色「退单」按钮,弹出退单明细窗,可以逐行勾选退,也可以「全部退单」。退掉的商品从库存删除,同时冲减对供应商的应付。已退的行会标红色「已退单」,列表状态旁显示「部分退单 / 已退单」徽章。
  • +
  • 确认进价:当初进价留空(待定价)的单,价格谈妥后打开单据详情,点「确认进价」补填真实进价。系统自动回填库存成本、已出库商品的成本和对供应商的应付;售价和应收不受影响。
  • +
+
+ +

3.5 找单:搜索与详细搜索

+
+
    +
  • 工具栏搜索框:输单号或供应商名称即搜;也支持按酒名搜(汉字、全拼、首字母都行)。
  • +
  • 时间范围:全部时间 / 近 7 天 / 近 30 天 / 本月 / 自定义区间。
  • +
  • 详细搜索:多条件组合查单——单号、商品名、商品编码、批次号、系列、规格、供应商、录单人、审核员、状态、日期区间。想查「这瓶酒是哪张单进的」,用商品编码搜最准。
  • +
+
+ +

3.6 打印

+
+
    +
  • 打印单据:点「打印」先弹出整页预览,确认无误再打。版式为针式打印机优化过:纯黑、加粗、实线表格,不发糊。
  • +
  • 打印标签:给本单商品逐件打标签(带二维码),见 5.6。
  • +
+
+ + +

4. 出库管理

+
谁用:操作员及以上 · 入口:左侧导航「出库管理」
+ +

4.1 新建出库单

+
+
    +
  1. 点「新建出库单」,选仓库客户出库日期
  2. +
  3. 点「添加商品」,从库存选货:弹窗分页展示在库商品,按编码 / 名称 / 系列搜索,输入即时出结果。
  4. +
  5. 选中后系统自动帮你填好:数量 = 该商品可用数量售价 = 入库时填的参考售价,需要再手动调。
  6. +
  7. 管理员账号下,每行还有实时利润列 =(售价 − 进价)× 数量,正数绿色、负数红色;底栏同时显示合计金额合计利润。操作员看不到进价和利润。
  8. +
  9. 保存草稿或提交审核。审核通过 → 库存扣减、生成对客户的应收账款
  10. +
+
出库数量不能超过库存,审核时系统会自动校验,不足会拦下来。
+
+ +

4.2 待定价出库:先发货,后定价

+
+

价格还没谈定也能先出货:售价留空提交,明细显示「待定价」。价格确定后打开单据详情,点「确认售价」补填,系统自动补一笔应收流水。

+
+ +

4.3 单据详情

+
+
    +
  • 管理员看到:成本价、售价、总售价、利润四列 + 底部合计金额与合计利润。
  • +
  • 操作员只看到:售价、小计。
  • +
  • 明细自动带出商品的生产日期和批次号,方便核货。
  • +
+
+ +

4.4 退单

+
+

客户退货:已审核出库单点红色「退单」,逐行退或全部退。退回的商品加回库存,同时按售价口径冲减对客户的应收。

+
+ +

4.5 打印

+
+
    +
  • 打印单据:带预览的出库单打印(金额按售价显示)。
  • +
  • 打印标签:给出库商品补打带二维码的标签。
  • +
+
+ + +

5. 库存管理

+
谁用:所有人(只读也能查) · 入口:左侧导航「库存管理」
+ +

5.1 库存怎么看

+
+

库存按批次维度展示:同一款酒不同批次分行列出,每行都有自己的编号、批次号、生产日期。顶部 KPI 卡里的「缺货预警」点一下即可只看缺货商品。

+ + + + + + + + + + +
说明
商品 / 系列 / 规格 / 批次号商品身份信息,商品下方带编码
库存当前在库数量
成本价入库进价(单瓶),仅管理员可见;待定价商品显示「待定价」
总价= 库存 × 成本价,仅管理员可见
生产日期 / 入库日期 / 供应商批次追溯信息
状态在售 预警 缺货(按数量与安全库存自动判定)
+
    +
  • 搜索:商品名(汉字 / 全拼 / 首字母)或商品编码,输入即搜。
  • +
  • 列设置:工具栏「显示字段」按钮勾选要显示的列,选择会被记住。
  • +
+
+ +

5.2 商品编辑抽屉

+
+

点击任意库存行,右侧滑出商品编辑抽屉:

+
    +
  • 商品信息:编码、系列、规格、成本价(单瓶)、总成本价(管理员)。
  • +
  • 商品图片:3 个图片槽位,点击上传;双击图片全屏预览,支持双指缩放、左右翻页。
  • +
  • 商品介绍 / 卖点:从介绍库搜索选择一篇介绍挂到商品上(介绍库在「基础数据 → 商品介绍」维护),顾客扫码时能看到。
  • +
+
+ +

5.3 库存盘点

+
+
    +
  1. 进入「库存盘点」页,系统自动生成盘点单号。
  2. +
  3. 盘点仓库,账面库存自动加载成盘点明细。
  4. +
  5. 选盘点类型(全盘 / 循环盘点)。
  6. +
  7. 逐行填实盘数量——和账面一致的不用动,只改对不上的。
  8. +
  9. 点「提交盘点」完成。
  10. +
+
+ +

5.4 标签打印与扫码防伪

+
+

每件商品都有唯一二维码。在库存行点「打印标签」(或入库/出库单里批量打),标签含:商品名、编码、系列、规格、批次号、生产日期、店名和联系方式、二维码。

+
顾客用手机扫标签二维码,会打开这件商品的公开页面:品名、图片、介绍、门店信息一目了然——既是防伪,也是你的线上小名片。
+
+ + +

6. 财务管理

+
谁用:店长 / 管理员为主 · 入口:左侧导航「财务管理」
+ +

6.1 页面结构

+
+
    +
  • 时间范围 chips:本月 / 近 3 个月等,切换后整页数据跟着变。
  • +
  • KPI 四卡:本月销售额、本月采购额、应收账款(客户欠我)、应付账款(我欠供应商),应收应付卡带「未结清 N 笔」提示。
  • +
  • 收支趋势图:按日/月的收支柱状图,生意起伏一眼看清。
  • +
  • 应收 / 应付汇总表:按往来单位汇总欠款,点击任意一行打开抽屉看该单位的明细流水和近期单据。
  • +
  • 收支流水表:所有财务记录,按类型 chips(收款 / 付款 / 应收 / 应付)筛选。
  • +
+
+ +

6.2 账款从哪来、怎么结清

+
+
    +
  • 入库单审核通过 → 自动生成应付;出库单审核通过 → 自动生成应收
  • +
  • 登记收支:点「登记收支」手动记一笔收款或付款(类型 + 往来单位 + 金额 + 日期 + 备注),适合与单据无关的杂项收支。
  • +
  • 结清:流水行点「结清」单笔结清;或在往来单位抽屉里整体处理某个单位的欠款;入库/出库单列表里也有结清入口。结清后状态从「未结清」变「已结清」。
  • +
+
结清操作目前不支持撤销,点之前确认好钱确实到账/付出了。
+
+ + +

7. 往来单位

+
谁用:操作员及以上 · 入口:左侧导航「往来单位」
+
+

供应商和客户都在这里管。录入库单选的「供应商」、出库单选的「客户」,来源就是这个列表。

+
    +
  • 列表:名称、类型徽章(供应商 / 客户)、联系人、电话、应收应付。顶部类型 chips 一键筛选,搜索支持拼音。
  • +
  • 新建 / 编辑:名称必填;电话、地址、备注选填;期初余额用于把老账本上的旧欠款带进系统(正数记应收或应付,视单位类型)。
  • +
  • 详情抽屉:点击任意一行,右侧滑出该单位的联系信息、应收应付余额、近期单据(入库应付、出库应收、收付款流水都在内),底部可打对账单或编辑。
  • +
+
已经关联过入库/出库单的单位不能删除,避免历史单据变成无头账。
+
+ + +

8. 基础数据

+
谁用:操作员及以上 · 入口:左侧导航「基础数据」
+
+

五个标签页,管的是「录单时那些下拉框里的选项」:

+ + + + + + + + + +
标签内容
商品档案全店所有商品编号的档案库。没有「新增商品」按钮——商品档案由入库单自动建立(每录一行建一个),这里只做查看和编辑。
商品介绍介绍库:维护商品介绍/卖点文案,库存页商品编辑抽屉里「从介绍库选择」的来源。
系列型号/产品线字典,如「飞天 53°」。
规格版本/包装字典,如「500ml」。
仓库仓库列表维护。默认仓库在「系统设置 → 偏好设置」里选。
+
字典改了什么,入库录单的下拉候选就变什么。平时不用专门来这里建——录单时输入新名字选「新增」就自动进字典了;这里更多是事后整理(改错别字、删没用的选项)。
+
+ + +

9. 设备与设置

+
谁用:管理员为主 · 入口:左侧导航「设备管理」「系统设置」「关于我们」
+ +

9.1 设备管理

+
+
    +
  • 登录设备:本店当前所有登录会话——谁、什么平台、什么时候登的、最近活跃、是否在线。管理员可对任意设备「强制下线」(员工离职、电脑丢失时第一时间用它)。
  • +
  • 外设设备:登记店里的标签打印机、小票打印机、扫码枪等(名称、型号、连接方式),方便交接与盘点资产。
  • +
  • 打印模板:查看当前单据/标签的打印模板样式。
  • +
+
+ +

9.2 系统设置(五个面板)

+
+ + + + + + + + + +
面板说明
门店信息店名、地址、电话、负责人,管理员可编辑;门店编号不可改。这些信息会印在标签和单据上。
用户管理新增用户(账号+初始密码+角色)、编辑、重置密码、启用/停用。四级角色见第 2 章。仅管理员可操作。
编号规则入库单、出库单等单号的前缀与序号。不要把序号调小到已用过的范围,会撞号。
授权兑换券本系统按「时长兑换券」授权:输入形如 JIUKU-XXXX-XXXX 的短码点「兑换续期」,时长直接叠加到现有到期日上,无需订阅。新门店自带 30 天试用。
偏好设置三套主题切换 + 默认仓库(新建单据时自动选中的仓库)。
+
授权到期会怎样?过期 7 天内是宽限期,一切照常;过期 7–15 天进入只读,只能看不能录;满 15 天锁定登录。任何阶段兑换新券立即恢复,数据不会丢。
+
+ +

9.3 关于我们

+
+

版本信息(有新版可一键更新)、授权状态与到期日、更新日志时间线,以及「反馈 Bug / 功能建议」入口。

+
+ + +

10. 常见问题 FAQ

+
+

Q1:忘记密码了怎么办?

+

找本店管理员:「系统设置 → 用户管理」→ 你的账号 →「重置密码」。管理员自己忘了,联系技术支持处理。

+ +

Q2:单据已提交(或已审核)才发现录错了,怎么改?

+

待审核的单点「撤回」退回草稿,改完重新提交(操作员只能撤自己的单)。已审核的单不能改,用「退单」把错的那几行退掉,再补一张正确的单。

+ +

Q3:库存数和实物对不上,从哪查起?

+

先在库存页找到那件商品记下商品编码,再到入库/出库管理用「详细搜索 → 商品编码」把相关单据全部拉出来,逐单核对。查清后做一次盘点(5.3)把账调平。

+ +

Q4:为什么同一款酒在库存里有好多行?

+

这是设计如此:系统按「件」记账,每次入库每行生成一个独立编号(见第 2 章)。不同行代表不同批次/不同来源的货,各自可追溯。

+ +

Q5:点打印没反应,或打出来发淡发糊?

+

先升级到最新版(Windows 打印已改为直接调用系统接口,老版本可能无响应)。针式打印机版式已做纯黑加粗优化;打印前先用「打印预览」确认。仍有问题检查打印机驱动和连接。

+ +

Q6:顾客扫商品标签二维码打不开页面?

+

早期版本的扫码报错(502 / 页面不存在)已修复。请确认手机有网络;很旧的标签可以在库存页重打一张新标签。

+ +

Q7:系统提示授权过期、变成只读了,怎么续?

+

「系统设置 → 授权兑换券」输入 JIUKU- 开头的兑换码点「兑换续期」,立即恢复。没有兑换码请联系客服购买。过期 15 天内数据都还在,别慌。

+ +

Q8:进货时价格还没谈好,能先入库吗?

+

能。进价留空提交即可(显示「待定价」),价格定了以后在入库单详情点「确认进价」补上,成本和应付自动补齐。出库同理,售价留空、事后「确认售价」。

+ +

Q9:出库审核提示「库存不足」?

+

出库数量超过了该商品在库数量。检查数量是否录多了,或对应的进货入库单是否还没审核(没审核的入库不算库存)。

+ +

Q10:员工离职 / 店里电脑丢了,账号安全怎么处理?

+

两步:①「设备管理」找到对应设备「强制下线」;②「系统设置 → 用户管理」停用该账号或重置密码。

+
+ + +
+ + diff --git a/docs/pay支付对接开发指南.html b/docs/pay支付对接开发指南.html new file mode 100644 index 0000000..27efdc6 --- /dev/null +++ b/docs/pay支付对接开发指南.html @@ -0,0 +1,207 @@ + + + + + +pay 支付对接开发指南 — 酒库管理系统 + + + +

← 文档索引

+

pay 支付对接开发指南

+
jiu 门店在应用内购买/续费授权:付款走 pay 收款服务,付成功后 pay 回调 jiu,jiu 直接给门店续期。
+
v1.0 · 2026-07-03 · 面向 jiu 后端开发 · pay 侧已就绪(下单+签名+webhook 推送已实现验证)· 本文档 = jiu 侧要实现的部分
+ +
给开发者:pay 服务(收款、支付宝渠道、签名鉴权、支付成功 webhook 推送)已开发并联调验证完毕。你只需实现 jiu 这一侧的 4 块:① 购买接口 ② webhook 接收器 ③ 续期逻辑 ④ 查单兜底。接口契约、签名算法、权益映射见下,照着写即可。
+ +

1. 名词与地址

+ + + + + + + + +
pay 服务地址https://pay.51yanmei.com
pay 下单接口POST /api/v1/orders
pay 查单接口GET /api/v1/orders/{out_trade_no}
pay 套餐列表GET /api/v1/products(含 biz_code)
jiu 回调接收器(你要实现POST /api/v1/pay/callback
共享密钥双向共用一把 HMAC 密钥,存 Bitwarden,两边经环境变量注入(pay: BIZ_JIU_SECRET;jiu 侧自定,同值)
+ +

2. 整体流程

+
jiu 客户端(已登录, 知道 shop_id) jiu 后端 pay 服务 支付宝 + │ │ │ │ + ①选套餐, 点购买 ──────────────────────►│ │ │ + │ ②建 Purchase(pending, shop_id, biz_code) │ │ + │ 调 pay 下单 (product_id + biz_ref=purchase_id, 签名) ─►建订单 │ + │ │◄──── {pay_url} ─────────────│ │ + │◄────────── 返回 pay_url ──────────────│ │ │ + ③打开 pay_url 付款 (PC扫码/手机拉App) ───────────────────────────────────────────────────►付款 + │ │ ④支付宝→pay 入账 ✓ │ + │ │◄── ⑤pay 签名 webhook ───────│ │ + │ ⑥验签→幂等→按 biz_code 给 shop 续期→回 SUCCESS │ + ④'客户端跳回 return_url / 刷新 → 已续期 ✓│ │ │
+ +

3. 签名算法(两个方向都用它)

+
+

签名串(4 段用换行 \n 连接,再 HMAC-SHA256,最后 base64):

+
sign = base64( HMAC_SHA256( secret, system + "\n" + timestamp + "\n" + nonce + "\n" + rawBody ) )
+

请求头

+ + + + + + +
Header说明
X-Pay-System固定 jiu
X-Pay-TimestampUnix 秒(服务端校验 ±5 分钟窗口)
X-Pay-Nonce随机串(如 uuid)
X-Pay-Sign上面算出的签名
+

rawBody = HTTP 请求体的原始字节(验签/签名都对同一份原始 body,勿先反序列化再重拼)。

+
方向:jiu→pay 下单 由 jiu 签名、pay 验签;pay→jiu 回调 由 pay 签名、jiu 验签。两边同一把密钥、同一算法。
+
+

Go 参考实现(与 pay 侧 util.HMACSign 完全一致)

+
// 签名(下单时调用)/ 验签(收 webhook 时对比)
+func hmacSign(secret string, parts ...string) string {
+    m := hmac.New(sha256.New, []byte(secret))
+    m.Write([]byte(strings.Join(parts, "\n")))
+    return base64.StdEncoding.EncodeToString(m.Sum(nil))
+}
+// 验签:hmac.Equal(([]byte)hmacSign(secret, "jiu", ts, nonce, rawBody), got)
+ +

4. jiu 要实现的四块

+ +

1购买接口 jiu POST /api/v1/license/purchase

+

鉴权 = jiu 自己的登录态(当前用户/门店)。步骤:

+
    +
  1. 校验登录态,拿到当前 shop_id;入参为套餐(前端传 biz_code 或 plan 标识)。
  2. +
  3. Purchase 记录(status=pending,存 shop_id / product_biz_code / amount)。
  4. +
  5. 调 pay 下单(见 §5.1),biz_ref = purchase_idreturn_url = jiu 结果页。
  6. +
  7. 把 pay 返回的 pay_url(和 out_trade_no,回写 Purchase)返给客户端。
  8. +
+
建议新增表 license_purchasesid · shop_id · product_biz_code · amount · out_trade_no(index) · status(pending/paid/failed) · paid_at · created_atout_trade_no 兼作对账键与幂等键。
+ +

2webhook 接收器 jiu POST /api/v1/pay/callback(核心)

+
    +
  1. 读原始 body(勿被框架提前解析掉),取 4 个签名头。
  2. +
  3. 验签hmac.Equal(hmacSign(secret,"jiu",ts,nonce,rawBody), X-Pay-Sign)校验时间戳 ±5 分钟。任一不过 → 401,不处理。
  4. +
  5. 幂等:按 out_trade_no 查 Purchase;若已 paid → 直接回 {"code":"SUCCESS"}(pay 会重发,必须幂等)。
  6. +
  7. 金额校验:payload.amount 与 Purchase.amount 一致(分级比较,防篡改)。
  8. +
  9. 续期:按 product_biz_code 映射权益(§6),给该 shop 的 License 直接叠加 ExpiresAt(§7)。
  10. +
  11. 标记 Purchase=paid;回 HTTP 200 + {"code":"SUCCESS"}(pay 认响应体含 SUCCESS 才算成功,否则退避重试)。
  12. +
+
这是「钱已到账」的入口——验签 + 幂等 + 金额校验三者缺一不可。验签失败绝不能续期(防伪造「已付款」骗授权)。
+ +

3续期逻辑 jiu

+

复用/参照现有 license 服务(见 授权体系设计)。方式 = 直接叠加(不生成兑换码):

+
base := license.ExpiresAt
+if base == nil || base.Before(now) { base = now }   // 已过期从现在起算
+license.ExpiresAt = base + duration(bizCode)         // +30 或 +365 天
+license.Tier       = tier(bizCode)                   // standard / pro
+license.Type       = billType(bizCode)               // monthly / annual
+license.MaxDevices = maxDevices(bizCode)             // 2 / 5
+license.Features    = features(bizCode)              // 仓库数/图片额度/AI 分析
+license.IsActive   = true
+ +

4查单兜底 jiu

+

防 webhook 全丢:对 pending 超时(如 >5 分钟)的 Purchase,定时主动查 pay GET /api/v1/orders/{out_trade_no},若返回 status=paid 则走与 webhook 相同的续期入账(同样幂等)。

+ +

5. 与 pay 的接口契约

+ +

5.1 下单 pay POST https://pay.51yanmei.com/api/v1/orders

+
// Headers: Content-Type: application/json + 4 个签名头(§3)
+// Body(对这份原始 body 签名):
+{
+  "product_id": 3,                       // pay 套餐 id(见 §6,或先 GET /products 按 biz_code 查 id)
+  "biz_system": "jiu",
+  "biz_ref": "<purchase_id>",           // jiu 的购买记录 id,回调原样带回
+  "return_url": "https://jiu.51yanmei.com/license/result"
+}
+// 成功响应:
+{ "data": { "pay_url": "https://openapi.alipay.com/...", "out_trade_no": "yanmei-...", "amount": "2999.00", "subject": "年付标准" } }
+

data.pay_url 交给客户端打开;out_trade_no 回写 Purchase。金额以 pay 套餐表为准,不要自己传金额。

+ +

5.2 回调 payjiu(pay 主动 POST 到你的接收器)

+
// Headers: 4 个签名头(§3),你要验签
+{
+  "out_trade_no": "yanmei-20260703...-xxxx",
+  "biz_system": "jiu",
+  "biz_ref": "<purchase_id>",
+  "product_biz_code": "annual_standard",
+  "amount": "2999.00",
+  "trade_no": "2026070322001...",        // 支付宝交易号
+  "channel": "alipay",
+  "paid_at": "2026-07-03T14:36:30+08:00"
+}
+// 你必须回:HTTP 200 + {"code":"SUCCESS"}(否则 pay 每 60s 重试,24h 内)
+ +

6. 套餐 biz_code → 权益映射

+ + + + + + +
套餐biz_code价格时长tier客户端(MaxDevices)权益(Features)
月付·标准monthly_standard¥299+30 天standard2单店/单仓库 · 千张图片分享
年付·标准annual_standard¥2999+365 天standard2单店/单仓库 · 千张图片分享
月付·高级monthly_pro¥599+30 天pro5单店/多仓库 · 万张照片分享 · 免费 AI 周/月度商业数据分析
年付·高级annual_pro¥5999+365 天pro5单店/多仓库 · 万张照片分享 · 免费 AI 周/月度商业数据分析
+

建议 Features(License.Features JSON)编码:{"max_warehouses": 1|0(0=不限), "image_quota": 1000|10000, "ai_analysis": false|true}。具体键名由 jiu 侧按现有能力开关定义,本表给权益语义。

+
jiu 当前 Tier 未做能力差异(仅 standard)。标准/高级的实际功能开关(多仓库限制、图片额度、AI 分析)需 jiu 侧后续按此权益表落地;先把 tier/max_devices/features 正确写入 License,能力 gating 逐步接。
+ +

7. 安全红线

+
    +
  • 验签必过才处理(防伪造付款回调)。
  • +
  • 幂等:同一 out_trade_no 只续一次(pay 会重发)。
  • +
  • 金额核对:回调 amount == Purchase.amount。
  • +
  • 时间戳窗口 ±5 分钟,防重放。
  • +
  • 密钥只走环境变量 / Bitwarden,不写代码/配置/仓库
  • +
  • 全程 HTTPS(jiu.51yanmei.com 已有)。
  • +
+ +

8. 客户端(Web + App)

+
    +
  • Web 入口:拿到 pay_url 直接 window.location 跳转;付完支付宝跳回 return_url(带 out_trade_no),结果页轮询 jiu 授权状态。
  • +
  • App 入口(Flutter):用内置 webview 打开 pay_url。
  • +
  • 手机端 pay 会走支付宝 wap.pay(H5)自动拉起支付宝 App;webview 需放行 alipays:// / alipay:// scheme 唤起(拦截非 http(s) 跳转交系统打开),付完靠 return_url 回跳 webview 页面。
  • +
  • 到账以 webhook 续期为准,客户端 return 后应查 jiu 授权状态确认,而非只信 return。
  • +
+ +

9. 联调 Checklist

+
    +
  1. 约定并配置共享密钥(pay BIZ_JIU_SECRET = jiu 侧同值);pay 侧配 BIZ_JIU_CALLBACK_URL = jiu 接收器地址。
  2. +
  3. pay 侧 seed 4 个真实套餐(biz_code 见 §6),jiu 记下对应 product_id(或用 GET /products 动态取)。
  4. +
  5. jiu 实现购买接口 → 下单 → 拿 pay_url(先验证签名被 pay 接受)。
  6. +
  7. jiu 实现 webhook 接收器 → 用 pay 真实 1 分钱订单触发(临时把某套餐改 0.01)→ 验签/幂等/续期全走通。
  8. +
  9. 验证幂等(pay 重发不重复续期)、查单兜底(模拟 webhook 丢失)。
  10. +
  11. 客户端 Web + App webview 两端各跑一遍付款→回跳→授权已续。
  12. +
+ +

相关:授权体系设计(兑换券模型) · 数据库 Schema · pay 侧设计见 pay 仓 docs/pay接入jiu授权续费对接方案.html

+ + diff --git a/docs/user-manual.md b/docs/user-manual.md index 7044bdb..65e029f 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -1,3 +1,5 @@ +> 本文档已迁移至 [manual/user-manual.html](manual/user-manual.html),本文件停止更新。 + # 酒库管理系统 — 用户操作手册 > 适用版本:v1.x