docs: 用户/开发手册 HTML 版、db-schema 补 license_purchases、CLAUDE.md 文档地图、pay 对接指南
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
This commit is contained in:
@@ -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 `<part>-v<ver>` → 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**。
|
||||
|
||||
+19
-43
@@ -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<int>` 实现行级 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 <part> [version]` slash command:本地 build→test→更新 CHANGELOG→commit→tag→push;CI(Forgejo) 按 tag 前缀触发对应 workflow 自动编译/测试/发 Release/部署 EC2/Telegram 通知。**测试未过禁止发版**。
|
||||
用 `/release <part> [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 部署说明 |
|
||||
|
||||
+21
-1
@@ -58,7 +58,7 @@
|
||||
<div class="layout">
|
||||
<nav>
|
||||
<div style="font-size:13px;font-weight:700;color:var(--primary-dark);margin-bottom:14px;">📊 数据库 Schema</div>
|
||||
<div class="navgroup"><div class="navtitle">租户 · 用户 · 安全</div><a href="#shops"><code>shops</code></a><a href="#users"><code>users</code></a><a href="#user_sessions"><code>user_sessions</code></a><a href="#login_attempts"><code>login_attempts</code></a></div><div class="navgroup"><div class="navtitle">授权 · 兑换券</div><a href="#licenses"><code>licenses</code></a><a href="#license_devices"><code>license_devices</code></a><a href="#license_codes"><code>license_codes</code></a></div><div class="navgroup"><div class="navtitle">商品主数据</div><a href="#products"><code>products</code></a><a href="#product_categories"><code>product_categories</code></a><a href="#product_images"><code>product_images</code></a><a href="#product_name_options"><code>product_name_options</code></a><a href="#product_series_options"><code>product_series_options</code></a><a href="#product_spec_options"><code>product_spec_options</code></a><a href="#product_origin_options"><code>product_origin_options</code></a><a href="#product_shelf_life_options"><code>product_shelf_life_options</code></a><a href="#product_storage_options"><code>product_storage_options</code></a><a href="#product_description_docs"><code>product_description_docs</code></a></div><div class="navgroup"><div class="navtitle">仓库 · 往来单位</div><a href="#warehouses"><code>warehouses</code></a><a href="#partners"><code>partners</code></a></div><div class="navgroup"><div class="navtitle">出入库单据</div><a href="#stock_in_orders"><code>stock_in_orders</code></a><a href="#stock_in_items"><code>stock_in_items</code></a><a href="#stock_out_orders"><code>stock_out_orders</code></a><a href="#stock_out_items"><code>stock_out_items</code></a></div><div class="navgroup"><div class="navtitle">库存 · 流水 · 盘点</div><a href="#inventories"><code>inventories</code></a><a href="#inventory_logs"><code>inventory_logs</code></a><a href="#inventory_checks"><code>inventory_checks</code></a><a href="#inventory_check_items"><code>inventory_check_items</code></a></div><div class="navgroup"><div class="navtitle">财务</div><a href="#finance_records"><code>finance_records</code></a></div><div class="navgroup"><div class="navtitle">配置 · 运维 · 反馈</div><a href="#number_rules"><code>number_rules</code></a><a href="#feedbacks"><code>feedbacks</code></a><a href="#error_reports"><code>error_reports</code></a></div>
|
||||
<div class="navgroup"><div class="navtitle">租户 · 用户 · 安全</div><a href="#shops"><code>shops</code></a><a href="#users"><code>users</code></a><a href="#user_sessions"><code>user_sessions</code></a><a href="#login_attempts"><code>login_attempts</code></a></div><div class="navgroup"><div class="navtitle">授权 · 兑换券</div><a href="#licenses"><code>licenses</code></a><a href="#license_devices"><code>license_devices</code></a><a href="#license_codes"><code>license_codes</code></a><a href="#license_purchases"><code>license_purchases</code></a></div><div class="navgroup"><div class="navtitle">商品主数据</div><a href="#products"><code>products</code></a><a href="#product_categories"><code>product_categories</code></a><a href="#product_images"><code>product_images</code></a><a href="#product_name_options"><code>product_name_options</code></a><a href="#product_series_options"><code>product_series_options</code></a><a href="#product_spec_options"><code>product_spec_options</code></a><a href="#product_origin_options"><code>product_origin_options</code></a><a href="#product_shelf_life_options"><code>product_shelf_life_options</code></a><a href="#product_storage_options"><code>product_storage_options</code></a><a href="#product_description_docs"><code>product_description_docs</code></a></div><div class="navgroup"><div class="navtitle">仓库 · 往来单位</div><a href="#warehouses"><code>warehouses</code></a><a href="#partners"><code>partners</code></a></div><div class="navgroup"><div class="navtitle">出入库单据</div><a href="#stock_in_orders"><code>stock_in_orders</code></a><a href="#stock_in_items"><code>stock_in_items</code></a><a href="#stock_out_orders"><code>stock_out_orders</code></a><a href="#stock_out_items"><code>stock_out_items</code></a></div><div class="navgroup"><div class="navtitle">库存 · 流水 · 盘点</div><a href="#inventories"><code>inventories</code></a><a href="#inventory_logs"><code>inventory_logs</code></a><a href="#inventory_checks"><code>inventory_checks</code></a><a href="#inventory_check_items"><code>inventory_check_items</code></a></div><div class="navgroup"><div class="navtitle">财务</div><a href="#finance_records"><code>finance_records</code></a></div><div class="navgroup"><div class="navtitle">配置 · 运维 · 反馈</div><a href="#number_rules"><code>number_rules</code></a><a href="#feedbacks"><code>feedbacks</code></a><a href="#error_reports"><code>error_reports</code></a></div>
|
||||
</nav>
|
||||
<main>
|
||||
<h1>数据库 Schema · 酒库管理系统</h1>
|
||||
@@ -212,6 +212,26 @@
|
||||
<tr><td class="mono">updated_at</td><td class="mono ty">DATETIME</td><td class="ctr">否</td><td class="mono df">CURRENT_TIMESTAMP</td><td></td></tr></tbody>
|
||||
</table>
|
||||
<div class="keys"><span class="tag pk">PRIMARY</span> <span class="mono kd">(`id`)</span><br><span class="tag uk">UNIQUE uk_code</span> <span class="mono kd">(`code`)</span><br><span class="tag ix">INDEX idx_status</span> <span class="mono kd">(`status`)</span><br><span class="tag ix">INDEX idx_redeemed_shop</span> <span class="mono kd">(`redeemed_shop_id`)</span></div>
|
||||
</div><div class="card" id="license_purchases">
|
||||
<h3><code class="tname">license_purchases</code> <span class="tcomment">在线购买/续费记录</span></h3>
|
||||
<div class="callout ok"><b>pay 收款中枢:</b>契约见 <code>~/code/pay-contract</code>(v1.0.0)。<code>out_trade_no</code> = pay 订单号,兼作对账键与幂等键(同一单只续期一次);<code>amount</code> 为 pay 下单响应回传的权威金额,webhook 回调时逐分核对。</div>
|
||||
<table>
|
||||
<thead><tr><th style="width:23%">列名</th><th style="width:20%">类型</th><th style="width:7%">可空</th><th style="width:16%">默认</th><th>说明</th></tr></thead>
|
||||
<tbody><tr><td class="mono"><b>id</b> <span class="tag pk">PK</span></td><td class="mono ty">BIGINT UNSIGNED</td><td class="ctr">否</td><td class="mono df">AUTO_INC</td><td></td></tr>
|
||||
<tr><td class="mono">shop_id <span class="tag tnt">租户</span></td><td class="mono ty">BIGINT UNSIGNED</td><td class="ctr">否</td><td class="mono df">—</td><td></td></tr>
|
||||
<tr><td class="mono">user_id</td><td class="mono ty">BIGINT UNSIGNED</td><td class="ctr">否</td><td class="mono df">—</td><td>下单管理员</td></tr>
|
||||
<tr><td class="mono">product_biz_code</td><td class="mono ty">VARCHAR(64)</td><td class="ctr">否</td><td class="mono df">—</td><td>套餐稳定码:monthly_standard / annual_standard / monthly_pro / annual_pro</td></tr>
|
||||
<tr><td class="mono">amount</td><td class="mono ty">VARCHAR(16)</td><td class="ctr">是</td><td class="mono df">NULL</td><td>pay 下单回传金额,如 2999.00</td></tr>
|
||||
<tr><td class="mono">out_trade_no</td><td class="mono ty">VARCHAR(64)</td><td class="ctr">是</td><td class="mono df">NULL</td><td>pay 订单号(对账+幂等键)</td></tr>
|
||||
<tr><td class="mono">status</td><td class="mono ty">ENUM('pending','paid','failed')</td><td class="ctr">否</td><td class="mono df">pending</td><td></td></tr>
|
||||
<tr><td class="mono">trade_no</td><td class="mono ty">VARCHAR(64)</td><td class="ctr">是</td><td class="mono df">NULL</td><td>渠道交易号(支付宝/微信)</td></tr>
|
||||
<tr><td class="mono">channel</td><td class="mono ty">VARCHAR(16)</td><td class="ctr">是</td><td class="mono df">NULL</td><td></td></tr>
|
||||
<tr><td class="mono">paid_at</td><td class="mono ty">DATETIME</td><td class="ctr">是</td><td class="mono df">NULL</td><td></td></tr>
|
||||
<tr><td class="mono">created_at</td><td class="mono ty">DATETIME</td><td class="ctr">否</td><td class="mono df">CURRENT_TIMESTAMP</td><td></td></tr>
|
||||
<tr><td class="mono">updated_at</td><td class="mono ty">DATETIME</td><td class="ctr">否</td><td class="mono df">CURRENT_TIMESTAMP</td><td></td></tr>
|
||||
<tr><td class="mono">deleted_at</td><td class="mono ty">DATETIME</td><td class="ctr">是</td><td class="mono df">NULL</td><td>软删除</td></tr></tbody>
|
||||
</table>
|
||||
<div class="keys"><span class="tag pk">PRIMARY</span> <span class="mono kd">(`id`)</span><br><span class="tag uk">UNIQUE uk_out_trade_no</span> <span class="mono kd">(`out_trade_no`)</span><br><span class="tag ix">INDEX idx_shop</span> <span class="mono kd">(`shop_id`)</span><br><span class="tag ix">INDEX idx_status</span> <span class="mono kd">(`status`)</span></div>
|
||||
</div></section><section><h2>商品主数据</h2><p class="gdesc">products 每行 = 一个特有产品/序列号(非 SKU),是商品信息的单一来源。基础数据字典为入库选文本服务。</p><div class="card" id="products">
|
||||
<h3><code class="tname">products</code> <span class="tcomment">商品</span></h3>
|
||||
<div class="callout ok"><b>单一来源铁律:</b>批次号 <code>batch_no</code>、生产日期 <code>production_date</code>、进价、图片、public_id 均以此表为准。每行 = 一个序列号(<code>uk_product_code</code> 同店唯一)。</div>
|
||||
|
||||
+39
-20
@@ -27,34 +27,53 @@
|
||||
<h1>酒库管理系统 · 文档索引</h1>
|
||||
<div class="sub">项目所有文档的单一入口。新增文档须同时登记到此处。</div>
|
||||
|
||||
<h2>设计方案 / 原型</h2>
|
||||
<h2>📖 手册</h2>
|
||||
<ul>
|
||||
<li><a href="manual/user-manual.html">用户手册</a><span class="tag html">HTML</span> <span class="hint">— 面向酒行员工的操作手册(登录/入库/出库/库存/财务/设置)</span></li>
|
||||
<li><a href="manual/dev-manual.html">开发手册</a><span class="tag html">HTML</span> <span class="hint">— 面向后续开发者/维护者:架构总览/后端/前端/数据库/API 参考/运维/发版工作流</span></li>
|
||||
<li><a href="db-schema.html">数据库 Schema(列级文档)</a><span class="tag html">HTML</span> <span class="hint">— 31 张表全字段 + 索引 + 租户列标注</span></li>
|
||||
<li><a href="user-manual.md">用户手册(旧版)</a><span class="tag">MD</span> <span class="hint">— 已被 HTML 版取代,保留归档</span></li>
|
||||
</ul>
|
||||
|
||||
<h2>🏗 设计方案</h2>
|
||||
<ul>
|
||||
<li><a href="pay支付对接开发指南.html">pay 支付对接开发指南</a><span class="tag html">HTML</span> <span class="hint">— jiu 门店应用内购买/续费授权:付款走 pay,付成功后 pay 签名 webhook 回调 jiu 直接续期。含签名算法、下单/回调接口契约、套餐 biz_code→权益映射、续期逻辑、安全红线、客户端(Web+App webview)、联调清单。pay 侧已就绪,本文档=jiu 侧要实现的部分</span></li>
|
||||
<li><a href="design/order-return-prototype.html">退单原型(已审核单据)</a><span class="tag html">HTML</span></li>
|
||||
<li><a href="design/order-print-layouts.html">出入库单打印排版方案(6 种黑白样式精选)</a><span class="tag html">HTML</span> · <a href="design/order-print-layouts.pdf">PDF</a></li>
|
||||
<li><a href="design/stock-in-cost-confirm-design.html">入库确认进价(暂估价前向补偿)设计</a><span class="tag html">HTML</span></li>
|
||||
<li><a href="design/inventory-filter-spec.md">库存筛选规格</a><span class="tag">MD</span></li>
|
||||
</ul>
|
||||
|
||||
<h2>📚 知识库 · 调研</h2>
|
||||
<ul>
|
||||
<li><a href="context/project.md">项目上下文(Agent 必读,项目全貌)</a><span class="tag">MD</span></li>
|
||||
<li><a href="dev-setup.md">开发环境搭建</a><span class="tag">MD</span></li>
|
||||
<li><a href="architecture/license-design.md">授权体系设计(兑换券模型)</a><span class="tag">MD</span></li>
|
||||
<li><a href="architecture/session-security.md">会话安全设计(sid 校验/踢人/失败锁定)</a><span class="tag">MD</span></li>
|
||||
<li><a href="android-signing.md">Android 签名</a> · <a href="ios-signing.md">iOS 签名</a> · <a href="macos-signing.md">macOS 签名</a><span class="tag">MD</span></li>
|
||||
<li><a href="deployment-nas-gitea.md">部署:NAS Gitea/Forgejo</a><span class="tag">MD</span></li>
|
||||
<li><a href="TODO.md">待办(后续迭代事项)</a><span class="tag">MD</span></li>
|
||||
</ul>
|
||||
|
||||
<h2>🔧 Runbook · 排障</h2>
|
||||
<ul>
|
||||
<li><a href="runbooks/ec2-to-ali-migration.html">EC2 → 阿里云 迁移计划</a><span class="tag html">HTML</span> <span class="hint">— jiu 服务/数据/DB/文件/域名/CI 整体迁移(2026-07-02 已割接)</span></li>
|
||||
</ul>
|
||||
|
||||
<h2>🧪 测试 · 审计报告</h2>
|
||||
<ul>
|
||||
<li><a href="testing/license-test-flow.html">授权兑换券测试流程</a><span class="tag html">HTML</span></li>
|
||||
<li><a href="security/2026-07-03-audit.md">安全审计(2026-07-03)</a> · <a href="security/2026-07-03-precommit-audit.md">提交前安全审计</a><span class="tag">MD</span></li>
|
||||
<li><a href="review/2026-07-03-precommit-review.md">提交前代码审查(2026-07-03)</a> · <a href="review/2026-07-03-uncommitted-review.md">未提交变更审查</a><span class="tag">MD</span></li>
|
||||
<li><a href="review/stockorder-empty-fields-stats.html">出入库单空字段统计</a><span class="tag html">HTML</span></li>
|
||||
<li><a href="review/inventory-qty-packsize-report.md">库存「装箱数当数量」脏数据报告</a><span class="tag">MD</span> · <a href="review/inventory-qty-packsize-dirty.csv">CSV</a></li>
|
||||
<li><a href="review/stock-order-bugs.md">出入库单 bug 清单</a> · <a href="review/validation-bugs.md">校验 bug 清单</a> · <a href="review/flutter-layout-bugs.md">Flutter 布局 bug 清单</a><span class="tag">MD</span></li>
|
||||
<li><a href="review/lint-report.md">Lint 报告</a><span class="tag">MD</span></li>
|
||||
</ul>
|
||||
|
||||
<h2>实现计划</h2>
|
||||
<ul>
|
||||
<li class="hint">(暂无)</li>
|
||||
</ul>
|
||||
|
||||
<h2>排障 / 运维 Runbook</h2>
|
||||
<ul>
|
||||
<li><a href="runbooks/ec2-to-ali-migration.html">EC2 → 阿里云 迁移计划</a><span class="tag html">HTML</span> <span class="hint">— jiu 服务/数据/DB/文件/域名/CI 整体迁移</span></li>
|
||||
</ul>
|
||||
|
||||
<h2>知识库 / 调研 / 测试</h2>
|
||||
<ul>
|
||||
<li><a href="testing/license-test-flow.html">授权兑换券测试流程</a><span class="tag html">HTML</span></li>
|
||||
</ul>
|
||||
|
||||
<h2>历史 Markdown 文档(逐步迁移)</h2>
|
||||
<ul>
|
||||
<li><a href="deployment-nas-gitea.md">部署:NAS Gitea/Forgejo</a><span class="tag">MD</span></li>
|
||||
<li><a href="dev-setup.md">开发环境搭建</a><span class="tag">MD</span></li>
|
||||
<li><a href="android-signing.md">Android 签名</a> · <a href="ios-signing.md">iOS 签名</a> · <a href="macos-signing.md">macOS 签名</a><span class="tag">MD</span></li>
|
||||
<li><a href="user-manual.md">用户手册</a><span class="tag">MD</span></li>
|
||||
<li class="hint">另有 api/ architecture/ requirements/ review/ security/ runbooks/ context/ 等目录下的过程文档</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,669 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>开发手册 — 岩美酒库</title>
|
||||
<style>
|
||||
:root{
|
||||
--primary:#2563AC; --primary-dark:#154072; --danger:#D14343; --danger-bg:#FDECEC;
|
||||
--success:#2E8B57; --success-bg:#E6F3EC; --warn:#B45309; --warn-bg:#FFF4E5; --accent:#8B2331;
|
||||
--ink:#232934; --muted:#6E7888; --border:#DCE2EB; --paper:#F5F7FA; --head:#F0F4FF;
|
||||
}
|
||||
*{box-sizing:border-box;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
|
||||
body{margin:0;background:var(--paper);color:var(--ink);line-height:1.6;}
|
||||
main{max-width:980px;margin:0 auto;padding:26px 30px 60px;}
|
||||
h1{font-size:22px;margin:0 0 4px;}
|
||||
.sub{color:var(--muted);font-size:13px;margin-bottom:6px;}
|
||||
h2{font-size:17px;margin:38px 0 6px;color:var(--primary-dark);border-bottom:2px solid var(--head);padding-bottom:6px;}
|
||||
h3{font-size:14px;margin:20px 0 8px;color:var(--primary-dark);}
|
||||
.gdesc{font-size:12.5px;color:var(--muted);margin:6px 0 12px;}
|
||||
.card{background:#fff;border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin:14px 0;}
|
||||
table{width:100%;border-collapse:collapse;font-size:12.5px;margin:6px 0;}
|
||||
th{background:var(--head);color:var(--primary-dark);font-weight:600;font-size:11.5px;text-align:left;padding:7px 9px;border-bottom:1px solid var(--border);}
|
||||
td{padding:6px 9px;border-bottom:1px solid #EEF1F6;vertical-align:top;}
|
||||
tr:last-child td{border-bottom:0;}
|
||||
.mono{font-family:ui-monospace,Menlo,monospace;}
|
||||
code{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;background:#EEF2F8;padding:1px 5px;border-radius:4px;color:var(--primary-dark);}
|
||||
pre{background:#232934;color:#E8ECF3;border-radius:8px;padding:12px 14px;font-size:12px;line-height:1.55;overflow:auto;}
|
||||
pre code{background:none;color:inherit;padding:0;}
|
||||
.tag{display:inline-block;font-size:10px;padding:0 6px;border-radius:9px;font-weight:600;line-height:16px;white-space:nowrap;}
|
||||
.tag.pub{background:var(--success-bg);color:var(--success);}
|
||||
.tag.jwt{background:#E5EEF9;color:var(--primary);}
|
||||
.tag.admin{background:var(--warn-bg);color:var(--warn);}
|
||||
.tag.super{background:var(--danger-bg);color:var(--danger);}
|
||||
.tag.rl{background:#EEE;color:var(--muted);}
|
||||
.callout{border-left:4px solid var(--primary);background:#F0F6FF;padding:9px 13px;border-radius:4px;margin:8px 0 12px;font-size:12.5px;}
|
||||
.callout.danger{border-color:var(--danger);background:var(--danger-bg);}
|
||||
.callout.warn{border-color:var(--warn);background:var(--warn-bg);}
|
||||
.callout.ok{border-color:var(--success);background:var(--success-bg);}
|
||||
.toc{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:6px 18px;background:#fff;border:1px solid var(--border);border-radius:10px;padding:14px 18px;margin:16px 0;}
|
||||
.toc a{font-size:13px;color:var(--primary);text-decoration:none;}
|
||||
.toc a:hover{text-decoration:underline;}
|
||||
.toc b{grid-column:1/-1;font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-top:4px;}
|
||||
.svgwrap{background:#fff;border:1px solid var(--border);border-radius:10px;padding:14px;margin:12px 0;}
|
||||
svg{display:block;width:100%;height:auto;max-width:900px;margin:0 auto;}
|
||||
svg text{font-family:-apple-system,"PingFang SC",sans-serif;}
|
||||
.m{font-family:ui-monospace,Menlo,monospace;font-weight:700;font-size:11px;}
|
||||
.m.get{color:var(--success);} .m.post{color:var(--primary);} .m.put{color:var(--warn);} .m.del{color:var(--danger);} .m.patch{color:var(--accent);}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>岩美酒库 · 开发手册</h1>
|
||||
<div class="sub">面向后续开发者/维护者 · server v1.1.1 / client v1.1.1 · 更新于 2026-07-03 · 来源以仓库代码为准(<code>backend/</code> <code>client/</code> <code>deploy/</code> <code>scripts/</code>)</div>
|
||||
|
||||
<div class="toc">
|
||||
<b>目录</b>
|
||||
<a href="#arch">1. 系统架构总览</a>
|
||||
<a href="#backend">2. 后端(Go / Gin)</a>
|
||||
<a href="#frontend">3. 前端(Flutter)</a>
|
||||
<a href="#db">4. 数据库</a>
|
||||
<a href="#api">5. API 参考</a>
|
||||
<a href="#ops">6. 数据操作与运维</a>
|
||||
<a href="#workflow">7. 开发环境与工作流</a>
|
||||
</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<h2 id="arch">1. 系统架构总览</h2>
|
||||
<p class="gdesc">五端 Flutter 客户端统一访问 <code>https://jiu.51yanmei.com</code>。阿里云主机上 nginx 1.24 终结 TLS(443 ssl+http2,certbot webroot 自动续期,80 → 443 跳转),按 location 分发:API/健康检查/版本反代到本机 8081 的 Go 后端(systemd <code>jiu.service</code>),静态资源(Flutter Web <code>/app</code>、商品图 <code>/images</code>、安装包 <code>/downloads</code>、营销站根路径)直接由 nginx 服务;<code>/product/:id</code> 与 <code>/app/product/:id</code>(扫码分享页)回代后端注入 OG 标签。后端连本机 Docker 容器 <code>jiu_mysql</code>(MySQL 8,127.0.0.1:3306)。未鉴权的 <code>/api/v1/(public|auth)/</code> 在 nginx 层再加一道 per-IP 限流(10r/s burst 20)。源:<code>deploy/nginx-jiu-ali.conf</code>、<code>deploy/jiu.service</code>。</p>
|
||||
|
||||
<div class="svgwrap">
|
||||
<svg viewBox="0 0 900 470" role="img" aria-label="系统架构图">
|
||||
<defs>
|
||||
<marker id="ar" markerWidth="8" markerHeight="8" refX="7" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L8,3.5 L0,7 Z" fill="#6E7888"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<!-- clients -->
|
||||
<g font-size="12" text-anchor="middle">
|
||||
<rect x="30" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="100" y="43" fill="#154072">Windows</text>
|
||||
<rect x="200" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="270" y="43" fill="#154072">macOS</text>
|
||||
<rect x="370" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="440" y="43" fill="#154072">Web (/app)</text>
|
||||
<rect x="540" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="610" y="43" fill="#154072">Android</text>
|
||||
<rect x="710" y="20" width="140" height="36" rx="6" fill="#F0F4FF" stroke="#2563AC"/><text x="780" y="43" fill="#154072">iOS</text>
|
||||
</g>
|
||||
<line x1="100" y1="56" x2="430" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||||
<line x1="270" y1="56" x2="437" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||||
<line x1="440" y1="56" x2="443" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||||
<line x1="610" y1="56" x2="450" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||||
<line x1="780" y1="56" x2="457" y2="98" stroke="#6E7888" marker-end="url(#ar)"/>
|
||||
<text x="450" y="88" text-anchor="middle" font-size="11" fill="#6E7888">https://jiu.51yanmei.com</text>
|
||||
<!-- nginx -->
|
||||
<rect x="30" y="100" width="840" height="170" rx="8" fill="#fff" stroke="#154072" stroke-width="1.5"/>
|
||||
<text x="450" y="122" text-anchor="middle" font-size="13" font-weight="700" fill="#154072">ali · nginx 1.24 — 443 ssl+http2(certbot webroot 自动续期)· 80 → 443</text>
|
||||
<g font-size="11" text-anchor="middle">
|
||||
<rect x="50" y="136" width="190" height="52" rx="5" fill="#E6F3EC" stroke="#2E8B57"/>
|
||||
<text x="145" y="157" fill="#2E8B57">静态:/app(Flutter Web)</text>
|
||||
<text x="145" y="173" fill="#2E8B57">/images · /downloads · / 营销站</text>
|
||||
<rect x="260" y="136" width="190" height="52" rx="5" fill="#F0F4FF" stroke="#2563AC"/>
|
||||
<text x="355" y="157" fill="#154072">反代:/api /health /version</text>
|
||||
<text x="355" y="173" fill="#154072">→ 127.0.0.1:8081</text>
|
||||
<rect x="470" y="136" width="190" height="52" rx="5" fill="#FFF4E5" stroke="#B45309"/>
|
||||
<text x="565" y="157" fill="#B45309">限流反代:/api/v1/(public|auth)</text>
|
||||
<text x="565" y="173" fill="#B45309">10r/s · burst 20(per-IP)</text>
|
||||
<rect x="680" y="136" width="170" height="52" rx="5" fill="#FDECEC" stroke="#8B2331"/>
|
||||
<text x="765" y="157" fill="#8B2331">/product · /app/product</text>
|
||||
<text x="765" y="173" fill="#8B2331">OG 注入 → 回代后端</text>
|
||||
<text x="450" y="252" fill="#6E7888" font-size="11">证书 /etc/letsencrypt/live/jiu.51yanmei.com/ · client_max_body_size 20m · /import 超时 300s · 安全头 HSTS/XFO/nosniff</text>
|
||||
</g>
|
||||
<line x1="450" y1="270" x2="450" y2="308" stroke="#6E7888" marker-end="url(#ar)"/>
|
||||
<text x="470" y="294" font-size="11" fill="#6E7888">proxy_pass 127.0.0.1:8081</text>
|
||||
<!-- backend -->
|
||||
<rect x="180" y="310" width="540" height="56" rx="8" fill="#fff" stroke="#2563AC" stroke-width="1.5"/>
|
||||
<text x="450" y="333" text-anchor="middle" font-size="13" font-weight="700" fill="#154072">jiu-server(Go / Gin)— systemd jiu.service</text>
|
||||
<text x="450" y="352" text-anchor="middle" font-size="11" fill="#6E7888">/opt/jiu/backend/jiu-server · EnvironmentFile=/opt/jiu/config/production.env(SERVER_PORT=8081)</text>
|
||||
<line x1="450" y1="366" x2="450" y2="402" stroke="#6E7888" marker-end="url(#ar)"/>
|
||||
<!-- mysql -->
|
||||
<rect x="280" y="404" width="340" height="50" rx="8" fill="#fff" stroke="#2E8B57" stroke-width="1.5"/>
|
||||
<text x="450" y="425" text-anchor="middle" font-size="13" font-weight="700" fill="#2E8B57">MySQL 8 — docker 容器 jiu_mysql</text>
|
||||
<text x="450" y="443" text-anchor="middle" font-size="11" fill="#6E7888">127.0.0.1:3306 · 库 jiu_db · 每日备份见 §6</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="callout warn">阿里机 8080 已被 pay 项目 <code>payd</code> 占用,后端端口固定 <b>8081</b>;本地开发默认仍是 8080。改 nginx 时只动 jiu 的 server block(<code>/etc/nginx/conf.d/jiu.conf</code>),别碰同机其他站点。</div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<h2 id="backend">2. 后端(Go / Gin)</h2>
|
||||
|
||||
<h3>2.1 目录分层</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:32%">目录 / 文件</th><th>职责</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">main.go</td><td>入口:配置加载、生产前置检查(CORS/JWT 密钥)、连库、AutoMigrate、启动回填、SetTrustedProxies、挂路由</td></tr>
|
||||
<tr><td class="mono">cmd/</td><td>一次性/运维工具(seed、gencode、import-history、fix-inventory-products、backfill-pinyin,详见 §6.4)</td></tr>
|
||||
<tr><td class="mono">config/</td><td>Viper 配置结构体 + <code>config.yaml</code>(本地,不入 git)+ <code>version.yaml</code>(发版清单,/version 实时读)</td></tr>
|
||||
<tr><td class="mono">internal/handler/</td><td>HTTP 处理器,每业务模块一个文件;参数校验 + 组织响应,<code>*_test.go</code> 为 SQLite in-memory 集成测试</td></tr>
|
||||
<tr><td class="mono">internal/service/</td><td>业务逻辑层:auth(登录/失败锁定)、license(兑换券)、stock(审核/库存事务)、migrate(启动回填)、session_cleanup</td></tr>
|
||||
<tr><td class="mono">internal/model/</td><td>GORM 模型;<code>base.go</code> 提供 Base/TenantBase/Date/JSON 公共类型</td></tr>
|
||||
<tr><td class="mono">internal/middleware/</td><td>JWT 鉴权 + 会话校验、角色拦截(ReadOnly/AdminOnly/SuperAdminOnly)、LicenseGuard、限流</td></tr>
|
||||
<tr><td class="mono">internal/util/</td><td>通用工具(<code>pinyin.go</code> 拼音搜索列生成等)</td></tr>
|
||||
<tr><td class="mono">internal/router/router.go</td><td>全部路由注册与中间件编排(§5 的唯一真相源)</td></tr>
|
||||
<tr><td class="mono">schema/schema.sql</td><td>完整建表 DDL(新装用;线上结构演进靠 AutoMigrate)</td></tr>
|
||||
<tr><td class="mono">seeds/</td><td>门店种子数据 SQL(S001 全量测试数据、S002 空门店)</td></tr>
|
||||
<tr><td class="mono">testutil/setup.go</td><td>测试工具:SQLite in-memory DB、CreateTestXxx 工厂、GetAuthToken</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>2.2 JWT 与多租户隔离</h3>
|
||||
<p class="gdesc">源:<code>backend/internal/middleware/auth.go</code>。JWT(HS256,Access 60 分钟 / Refresh 7 天)Claims 携带 <code>user_id / shop_id / role / sid / lic_exp</code>。带 <code>sid</code> 的 token 每请求校验 <code>user_sessions</code>(撤销/禁用即时下线,<code>last_seen_at</code> 30s 节流刷新)。</p>
|
||||
<div class="callout danger"><b>铁律:</b><code>shop_id</code> 永远用 <code>middleware.GetShopID(c)</code> 从 JWT 取,<b>绝不</b>从请求参数/URL/请求体读;所有查询必须带 <code>WHERE shop_id = ?</code>。违反即多租户越权。</div>
|
||||
|
||||
<h3>2.3 角色中间件</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:26%">中间件</th><th style="width:30%">挂载位置</th><th>行为</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">ReadOnly()</td><td>业务路由组全局 + /license</td><td><code>role=readonly</code> 的非 GET 请求 → 403(<code>code: READONLY_USER</code>)</td></tr>
|
||||
<tr><td class="mono">AdminOnly()</td><td>/users 组、/shop 写、/sessions/:id 删</td><td>仅 <code>admin / superadmin</code> 放行</td></tr>
|
||||
<tr><td class="mono">SuperAdminOnly()</td><td>/admin 组</td><td>仅 <code>superadmin</code> 放行(清数据/对账/错误报告/反馈后台)</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<p class="gdesc">角色顺序:<code>superadmin > admin > operator > readonly</code>。新增写路由默认已受 ReadOnly 保护;仅管理员可用的要再包 AdminOnly 子组。</p>
|
||||
|
||||
<h3>2.4 LicenseGuard(授权过期降级)</h3>
|
||||
<p class="gdesc">源:<code>middleware/license_guard.go</code>。按<b>当前 DB</b> 的有效授权实时算 phase(每店 30s 缓存,激活/续费调用 <code>InvalidateLicensePhase</code> 即时生效),不信任登录时 token 快照:</p>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:18%">phase</th><th style="width:28%">条件</th><th>效果</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><span class="tag pub">normal</span></td><td>未过期 / 永久授权</td><td>正常读写</td></tr>
|
||||
<tr><td><span class="tag admin">grace</span></td><td>过期 0–7 天</td><td>可写,前端展示提醒横幅</td></tr>
|
||||
<tr><td><span class="tag admin">readonly</span></td><td>过期 7–15 天</td><td>非 GET → 403(<code>phase: readonly</code>)</td></tr>
|
||||
<tr><td><span class="tag super">locked</span></td><td>过期 15+ 天,或授权全部被停用(吊销)</td><td>所有业务请求 403(<code>phase: locked</code>)</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<p class="gdesc"><code>/license/*</code> 与 登出/心跳/在线会话 挂在 LicenseGuard <b>之外</b>——锁定时仍能查看授权状态并激活新码。</p>
|
||||
|
||||
<h3>2.5 限流</h3>
|
||||
<p class="gdesc">源:<code>middleware/ratelimit.go</code>(进程内令牌桶 + janitor 清理,<code>config.C.RateLimit.Enabled=false</code> 可整体关闭)。两个维度:</p>
|
||||
<ul style="font-size:13px">
|
||||
<li><b>per-IP</b>(未鉴权接口逐条挂):login / refresh / register / errors / 公开商品读 / 店铺商品列表各自独立配额,IP 取自 nginx 写入的 <code>X-Real-IP</code>(main.go 只信任 127.0.0.1 代理,不可伪造)。</li>
|
||||
<li><b>per-Shop</b>(挂在 JWT 之后,作用于全部已鉴权路由):按 <code>shop_id</code> 限 RPS+burst,防单店打爆共享后端。</li>
|
||||
</ul>
|
||||
<p class="gdesc">超限返回 429 + <code>Retry-After: 60</code>。nginx 层对 <code>/api/v1/(public|auth)/</code> 还有最外层 10r/s 兜底(§1)。</p>
|
||||
|
||||
<h3>2.6 单据审核流状态机</h3>
|
||||
<p class="gdesc">入库单 / 出库单同一状态机(<code>service/stock.go</code> + <code>handler/stock_in.go</code>、<code>stock_out.go</code>)。审核通过时同一事务内:更新 <code>inventories</code>(FOR UPDATE 锁行)+ 写 <code>inventory_logs</code> + 生成财务应收/应付;出库先校验库存充足,不足回滚。</p>
|
||||
<div class="svgwrap">
|
||||
<svg viewBox="0 0 900 300" role="img" aria-label="单据审核流状态机">
|
||||
<defs>
|
||||
<marker id="ar2" markerWidth="8" markerHeight="8" refX="7" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L8,3.5 L0,7 Z" fill="#6E7888"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<g font-size="13" text-anchor="middle" font-weight="700">
|
||||
<rect x="50" y="90" width="140" height="46" rx="23" fill="#EEF2F8" stroke="#6E7888"/><text x="120" y="118" fill="#232934">draft 草稿</text>
|
||||
<rect x="330" y="90" width="150" height="46" rx="23" fill="#FFF4E5" stroke="#B45309"/><text x="405" y="118" fill="#B45309">pending 审核中</text>
|
||||
<rect x="640" y="30" width="160" height="46" rx="23" fill="#E6F3EC" stroke="#2E8B57"/><text x="720" y="58" fill="#2E8B57">approved 已审核</text>
|
||||
<rect x="640" y="160" width="160" height="46" rx="23" fill="#FDECEC" stroke="#D14343"/><text x="720" y="188" fill="#D14343">rejected 已拒绝</text>
|
||||
</g>
|
||||
<g font-size="11.5" fill="#6E7888">
|
||||
<!-- submit -->
|
||||
<line x1="190" y1="107" x2="328" y2="107" stroke="#6E7888" marker-end="url(#ar2)"/>
|
||||
<text x="259" y="99" text-anchor="middle">PUT /:id/submit</text>
|
||||
<!-- withdraw back edge -->
|
||||
<path d="M340,136 C280,180 220,180 195,140" fill="none" stroke="#B45309" stroke-dasharray="5 4" marker-end="url(#ar2)"/>
|
||||
<text x="268" y="185" text-anchor="middle" fill="#B45309">withdraw 撤回(管理员任意单 / 操作员限本人单)</text>
|
||||
<!-- approve -->
|
||||
<line x1="480" y1="98" x2="637" y2="60" stroke="#2E8B57" marker-end="url(#ar2)"/>
|
||||
<text x="545" y="65" text-anchor="middle" fill="#2E8B57">approve 审核通过</text>
|
||||
<!-- reject -->
|
||||
<line x1="480" y1="122" x2="637" y2="176" stroke="#D14343" marker-end="url(#ar2)"/>
|
||||
<text x="545" y="165" text-anchor="middle" fill="#D14343">reject 拒绝</text>
|
||||
<!-- approved actions -->
|
||||
<rect x="560" y="230" width="320" height="58" rx="6" fill="#F0F6FF" stroke="#2563AC"/>
|
||||
<line x1="720" y1="76" x2="720" y2="228" stroke="#2563AC" stroke-dasharray="4 4" marker-end="url(#ar2)"/>
|
||||
<text x="720" y="250" text-anchor="middle" fill="#154072">approved 后续动作(POST,service 内判权):</text>
|
||||
<text x="720" y="268" text-anchor="middle" fill="#154072">return 退单(冲库存+冲应收/应付) · confirm-cost 确认进价(入库)</text>
|
||||
<text x="720" y="283" text-anchor="middle" fill="#154072">confirm-sale 确认售价(出库,重算应收/利润)</text>
|
||||
<!-- draft edit note -->
|
||||
<text x="120" y="160" text-anchor="middle">draft 可改/可删</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h3>2.7 AutoMigrate 与启动回填</h3>
|
||||
<ul style="font-size:13px">
|
||||
<li><b>AutoMigrate</b>(main.go):启动时对 31 个 model 只增不删地同步表结构;另幂等显式建 <code>uk_shop_code(products)</code> 唯一索引与 <code>idx_so_items_shop_code(stock_out_items)</code>(嵌入字段无法用 struct tag 表达)。</li>
|
||||
<li><b>BackfillPricingColumns</b>(<code>service/migrate.go</code>):2026-07 定价字段消歧的一次性迁移,旧列(unit_price/total_price/total_amount)值拷入新列(cost_price/cost_amount/cost_total/sale_total/sale_amount/profit_total),幂等(新列为 0 才拷),旧列观察一版后手动 DROP;全新安装(无旧列)整体跳过。</li>
|
||||
<li><b>backfillPartnerPinyin</b>(main.go):为 <code>name_pinyin</code> 为空的存量往来单位补拼音搜索列;products 同机制由 handler 写入时生成 + 启动兜底。</li>
|
||||
</ul>
|
||||
|
||||
<h3>2.8 拼音搜索</h3>
|
||||
<p class="gdesc">源:<code>internal/util/pinyin.go</code>(go-pinyin)。<code>ToPinyin(name)</code> 返回全拼 + 首字母,Create/Update 时自动写入 <code>name_pinyin / name_initials</code> 两列;搜索 SQL 同时 LIKE 匹配 name、code、name_pinyin、name_initials(汉字/全拼/首字母三种输入都命中)。新增需要拼音搜索的实体直接复用 <code>ToPinyin()</code>。</p>
|
||||
|
||||
<h3>2.9 客户端异常上报</h3>
|
||||
<p class="gdesc">客户端 <code>POST /api/v1/public/errors</code>(per-IP 限流)→ <code>error_reports</code> 表;超管在 <code>GET /api/v1/admin/errors</code> 查看。前端两处统一捕获(main.dart 的 runZonedGuarded + Dio 拦截器上报 5xx),业务代码只在技术性异常处手动 <code>reportError(e, st)</code>,已知业务错误(AppException)不上报。</p>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<h2 id="frontend">3. 前端(Flutter)</h2>
|
||||
|
||||
<h3>3.1 目录分层</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:26%">目录</th><th>职责</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">lib/core/</td><td>横切基建:api(Dio 封装,401 自动刷新)、auth(AuthNotifier + 持久化)、config(AppConfig,BASE_URL 注入)、router(go_router 登录重定向)、theme(token 三主题)、responsive、update(应用内更新)、errors(reportError)</td></tr>
|
||||
<tr><td class="mono">lib/models/</td><td>与后端 JSON 对应的数据模型</td></tr>
|
||||
<tr><td class="mono">lib/providers/</td><td>Riverpod 状态管理,每业务模块一个 provider(列表缓存/刷新/心跳等)</td></tr>
|
||||
<tr><td class="mono">lib/repositories/</td><td>数据访问层:封装 API 调用,供 provider 消费</td></tr>
|
||||
<tr><td class="mono">lib/screens/</td><td>页面(auth/shell/stock_in/stock_out/inventory/partners/finance/products/devices/settings/about/public/shared)</td></tr>
|
||||
<tr><td class="mono">lib/widgets/</td><td>共享组件;<code>widgets/ds/</code> 为设计系统组件库(唯一样式来源,见 3.3)</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>3.2 数据流</h3>
|
||||
<p class="gdesc">单向:Screen 只组合 ds 组件并 watch provider;provider 调 repository;repository 走 Dio(ApiClient 统一注入 token、401 自动刷新、5xx 自动上报)。</p>
|
||||
<div class="svgwrap">
|
||||
<svg viewBox="0 0 900 90" role="img" aria-label="前端数据流">
|
||||
<defs>
|
||||
<marker id="ar3" markerWidth="8" markerHeight="8" refX="7" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L8,3.5 L0,7 Z" fill="#6E7888"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<g font-size="12.5" text-anchor="middle" font-weight="700">
|
||||
<rect x="20" y="22" width="140" height="46" rx="8" fill="#F0F4FF" stroke="#2563AC"/><text x="90" y="43" fill="#154072">Screen</text><text x="90" y="59" font-size="10.5" font-weight="400" fill="#6E7888">screens/ + ds 组件</text>
|
||||
<rect x="205" y="22" width="150" height="46" rx="8" fill="#F0F4FF" stroke="#2563AC"/><text x="280" y="43" fill="#154072">Provider</text><text x="280" y="59" font-size="10.5" font-weight="400" fill="#6E7888">Riverpod 状态/缓存</text>
|
||||
<rect x="400" y="22" width="150" height="46" rx="8" fill="#F0F4FF" stroke="#2563AC"/><text x="475" y="43" fill="#154072">Repository</text><text x="475" y="59" font-size="10.5" font-weight="400" fill="#6E7888">API 调用封装</text>
|
||||
<rect x="595" y="22" width="140" height="46" rx="8" fill="#FFF4E5" stroke="#B45309"/><text x="665" y="43" fill="#B45309">Dio</text><text x="665" y="59" font-size="10.5" font-weight="400" fill="#6E7888">401 刷新 · 5xx 上报</text>
|
||||
<rect x="780" y="22" width="100" height="46" rx="8" fill="#E6F3EC" stroke="#2E8B57"/><text x="830" y="49" fill="#2E8B57">API</text>
|
||||
</g>
|
||||
<line x1="160" y1="45" x2="203" y2="45" stroke="#6E7888" marker-end="url(#ar3)"/>
|
||||
<line x1="355" y1="45" x2="398" y2="45" stroke="#6E7888" marker-end="url(#ar3)"/>
|
||||
<line x1="550" y1="45" x2="593" y2="45" stroke="#6E7888" marker-end="url(#ar3)"/>
|
||||
<line x1="735" y1="45" x2="778" y2="45" stroke="#6E7888" marker-end="url(#ar3)"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h3>3.3 ds 设计系统(单一真相源)</h3>
|
||||
<p class="gdesc">UI 的真相源是原型 <code>.superpowers/prototype/</code>(tokens + <code>atoms.css</code>);Flutter 侧一对一镜像,屏只组合 ds 组件、不自己堆样式。令牌层由 <code>lib/core/theme/token_source/tokens.css</code> codegen 生成 <code>app_tokens.g.dart / app_dims.g.dart / app_chrome.g.dart</code>——改样式改源后重跑脚本,<b>禁止手改 .g 文件、禁止硬编码色值</b>。</p>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:30%">widgets/ds/ 文件</th><th>镜像对象</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">ds_atoms.dart</td><td>atoms.css 原子组件全集(按钮/输入/徽章/卡片…),尺寸引 AppDims、颜色引 context.tokens</td></tr>
|
||||
<tr><td class="mono">ds_table.dart</td><td>原型 .table + .toolbar + .pager(toolbar 与表连成一卡,pager 卡外透明)</td></tr>
|
||||
<tr><td class="mono">ds_kpi.dart</td><td>原型 .kpi 汇总卡(图标块 .ic 五种 tone)</td></tr>
|
||||
<tr><td class="mono">ds_menu.dart</td><td>原型 .menu 下拉(定位规则照抄 shell.js openMenu)</td></tr>
|
||||
<tr><td class="mono">ds_toast.dart</td><td>原型 .toast 1:1(单例顶替、2.2s 自动消失)</td></tr>
|
||||
<tr><td class="mono">ds_bar_chart.dart</td><td>原型 finance.html 收支趋势分组柱状图(度量逐像素照抄)</td></tr>
|
||||
<tr><td class="mono">grid_combo_cell.dart</td><td>表格内联可搜索下拉单元格(入库/出库编辑网格用)</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<p class="gdesc"><b>闸机 1 — 代码端</b>:<code>client/tool/check_ds_code.mjs</code> 扫描 lib/ 禁 <code>Color(0x..)</code> 与 <code>Colors.x</code> 硬编码(合理特例需 <code>// ds-ignore: 理由</code>),<code>--changed --strict</code> 供 pre-commit。</p>
|
||||
|
||||
<h3>3.4 golden × 3 主题 + fidelity 像素闸</h3>
|
||||
<ul style="font-size:13px">
|
||||
<li><b>golden</b>:<code>client/test/golden/*_golden_test.dart</code> 对每屏 × 三主题(a/b/c)出基准图 <code>test/golden/goldens/<prefix>_<theme>.png</code>;更新用 <code>flutter test --update-goldens</code>。</li>
|
||||
<li><b>闸机 2 — fidelity</b>:<code>tools/fidelity.mjs</code> 把<b>原型截图当基准</b>(Playwright 截 <code>.superpowers/prototype/screens/*.html</code>,注入 Noto Sans SC 统一字体)与 Flutter golden 做 pixelmatch diff,超逐屏校准阈值即 exit 1;支持 zones 分区阈值抓局部错位。屏注册表在 <code>tools/screens.mjs</code>(fidelity 与人工目检 <code>tools/ds-compare.mjs</code> 共用)。</li>
|
||||
</ul>
|
||||
<pre><code>cd client && flutter test --update-goldens test/golden/inventory_list_golden_test.dart
|
||||
node tools/fidelity.mjs inventory # 仓库根运行;--themes a,b,c;--update 刷新原型基准</code></pre>
|
||||
|
||||
<h3>3.5 响应式</h3>
|
||||
<p class="gdesc">断点统一 <code>context.isMobile</code>(宽 < 600,<code>core/responsive/responsive.dart</code>),弹窗宽度 <code>context.dialogWidth(X)</code>(≤ 屏宽 92%)。列表屏 <code>DataTableCard</code> 必传 <code>mobileCards</code>(窄屏卡片流),窄屏导航为 Drawer 抽屉。平台判断先 <code>kIsWeb</code> 再 <code>dart:io Platform</code>。后端 URL 一律 <code>AppConfig</code>(<code>--dart-define=BASE_URL=...</code> 注入),禁止硬编码。</p>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<h2 id="db">4. 数据库</h2>
|
||||
<p class="gdesc">MySQL 8 · 所有业务表含 <code>shop_id</code> 租户隔离列。<code>schema.sql</code> 定义 26 张表;另有 5 张仅由 AutoMigrate 创建(4 张商品属性字典 + <code>error_reports</code>),共 31 张。<b>完整列级文档见 <a href="../db-schema.html">db-schema.html</a></b>。</p>
|
||||
|
||||
<h3>4.1 分组速览</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:14%">分组</th><th style="width:26%">表</th><th>要点</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>租户与账号</td><td class="mono">shops<br>users<br>user_sessions<br>login_attempts</td><td>shops=租户根(code 唯一);users 角色 ENUM 四档、<code>uk_shop_username</code>;user_sessions 支撑 sid 会话校验/踢人(revoked_at、last_seen_at);login_attempts 记失败登录供锁定</td></tr>
|
||||
<tr><td>授权</td><td class="mono">licenses<br>license_devices<br>license_codes</td><td>时长兑换券模型:license_codes 为平台码池(短码),激活兑入 licenses(expires_at 驱动 LicenseGuard),license_devices 记设备绑定</td></tr>
|
||||
<tr><td>商品与字典</td><td class="mono">products<br>product_categories<br>product_images<br>product_*_options ×6<br>product_description_docs</td><td><b>product = 特有产品/序列号(非 SKU)</b>:code 同店唯一(uk_shop_code),name/series/spec=品牌/型号/版本,含 name_pinyin/name_initials 搜索列、public_id 公开页;字典表(名称/系列/规格/产地/保质期/储存/介绍)供入库选取后建 product</td></tr>
|
||||
<tr><td>仓库与往来</td><td class="mono">warehouses<br>partners</td><td>partners.type=supplier/customer,含拼音搜索列</td></tr>
|
||||
<tr><td>入库</td><td class="mono">stock_in_orders<br>stock_in_items</td><td>单据状态机(§2.6)、<code>uk_order_no(shop_id,order_no)</code>;明细含快照列 + cost_price/cost_amount + 批次/生产日期/有效期</td></tr>
|
||||
<tr><td>出库</td><td class="mono">stock_out_orders<br>stock_out_items</td><td>同状态机;单头 sale_total/profit_total;明细 cost_price(成本快照)+ sale_price/sale_amount;<code>idx_so_items_shop_code</code> 支撑按编码反查</td></tr>
|
||||
<tr><td>库存</td><td class="mono">inventories<br>inventory_logs<br>inventory_checks<br>inventory_check_items</td><td>inventories=批次行(每行一个入库批,stock_in_item_id 溯源,idx_fifo 支撑先进先出);logs 记每次变动 qty_before/after + ref;盘点单 draft→completed</td></tr>
|
||||
<tr><td>财务</td><td class="mono">finance_records</td><td>type=receivable/payable/receipt/payment,balance=操作后余额,status=open/closed,ref_type/ref_id 关联单据</td></tr>
|
||||
<tr><td>其他</td><td class="mono">number_rules<br>feedbacks<br>error_reports</td><td>number_rules 每店每类型一行(uk_shop_type),单号=前缀+日期+6 位序号;feedbacks 意见反馈;error_reports 客户端异常</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>4.2 定价字段口径(2026-07 消歧)</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:30%">新列</th><th style="width:18%">旧列(弃用)</th><th>口径</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">stock_in_items.cost_price</td><td class="mono">unit_price</td><td>进价 · 单瓶</td></tr>
|
||||
<tr><td class="mono">stock_in_items.cost_amount</td><td class="mono">total_price</td><td>总进价 = quantity × cost_price</td></tr>
|
||||
<tr><td class="mono">stock_in_orders.cost_total</td><td class="mono">total_amount</td><td>应付合计 = Σ 明细 cost_amount</td></tr>
|
||||
<tr><td class="mono">stock_out_items.cost_price</td><td class="mono">unit_price</td><td>成本单价(入库成本快照)</td></tr>
|
||||
<tr><td class="mono">stock_out_items.cost_amount</td><td class="mono">total_price</td><td>成本小计 = quantity × cost_price</td></tr>
|
||||
<tr><td class="mono">stock_out_items.sale_price</td><td class="mono">—(新增)</td><td>实际销售单价,0 = 待定价</td></tr>
|
||||
<tr><td class="mono">stock_out_items.sale_amount</td><td class="mono">—(新增)</td><td>售价小计 = quantity × sale_price(待定价 = 0)</td></tr>
|
||||
<tr><td class="mono">stock_out_orders.sale_total</td><td class="mono">total_amount</td><td>应收合计 = Σ 明细 sale_amount</td></tr>
|
||||
<tr><td class="mono">stock_out_orders.profit_total</td><td class="mono">—(新增)</td><td>总利润 = Σ(sale_price>0 ? (sale_price−cost_price)×qty : 0),建单落库,确认售价/进价联动重算</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<div class="callout warn">行利润只统计<b>已定价</b>行(sale_price>0);待定价行不计利润也不计应收。旧列已停止读写,由启动回填拷值(§2.7),观察一版后手动 DROP——期间<b>勿</b>新代码引用旧列。</div>
|
||||
|
||||
<h3>4.3 关键机制</h3>
|
||||
<ul style="font-size:13px">
|
||||
<li><b>批次行 + 快照列</b>:<code>inventories</code> 每行 = 某 product 在某仓的一个入库批;出入库明细与库存行都带 <code>product_code/product_name/series/spec/...</code> 快照列(导入/审核时拷贝),显示优先 product、回退快照(<code>COALESCE</code> / 前端 <code>lineOrProduct</code>)。<b>快照列保留,勿删。</b></li>
|
||||
<li><b>生成列</b>:<code>inventory_check_items.diff_qty</code> 为 <code>GENERATED ALWAYS AS (actual_qty - system_qty) STORED</code>,不可写。</li>
|
||||
<li><b>FOR UPDATE 并发锁</b>:单号生成(number_rules)、库存扣减(inventories)等 check-then-act 一律事务内锁行;<code>products.code</code> 撞 <code>uk_shop_code</code> 时 max+1 重试(最多 5 次)。</li>
|
||||
<li><b>custom_fields JSON</b>:新业务字段优先放 JSON 扩展列,不轻易加物理列。</li>
|
||||
</ul>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<h2 id="api">5. API 参考</h2>
|
||||
<p class="gdesc">逐条核对自 <code>backend/internal/router/router.go</code>。权限图例:<span class="tag pub">公开</span> 无需登录(多带 per-IP 限流);<span class="tag jwt">JWT</span> 登录即可(readonly 角色写操作被 ReadOnly 拦);<span class="tag admin">Admin</span> = AdminOnly;<span class="tag super">Super</span> = SuperAdminOnly;<span class="tag rl">限流</span> = 独立 IP 限流。所有 <span class="tag jwt">JWT</span> 路由另受 per-Shop 限流;业务组(商品及以下)还受 LicenseGuard(§2.4)。</p>
|
||||
|
||||
<h3>5.1 根路由 / 认证 / 公开接口</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="m get">GET</td><td class="mono">/health</td><td>健康检查(连通性探测)</td><td><span class="tag pub">公开</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/version</td><td>版本清单(实时读 version.yaml)</td><td><span class="tag pub">公开</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/product/:public_id</td><td>扫码分享页(OG 标签注入的 index.html)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/auth/login</td><td>登录(失败锁定 + IP 限流双保险)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/auth/refresh</td><td>刷新 token</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/public/products/:public_id</td><td>公开商品详情(扫码页数据)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/public/shops/:shop_code/products</td><td>店铺公开商品列表(限流最紧)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/public/release</td><td>版本 + 下载链接 + changelog(官网/更新用)</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/public/errors</td><td>客户端异常上报 → error_reports</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/public/register</td><td>门店注册</td><td><span class="tag pub">公开</span> <span class="tag rl">限流</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>5.2 会话 / 授权(豁免 LicenseGuard)</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/auth/logout</td><td>登出(撤销会话)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/auth/ping</td><td>在线心跳</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/sessions</td><td>在线会话列表</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/sessions/:id</td><td>强制下线</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/license/info</td><td>授权状态(与 LicenseGuard 同一取数口径)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/license/activate</td><td>兑换/激活授权码(锁定期仍可用)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/license/verify</td><td>校验当前授权</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/license/deactivate</td><td>解绑设备</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/license/devices</td><td>已绑定设备列表</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/license/purchase</td><td>在线购买/续费下单,返回 pay 收银台 pay_url(契约 pay-contract v1.0.0)</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/license/purchase/:out_trade_no</td><td>购买单状态(支付结果页轮询,本店可见)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/pay/callback</td><td>pay 支付成功 webhook(HMAC 验签+幂等+金额核对后续期)</td><td><span class="tag pub">公开</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>5.3 商品 / 仓库 / 往来单位</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/products</td><td>商品列表(汉字/全拼/首字母/编码搜索)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/products</td><td>新建商品(独立序列号,code 自增 max+1)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/products/find-or-create</td><td>按编号找/建 product(导入用;禁止按名称合并)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/products/:id/detail</td><td>商品详情(含图片/字典属性)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/products/:id/price-history</td><td>进价/售价历史</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/products/:id/qrcode</td><td>公开页二维码</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/products/:id</td><td>更新商品</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/products/:id</td><td>删除商品(软删)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/products/:id/images</td><td>上传商品图片</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/products/:id/images/:image_id</td><td>删除商品图片</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/warehouses</td><td>仓库列表</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/warehouses</td><td>新建仓库</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/warehouses/:id</td><td>更新仓库</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/warehouses/:id</td><td>删除仓库</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/partners</td><td>往来单位列表(?type&keyword,拼音搜索)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/partners</td><td>新建往来单位</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/partners/:id</td><td>更新往来单位</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/partners/:id</td><td>删除往来单位</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>5.4 入库 / 出库</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-in/orders</td><td>入库单列表(?status&keyword,编码双路匹配)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-in/summary</td><td>入库汇总(KPI 卡)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-in/orders/:id</td><td>入库单详情</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-in/orders</td><td>创建入库单(每明细行新建独立 product)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id</td><td>更新(仅 draft)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/stock-in/orders/:id</td><td>删除(仅 draft)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id/submit</td><td>提交审核 draft→pending</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id/approve</td><td>审核通过(事务:+库存 +流水 +应付)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id/reject</td><td>拒绝</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-in/orders/:id/withdraw</td><td>撤回 pending→draft(管理员任意/操作员本人)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-in/orders/:id/return</td><td>退单(库存删除 + 冲应付;service 内判管理员)</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-in/orders/:id/confirm-cost</td><td>确认进价(暂估 0 价→真实价,前向补偿)</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-out/orders</td><td>出库单列表(?status&keyword)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-out/summary</td><td>出库汇总</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/stock-out/orders/:id</td><td>出库单详情</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-out/orders</td><td>创建出库单</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id</td><td>更新(仅 draft)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m del">DELETE</td><td class="mono">/api/v1/stock-out/orders/:id</td><td>删除(仅 draft)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id/submit</td><td>提交审核</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id/approve</td><td>审核通过(校验库存充足,事务扣减 FIFO)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id/reject</td><td>拒绝</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/stock-out/orders/:id/withdraw</td><td>撤回 pending→draft</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-out/orders/:id/return</td><td>退单(库存加回 + 冲应收;管理员或本人)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/stock-out/orders/:id/confirm-sale</td><td>确认售价(先出后定价,重算应收/利润)</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>5.5 库存 / 财务 / 用户 / 店铺 / 反馈 / 编号规则</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/inventory</td><td>库存列表(?warehouse_id&keyword&series&spec&in_stock)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/inventory/summary</td><td>库存汇总</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/inventory/logs</td><td>库存流水</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/inventory/:id/remark</td><td>更新库存行备注</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/inventory/checks</td><td>创建盘点单</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/inventory/checks/:id</td><td>盘点单详情</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/inventory/checks/:id/complete</td><td>完成盘点(盘盈盘亏落库)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/finance/records</td><td>财务流水列表(含日期区间)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/finance/records</td><td>手工记账(收款/付款)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/finance/records/:id/close</td><td>结清单条应收/应付</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/finance/records/close-by-ref</td><td>按关联单据结清</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/finance/summary</td><td>财务汇总</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/finance/trend</td><td>收支趋势(柱状图数据)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/users</td><td>用户列表</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/users</td><td>新建用户</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/users/:id</td><td>更新用户(角色/启用)</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/users/:id/reset-password</td><td>重置密码</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/shop/info</td><td>店铺信息</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/shop/info</td><td>更新店铺信息</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/shop/logo</td><td>上传店铺 logo</td><td><span class="tag admin">Admin</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/feedback</td><td>提交意见反馈</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/feedback/images</td><td>上传反馈附图</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/number-rules</td><td>单号规则列表</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m put">PUT</td><td class="mono">/api/v1/number-rules/:id</td><td>更新单号规则</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>5.6 数据导入(Excel)</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/products</td><td>导入商品</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/partners</td><td>导入往来单位</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/product-names</td><td>导入名称字典</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/product-series</td><td>导入系列字典</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/product-specs</td><td>导入规格字典</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/product-codes</td><td>导入商品编码</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/stock-in</td><td>导入历史入库单</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/stock-out</td><td>导入历史出库单</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/import/inventory</td><td>导入库存(按编号建 product,禁按名称合并)</td><td><span class="tag jwt">JWT</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>5.7 基础数据字典(/api/v1/product-options)</h3>
|
||||
<p class="gdesc">八组字典同构:每组 <span class="m get">GET</span>(支持 <code>?keyword</code> 服务端搜索)/ <span class="m post">POST</span> / <span class="m put">PUT /:id</span> / <span class="m del">DELETE /:id</span>,共 32 条路由,权限均为 <span class="tag jwt">JWT</span>。</p>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:32%">子路径</th><th>字典</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">/names</td><td>商品名称(品牌)</td></tr>
|
||||
<tr><td class="mono">/series</td><td>系列(型号)</td></tr>
|
||||
<tr><td class="mono">/specs</td><td>规格(版本)</td></tr>
|
||||
<tr><td class="mono">/categories</td><td>香型 / 分类</td></tr>
|
||||
<tr><td class="mono">/origins</td><td>产地</td></tr>
|
||||
<tr><td class="mono">/shelf-lives</td><td>保质期</td></tr>
|
||||
<tr><td class="mono">/storages</td><td>储存方式</td></tr>
|
||||
<tr><td class="mono">/description-docs</td><td>商品介绍文档</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>5.8 超级管理员(/api/v1/admin)</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:8%">Method</th><th style="width:38%">Path</th><th>说明</th><th style="width:16%">权限</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="m post">POST</td><td class="mono">/api/v1/admin/clear-data</td><td>清空本店业务数据</td><td><span class="tag super">Super</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/admin/reconcile</td><td>库存对账(inventories vs logs)</td><td><span class="tag super">Super</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/admin/errors</td><td>客户端异常报告列表</td><td><span class="tag super">Super</span></td></tr>
|
||||
<tr><td class="m get">GET</td><td class="mono">/api/v1/admin/feedback</td><td>意见反馈列表</td><td><span class="tag super">Super</span></td></tr>
|
||||
<tr><td class="m patch">PATCH</td><td class="mono">/api/v1/admin/feedback/:id</td><td>更新反馈处理状态</td><td><span class="tag super">Super</span></td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<h2 id="ops">6. 数据操作与运维</h2>
|
||||
|
||||
<h3>6.1 Schema 管理</h3>
|
||||
<p class="gdesc"><code>backend/schema/schema.sql</code> = 新装环境的完整 DDL(docker 初始化时执行);<b>线上结构演进只靠启动时 AutoMigrate(只增不删)</b>,没有独立迁移文件。表结构变更 = 同步改 schema.sql + 对应 model。改生产库数据前<b>先备份</b>。</p>
|
||||
|
||||
<h3>6.2 种子数据</h3>
|
||||
<pre><code>sh scripts/dev.sh seed S001 # 清空并写入 S001 全量测试数据(docker exec 注入 MySQL 容器)
|
||||
sh scripts/dev.sh seed S002 # 空门店(基础数据相同,无单据/库存)</code></pre>
|
||||
<p class="gdesc">每店一个 <code>backend/seeds/<shop_code>.sql</code>,文件顶部含 TRUNCATE,每次完整重建。</p>
|
||||
|
||||
<h3>6.3 每日备份</h3>
|
||||
<p class="gdesc"><code>.gitea/workflows/backup.yml</code>:每日北京时间 02:00(cron UTC 18:00)在 mac runner(开发者本机)跑 <code>scripts/ci/backup-db.sh</code>——SSH 到阿里主库机 dump MySQL,落到本机 <code>~/jiu-db-backups</code>,Telegram 通知结果;也可 workflow_dispatch 手动触发(外网 nginx 挡 API 触发,需 Forgejo UI 操作)。</p>
|
||||
|
||||
<h3>6.4 一次性工具(backend/cmd/)</h3>
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:26%">工具</th><th>用途</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">cmd/seed</td><td>初始化/重置测试数据(<code>--reset</code> 删表重建、<code>--clear</code> 清数据)</td></tr>
|
||||
<tr><td class="mono">cmd/gencode</td><td>平台方批量生成授权兑换券写入码池:<code>go run ./cmd/gencode -type annual -days 365 -count 100 -batch 2026-summer</code></td></tr>
|
||||
<tr><td class="mono">cmd/import-history</td><td>旧系统历史进销存一次性迁入(历史只读、4 列快照直存、不回放库存)</td></tr>
|
||||
<tr><td class="mono">cmd/fix-inventory-products</td><td>修复历史库存导入按「名称|系列|规格」错误合并 product 的数据(按快照编号重指)</td></tr>
|
||||
<tr><td class="mono">cmd/backfill-pinyin</td><td>为存量 products 补 name_pinyin/name_initials(现启动已自动兜底)</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
|
||||
<h3>6.5 生产环境配置</h3>
|
||||
<p class="gdesc">生产 env 在 <code>/opt/jiu/config/production.env</code>(systemd EnvironmentFile 加载):<code>SERVER_PORT=8081</code>、<code>DATABASE_DSN</code>、<code>JWT_SECRET</code>(生产必须非默认值否则拒绝启动)、CORS origin(生产禁 <code>*</code>)。密钥不入 git,需要时从 Bitwarden 取。</p>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<h2 id="workflow">7. 开发环境与工作流</h2>
|
||||
|
||||
<h3>7.1 本地开发</h3>
|
||||
<pre><code># 首次:起 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=只读</code></pre>
|
||||
<p class="gdesc">Go 命令前须 <code>export PATH="/opt/homebrew/bin:$PATH"</code>(dev.sh 已内置)。发版前自测「指向线上后端」的 macOS 包:<code>sh scripts/local_test.sh</code>(BASE_URL 固定生产域名,版本取最新 client tag patch+1)。</p>
|
||||
|
||||
<h3>7.2 完成标准(DoD 门禁)</h3>
|
||||
<pre><code># 后端
|
||||
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 原型像素闸(仓库根)</code></pre>
|
||||
<div class="callout danger">任一项失败禁止提交、禁止打 tag 发版。通用检查:多租户隔离未破坏(查询都带 shop_id)、schema.sql 与 model 同步、新接口有测试覆盖。</div>
|
||||
|
||||
<h3>7.3 Git 提交规范</h3>
|
||||
<pre><code>{类型}({模块}): {简短说明} # 类型: feat|fix|test|docs|chore|security|refactor
|
||||
# 模块: backend|client|db|deploy|docs
|
||||
feat(backend): 新增财务报表接口
|
||||
docs(api): 财务报表 API 接口文档</code></pre>
|
||||
|
||||
<h3>7.4 三条发版流水线</h3>
|
||||
<p class="gdesc">client / site / server 三条互不影响的流水线,各有 tag 前缀、独立版本序列、独立 CHANGELOG,由 Forgejo CI 按 tag 前缀路由(<code>.gitea/workflows/deploy-*.yml</code> + <code>scripts/ci/</code>)。用 <code>/release <part> [version]</code>:本地 build → test → 更新 CHANGELOG → commit → tag → push;省略版本则自增最新 tag 的 patch。回滚用 <code>manual.yml</code> 输入带前缀 tag,从 Forgejo Release 下载产物重部署。</p>
|
||||
|
||||
<div class="svgwrap">
|
||||
<svg viewBox="0 0 900 400" role="img" aria-label="发版流水线拓扑">
|
||||
<defs>
|
||||
<marker id="ar4" markerWidth="8" markerHeight="8" refX="7" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L8,3.5 L0,7 Z" fill="#6E7888"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<g font-size="11.5">
|
||||
<!-- lane labels -->
|
||||
<rect x="20" y="20" width="110" height="40" rx="6" fill="#F0F4FF" stroke="#2563AC"/>
|
||||
<text x="75" y="37" text-anchor="middle" font-weight="700" fill="#154072">client-v*</text>
|
||||
<text x="75" y="52" text-anchor="middle" fill="#6E7888" font-size="10">CHANGELOG-client</text>
|
||||
<rect x="20" y="230" width="110" height="40" rx="6" fill="#F0F4FF" stroke="#2563AC"/>
|
||||
<text x="75" y="247" text-anchor="middle" font-weight="700" fill="#154072">site-v*</text>
|
||||
<text x="75" y="262" text-anchor="middle" fill="#6E7888" font-size="10">CHANGELOG-site</text>
|
||||
<rect x="20" y="310" width="110" height="40" rx="6" fill="#F0F4FF" stroke="#2563AC"/>
|
||||
<text x="75" y="327" text-anchor="middle" font-weight="700" fill="#154072">server-v*</text>
|
||||
<text x="75" y="342" text-anchor="middle" fill="#6E7888" font-size="10">CHANGELOG-server</text>
|
||||
|
||||
<!-- client lane: workflow box -->
|
||||
<rect x="160" y="20" width="560" height="180" rx="8" fill="#fff" stroke="#154072" stroke-width="1.2"/>
|
||||
<text x="440" y="40" text-anchor="middle" font-weight="700" fill="#154072">deploy-client.yml — 五端构建矩阵</text>
|
||||
<!-- mac serial chain -->
|
||||
<text x="180" y="63" fill="#6E7888" font-size="10.5">mac runner(本机,容量 1,串行链):</text>
|
||||
<rect x="180" y="72" width="118" height="34" rx="5" fill="#E6F3EC" stroke="#2E8B57"/><text x="239" y="93" text-anchor="middle" fill="#2E8B57">build-client-web</text>
|
||||
<rect x="322" y="72" width="100" height="34" rx="5" fill="#E6F3EC" stroke="#2E8B57"/><text x="372" y="93" text-anchor="middle" fill="#2E8B57">build-macos</text>
|
||||
<rect x="446" y="72" width="108" height="34" rx="5" fill="#E6F3EC" stroke="#2E8B57"/><text x="500" y="93" text-anchor="middle" fill="#2E8B57">build-android</text>
|
||||
<rect x="578" y="72" width="90" height="34" rx="5" fill="#E6F3EC" stroke="#2E8B57"/><text x="623" y="93" text-anchor="middle" fill="#2E8B57">build-ios</text>
|
||||
<line x1="298" y1="89" x2="320" y2="89" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
<line x1="422" y1="89" x2="444" y2="89" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
<line x1="554" y1="89" x2="576" y2="89" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
<!-- windows parallel -->
|
||||
<text x="180" y="130" fill="#6E7888" font-size="10.5">windows runner(并行):</text>
|
||||
<rect x="180" y="138" width="150" height="34" rx="5" fill="#FFF4E5" stroke="#B45309"/><text x="255" y="159" text-anchor="middle" fill="#B45309">build-windows (Inno)</text>
|
||||
<!-- release job -->
|
||||
<rect x="520" y="138" width="180" height="40" rx="5" fill="#F0F4FF" stroke="#2563AC"/>
|
||||
<text x="610" y="155" text-anchor="middle" fill="#154072" font-weight="700">release-deploy-client</text>
|
||||
<text x="610" y="170" text-anchor="middle" fill="#6E7888" font-size="10">收齐产物 → Release → 部署</text>
|
||||
<line x1="623" y1="106" x2="617" y2="136" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
<line x1="330" y1="155" x2="518" y2="157" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
<text x="440" y="192" text-anchor="middle" fill="#6E7888" font-size="10">Android 需 ANDROID_* / iOS TestFlight 需 IOS_*·APPSTORE_* secrets,未配置优雅跳过</text>
|
||||
<line x1="130" y1="40" x2="158" y2="40" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
|
||||
<!-- site lane -->
|
||||
<rect x="160" y="230" width="560" height="44" rx="8" fill="#fff" stroke="#154072" stroke-width="1.2"/>
|
||||
<text x="440" y="257" text-anchor="middle" fill="#154072">deploy-site.yml — Eleventy 构建 → rsync /opt/jiu/marketing(nginx 根路径)</text>
|
||||
<line x1="130" y1="250" x2="158" y2="250" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
|
||||
<!-- server lane -->
|
||||
<rect x="160" y="310" width="560" height="44" rx="8" fill="#fff" stroke="#154072" stroke-width="1.2"/>
|
||||
<text x="440" y="337" text-anchor="middle" fill="#154072">deploy-server.yml — go build+test → Release → 部署 jiu-server + nginx/systemd</text>
|
||||
<line x1="130" y1="330" x2="158" y2="330" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
|
||||
<!-- deploy target -->
|
||||
<rect x="750" y="130" width="130" height="130" rx="8" fill="#E6F3EC" stroke="#2E8B57" stroke-width="1.5"/>
|
||||
<text x="815" y="170" text-anchor="middle" font-weight="700" fill="#2E8B57">阿里云主机</text>
|
||||
<text x="815" y="190" text-anchor="middle" fill="#2E8B57" font-size="10.5">/opt/jiu/*</text>
|
||||
<text x="815" y="207" text-anchor="middle" fill="#2E8B57" font-size="10.5">Forgejo Release</text>
|
||||
<line x1="700" y1="158" x2="748" y2="175" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
<line x1="720" y1="252" x2="748" y2="225" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
<line x1="720" y1="332" x2="748" y2="250" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
<!-- telegram -->
|
||||
<rect x="750" y="290" width="130" height="40" rx="8" fill="#F0F4FF" stroke="#2563AC"/>
|
||||
<text x="815" y="315" text-anchor="middle" fill="#154072">Telegram 通知</text>
|
||||
<line x1="815" y1="260" x2="815" y2="288" stroke="#6E7888" marker-end="url(#ar4)"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="card"><table>
|
||||
<thead><tr><th style="width:12%">part</th><th style="width:34%">范围</th><th style="width:14%">tag 前缀</th><th>要点</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>client</b></td><td class="mono">client/ 全平台 + version.yaml</td><td class="mono">client-v*</td><td>version.yaml 归 client;后端 /version 与 /public/release <b>每请求实时读</b>——client 发版立即生效,不重启后端、不重建官网</td></tr>
|
||||
<tr><td><b>site</b></td><td class="mono">web/ 营销站(不含 Web 版 app)</td><td class="mono">site-v*</td><td>官网下载页运行时 fetch /public/release 动态刷新版本徽章/链接/更新日志</td></tr>
|
||||
<tr><td><b>server</b></td><td class="mono">backend/ + nginx/systemd 基建</td><td class="mono">server-v*</td><td>nginx conf、jiu.service 归 server 流水线</td></tr>
|
||||
</tbody>
|
||||
</table></div>
|
||||
<pre><code>/release client 1.1.1 # 指定版本
|
||||
/release server # 省略则自增最新 server-v* tag 的 patch</code></pre>
|
||||
<p class="gdesc">CI 脚本在 <code>scripts/ci/</code>:<code>lib-forgejo.sh</code>(公共函数)+ <code>compile-{client-web,site,backend,macos,android,ios,windows}.sh</code> + <code>release-*.sh</code> + <code>deploy-*.sh</code>。runner:mac = 开发者本机(launchd 看门狗)、windows = 另一台机(nssm)、Forgejo 在 NAS(git.51yanmei.com)。</p>
|
||||
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:36px 0 12px">
|
||||
<div class="sub">相关文档:<a href="../db-schema.html">数据库 Schema(列级)</a> · <a href="../context/project.md">项目上下文 project.md</a> · <a href="../index.html">文档索引</a></div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,465 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>岩美酒库 · 用户使用手册</title>
|
||||
<style>
|
||||
:root{
|
||||
--primary:#2563AC; --primary-dark:#154072; --danger:#D14343; --danger-bg:#FDECEC;
|
||||
--success:#2E8B57; --success-bg:#E6F3EC; --warn:#B45309; --warn-bg:#FFF4E5; --accent:#8B2331;
|
||||
--ink:#232934; --muted:#6E7888; --border:#DCE2EB; --paper:#F5F7FA; --head:#F0F4FF;
|
||||
}
|
||||
*{box-sizing:border-box;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
|
||||
body{margin:0;background:var(--paper);color:var(--ink);line-height:1.6;}
|
||||
main{max-width:960px;margin:0 auto;padding:30px 26px 60px;}
|
||||
h1{font-size:22px;margin:0 0 4px;}
|
||||
.sub{color:var(--muted);font-size:13px;margin-bottom:6px;}
|
||||
.lead{color:var(--muted);font-size:13.5px;margin:8px 0 18px;}
|
||||
h2{font-size:17px;margin:38px 0 8px;color:var(--primary-dark);border-bottom:2px solid var(--head);padding-bottom:6px;}
|
||||
h3{font-size:14px;margin:20px 0 8px;color:var(--ink);}
|
||||
.who{font-size:12.5px;color:var(--muted);margin:2px 0 12px;}
|
||||
.who b{color:var(--primary-dark);}
|
||||
.card{background:#fff;border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin:14px 0;}
|
||||
table{width:100%;border-collapse:collapse;font-size:12.5px;margin:6px 0;}
|
||||
th{background:var(--head);color:var(--primary-dark);font-weight:600;font-size:11.5px;text-align:left;padding:7px 9px;border-bottom:1px solid var(--border);}
|
||||
td{padding:6px 9px;border-bottom:1px solid #EEF1F6;vertical-align:top;}
|
||||
tr:last-child td{border-bottom:0;}
|
||||
.mono{font-family:ui-monospace,Menlo,monospace;}
|
||||
code{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;background:#EEF2F8;padding:1px 5px;border-radius:4px;color:var(--primary-dark);}
|
||||
kbd{font-family:ui-monospace,Menlo,monospace;font-size:11px;background:#fff;border:1px solid var(--border);border-bottom-width:2px;border-radius:4px;padding:1px 6px;color:var(--ink);}
|
||||
.tag{display:inline-block;font-size:10px;padding:0 6px;border-radius:9px;font-weight:600;line-height:16px;}
|
||||
.tag.pk{background:#FDECEC;color:var(--danger);}
|
||||
.tag.uk{background:#E5EEF9;color:var(--primary);}
|
||||
.tag.ix{background:#EEE;color:var(--muted);}
|
||||
.tag.ok{background:var(--success-bg);color:var(--success);}
|
||||
.tag.warn{background:var(--warn-bg);color:var(--warn);}
|
||||
.callout{border-left:4px solid var(--primary);background:#F0F6FF;padding:9px 13px;border-radius:4px;margin:10px 0 12px;font-size:12.5px;}
|
||||
.callout.danger{border-color:var(--danger);background:var(--danger-bg);}
|
||||
.callout.warn{border-color:var(--warn);background:var(--warn-bg);}
|
||||
.callout.ok{border-color:var(--success);background:var(--success-bg);}
|
||||
ol,ul{margin:8px 0;padding-left:22px;font-size:13px;}
|
||||
li{margin:4px 0;}
|
||||
p{font-size:13px;margin:8px 0;}
|
||||
.toc{background:#fff;border:1px solid var(--border);border-radius:10px;padding:16px 18px;margin:18px 0 6px;}
|
||||
.toc .navtitle{font-size:11px;font-weight:700;color:var(--primary-dark);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;}
|
||||
.toc ol{margin:4px 0;padding-left:22px;font-size:13px;columns:2;column-gap:36px;}
|
||||
.toc a{color:var(--primary);text-decoration:none;}
|
||||
.toc a:hover{text-decoration:underline;}
|
||||
.flow{font-size:13px;background:#fff;border:1px dashed var(--border);border-radius:8px;padding:10px 14px;margin:10px 0;text-align:center;}
|
||||
.flow b{color:var(--primary-dark);}
|
||||
.faq-q{font-weight:700;font-size:13px;margin:14px 0 2px;color:var(--primary-dark);}
|
||||
.faq-a{font-size:13px;margin:0 0 6px;}
|
||||
.top-link{font-size:11px;}
|
||||
a{color:var(--primary);}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>岩美酒库 · 用户使用手册</h1>
|
||||
<div class="sub">适用版本:客户端 v1.1.1 · 更新日期:2026-07-03 · 受众:酒行店员 / 店长</div>
|
||||
<p class="lead">岩美酒库是为酒行门店设计的进销存管理系统:进货入库、卖酒出库、库存盘点、账款往来,一套系统全搞定。每瓶酒有自己的编号和二维码标签,卖出去、退回来都能查到来龙去脉。本手册按日常使用顺序编写,新员工建议先读第 1、2 章。</p>
|
||||
|
||||
<div class="toc" id="toc">
|
||||
<div class="navtitle">目录</div>
|
||||
<ol>
|
||||
<li><a href="#ch1">快速上手</a></li>
|
||||
<li><a href="#ch2">核心概念(必读)</a></li>
|
||||
<li><a href="#ch3">入库管理</a></li>
|
||||
<li><a href="#ch4">出库管理</a></li>
|
||||
<li><a href="#ch5">库存管理</a></li>
|
||||
<li><a href="#ch6">财务管理</a></li>
|
||||
<li><a href="#ch7">往来单位</a></li>
|
||||
<li><a href="#ch8">基础数据</a></li>
|
||||
<li><a href="#ch9">设备与设置</a></li>
|
||||
<li><a href="#ch10">常见问题 FAQ</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 1 快速上手 ═══════════════════════ -->
|
||||
<h2 id="ch1">1. 快速上手</h2>
|
||||
<div class="who"><b>谁用</b>:所有人 · <b>入口</b>:登录页 / 顶栏右上角</div>
|
||||
|
||||
<h3>1.1 注册新门店</h3>
|
||||
<div class="card">
|
||||
<p>第一次使用,先给自己的酒行开一个门店账号。在登录页点「注册新门店」,按页面提示分三步走:</p>
|
||||
<ol>
|
||||
<li><b>填写门店信息</b>:店名、地址、联系人。门店编号由系统自动分配,不用自己起。</li>
|
||||
<li><b>创建管理员账号</b>:填登录账号和密码(密码至少 6 位),这个账号就是本店的管理员,以后由它来添加其他员工。</li>
|
||||
<li><b>激活授权</b>:新门店自带 <b>30 天免费试用</b>,先用起来;有兑换券的话,登录后到「系统设置 → 授权兑换券」输入短码续期。</li>
|
||||
</ol>
|
||||
<p>提交成功后,记下系统分配的<b>门店编号</b>,登录时要用。</p>
|
||||
</div>
|
||||
|
||||
<h3>1.2 登录</h3>
|
||||
<div class="card">
|
||||
<ol>
|
||||
<li>登录页依次输入<b>门店编号</b>、<b>登录账号</b>、<b>密码</b>。</li>
|
||||
<li>勾选「记住我」(默认勾选):下次打开会自动填入最近登录的门店和账号,只需输密码。账号输入框还带历史下拉,多人共用一台电脑时点箭头切换。</li>
|
||||
<li>密码框右侧的小眼睛可以切换密码明文显示,方便核对。</li>
|
||||
</ol>
|
||||
<div class="callout">忘记密码?系统没有自助找回,请找<b>本店管理员</b>在「系统设置 → 用户管理」里帮你重置密码。管理员自己忘了密码,请联系技术支持。</div>
|
||||
</div>
|
||||
|
||||
<h3>1.3 五个平台都能用</h3>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead><tr><th style="width:22%">平台</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>Windows</b></td><td>官网下载安装包,双击安装。推荐收银台/办公电脑使用,支持针式打印机、标签打印机。</td></tr>
|
||||
<tr><td><b>macOS</b></td><td>官网下载 dmg 安装,已通过 Apple 公证,不会弹安全警告。</td></tr>
|
||||
<tr><td><b>Web 网页版</b></td><td>浏览器直接打开官网「立即使用」,免安装,临时用别人电脑也能登。</td></tr>
|
||||
<tr><td><b>Android</b></td><td>官网下载 APK 安装,手机上随时查库存、录单。</td></tr>
|
||||
<tr><td><b>iOS</b></td><td>通过 TestFlight 安装(官网下载页有指引链接)。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>同一个账号可以多端同时登录,数据实时同步;有新版本时应用内会提示更新。</p>
|
||||
</div>
|
||||
|
||||
<h3>1.4 换个顺眼的主题</h3>
|
||||
<div class="card">
|
||||
<p>点<b>右上角的小衣服图标</b>(登录页和主界面都有),三套配色随意换:</p>
|
||||
<ul>
|
||||
<li><b>A 经典蓝</b> — 浅色,默认主题</li>
|
||||
<li><b>B 琥珀</b> — 深色,晚上看着不刺眼</li>
|
||||
<li><b>C 酒窖</b> — 暖浅色</li>
|
||||
</ul>
|
||||
<p>也可以在「系统设置 → 偏好设置」里选,选择会被记住。</p>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 2 核心概念 ═══════════════════════ -->
|
||||
<h2 id="ch2">2. 核心概念(必读)</h2>
|
||||
<div class="who"><b>谁用</b>:所有人。花三分钟读完,后面所有操作都会顺很多。</div>
|
||||
|
||||
<h3>2.1 每瓶酒 = 一个独立编号</h3>
|
||||
<div class="card">
|
||||
<p>岩美酒库不是按「款」记账,而是按「件」记账——<b>入库时每录一行,系统就生成一个新的商品编号</b>。就算是同一款酒、同一个规格,录两行就是两个编号、两张标签。</p>
|
||||
<p>录单时的三个商品字段,含义按这样理解:</p>
|
||||
<table>
|
||||
<thead><tr><th style="width:20%">字段</th><th style="width:20%">实际含义</th><th>示例</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>名称</b></td><td>品牌</td><td>茅台</td></tr>
|
||||
<tr><td><b>系列</b></td><td>型号 / 产品线</td><td>飞天 53°</td></tr>
|
||||
<tr><td><b>规格</b></td><td>版本 / 包装</td><td>500ml</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout ok">好处:每件商品都能贴上带二维码的专属标签,卖出、退回、盘点都能精确到「这一件」,顾客扫码还能验真。所以库存页里同一款酒出现好几行是<b>正常的</b>——每行是不同批次/不同编号的货。</div>
|
||||
</div>
|
||||
|
||||
<h3>2.2 单据审核流:审核通过才动库存</h3>
|
||||
<div class="card">
|
||||
<div class="flow">草稿 → 提交 → <b>待审核</b> → 审核通过(<b>已审核</b>,此刻才增减库存、生成账款) / 拒绝(已拒绝,什么都不变)</div>
|
||||
<ul>
|
||||
<li><b>草稿</b>:随便存,可以反复改、可以删。</li>
|
||||
<li><b>待审核</b>:提交后锁定,不能再编辑。发现录错了用<b>「撤回」</b>退回草稿,改完重新提交——管理员能撤回任何单,操作员只能撤回自己提交的单。</li>
|
||||
<li><b>已审核</b>:库存和账款已经变动。这时想反悔,走<b>「退单」</b>(见第 3、4 章),不能直接改单。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>2.3 四级角色,各看各的</h3>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead><tr><th style="width:18%">角色</th><th style="width:42%">能做什么</th><th>特别说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>超级管理员</b></td><td>全部操作</td><td>本店最高权限</td></tr>
|
||||
<tr><td><b>管理员</b></td><td>全部业务操作 + 用户管理、门店信息、强制下线设备</td><td>能看到<b>成本价与利润</b></td></tr>
|
||||
<tr><td><b>操作员</b></td><td>录单、提交、审核、查询</td><td><b>看不到成本价和利润</b>,出库时只见售价和小计;只能撤回自己的单</td></tr>
|
||||
<tr><td><b>只读</b></td><td>只能看</td><td>所有新增/编辑/删除/审核按钮全部隐藏,顶栏显示「只读」标识</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout">进价、成本、利润这些敏感数字<b>只有管理员(含超管)能看到</b>——不只是前端藏起来,服务器也不会把数字发给操作员账号,放心让店员用。</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 3 入库管理 ═══════════════════════ -->
|
||||
<h2 id="ch3">3. 入库管理</h2>
|
||||
<div class="who"><b>谁用</b>:操作员及以上 · <b>入口</b>:左侧导航「入库管理」</div>
|
||||
|
||||
<h3>3.1 页面一眼看懂</h3>
|
||||
<div class="card">
|
||||
<p>顶部四张统计卡(KPI 卡),后两张<b>点一下就自动筛选列表</b>:</p>
|
||||
<table>
|
||||
<thead><tr><th>卡片</th><th>含义</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>近30天入库笔数 / 金额</td><td>最近 30 天的入库量概览</td></tr>
|
||||
<tr><td>待审核单数</td><td>点击 → 列表只显示待审核单,审核就从这里进</td></tr>
|
||||
<tr><td>草稿单数</td><td>点击 → 列表只显示草稿,接着没录完的单继续录</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>3.2 新建入库单</h3>
|
||||
<div class="card">
|
||||
<ol>
|
||||
<li>点「新建入库单」,先选<b>仓库</b>、<b>供应商</b>、<b>入库日期</b>(下拉都能搜索,支持拼音首字母)。</li>
|
||||
<li>在明细表格里一行一行录货。<b>记住:一行 = 一件独立编号的商品</b>。每行字段:
|
||||
<ul>
|
||||
<li><b>名称 / 系列 / 规格</b>:可搜索下拉;字典里没有的直接输入文字,会提示「新增」,选它就自动加进字典。</li>
|
||||
<li><b>生产日期</b>:点开用滚轮选年月日。</li>
|
||||
<li><b>批次号</b>:<b>必填</b>,没填会拦住不让提交。</li>
|
||||
<li><b>数量</b>:这一行的瓶数。</li>
|
||||
<li><b>进价(单瓶)</b>:<b>可以留空</b>!调货时价格还没谈好就先留空,算「待定价」,之后用「确认进价」补(见 3.4)。</li>
|
||||
<li><b>参考售价</b>:填上后会存到商品档案,<b>出库时自动带出来</b>,强烈建议填。</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>底部「总进价」自动合计,不用自己按计算器。</li>
|
||||
<li>录完点「保存草稿」(还想再改)或「提交审核」。</li>
|
||||
</ol>
|
||||
<div class="callout ok"><b>键盘党提速</b>:<kbd>Enter</kbd> 逐格往后跳;<kbd>⌘D</kbd> / <kbd>Ctrl+D</kbd> 复制上一行(同款酒连续录超快);<kbd>⌘Enter</kbd> / <kbd>Ctrl+Enter</kbd> 直接提交。</div>
|
||||
</div>
|
||||
|
||||
<h3>3.3 提交、审核、撤回</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><b>提交</b>:草稿单点「提交」进入待审核。</li>
|
||||
<li><b>审核</b>:待审核单点「通过」→ 库存增加、自动生成一笔对供应商的<b>应付账款</b>;点「拒绝」→ 什么都不变。</li>
|
||||
<li><b>撤回</b>:待审核单点「撤回」退回草稿,改完重新提交。操作员只能撤自己的单。</li>
|
||||
</ul>
|
||||
<div class="callout warn">审核前请核对好数量和进价——审核一过库存和账就都动了,之后只能靠「退单」纠正。</div>
|
||||
</div>
|
||||
|
||||
<h3>3.4 退单与确认进价(已审核单的两个后悔药)</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><b>退单</b>(仅管理员):已审核单点红色「退单」按钮,弹出退单明细窗,可以逐行勾选退,也可以「全部退单」。退掉的商品从库存删除,同时冲减对供应商的应付。已退的行会标红色「已退单」,列表状态旁显示「部分退单 / 已退单」徽章。</li>
|
||||
<li><b>确认进价</b>:当初进价留空(待定价)的单,价格谈妥后打开单据详情,点「确认进价」补填真实进价。系统自动回填库存成本、已出库商品的成本和对供应商的应付;售价和应收不受影响。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>3.5 找单:搜索与详细搜索</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li>工具栏搜索框:输单号或供应商名称即搜;也支持按<b>酒名</b>搜(汉字、全拼、首字母都行)。</li>
|
||||
<li>时间范围:全部时间 / 近 7 天 / 近 30 天 / 本月 / 自定义区间。</li>
|
||||
<li><b>详细搜索</b>:多条件组合查单——单号、商品名、商品编码、批次号、系列、规格、供应商、录单人、审核员、状态、日期区间。想查「这瓶酒是哪张单进的」,用商品编码搜最准。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>3.6 打印</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><b>打印单据</b>:点「打印」先弹出整页预览,确认无误再打。版式为针式打印机优化过:纯黑、加粗、实线表格,不发糊。</li>
|
||||
<li><b>打印标签</b>:给本单商品逐件打标签(带二维码),见 5.6。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 4 出库管理 ═══════════════════════ -->
|
||||
<h2 id="ch4">4. 出库管理</h2>
|
||||
<div class="who"><b>谁用</b>:操作员及以上 · <b>入口</b>:左侧导航「出库管理」</div>
|
||||
|
||||
<h3>4.1 新建出库单</h3>
|
||||
<div class="card">
|
||||
<ol>
|
||||
<li>点「新建出库单」,选<b>仓库</b>、<b>客户</b>、<b>出库日期</b>。</li>
|
||||
<li>点「添加商品」,从库存选货:弹窗分页展示在库商品,按<b>编码 / 名称 / 系列</b>搜索,输入即时出结果。</li>
|
||||
<li>选中后系统自动帮你填好:<b>数量 = 该商品可用数量</b>、<b>售价 = 入库时填的参考售价</b>,需要再手动调。</li>
|
||||
<li>管理员账号下,每行还有实时<b>利润</b>列 =(售价 − 进价)× 数量,正数绿色、负数红色;底栏同时显示<b>合计金额</b>和<b>合计利润</b>。操作员看不到进价和利润。</li>
|
||||
<li>保存草稿或提交审核。审核通过 → 库存扣减、生成对客户的<b>应收账款</b>。</li>
|
||||
</ol>
|
||||
<div class="callout warn">出库数量不能超过库存,审核时系统会自动校验,不足会拦下来。</div>
|
||||
</div>
|
||||
|
||||
<h3>4.2 待定价出库:先发货,后定价</h3>
|
||||
<div class="card">
|
||||
<p>价格还没谈定也能先出货:<b>售价留空</b>提交,明细显示「待定价」。价格确定后打开单据详情,点<b>「确认售价」</b>补填,系统自动补一笔应收流水。</p>
|
||||
</div>
|
||||
|
||||
<h3>4.3 单据详情</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><b>管理员</b>看到:成本价、售价、总售价、利润四列 + 底部合计金额与合计利润。</li>
|
||||
<li><b>操作员</b>只看到:售价、小计。</li>
|
||||
<li>明细自动带出商品的生产日期和批次号,方便核货。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>4.4 退单</h3>
|
||||
<div class="card">
|
||||
<p>客户退货:已审核出库单点红色「退单」,逐行退或全部退。退回的商品<b>加回库存</b>,同时按<b>售价口径</b>冲减对客户的应收。</p>
|
||||
</div>
|
||||
|
||||
<h3>4.5 打印</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><b>打印单据</b>:带预览的出库单打印(金额按售价显示)。</li>
|
||||
<li><b>打印标签</b>:给出库商品补打带二维码的标签。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 5 库存管理 ═══════════════════════ -->
|
||||
<h2 id="ch5">5. 库存管理</h2>
|
||||
<div class="who"><b>谁用</b>:所有人(只读也能查) · <b>入口</b>:左侧导航「库存管理」</div>
|
||||
|
||||
<h3>5.1 库存怎么看</h3>
|
||||
<div class="card">
|
||||
<p>库存按<b>批次维度</b>展示:同一款酒不同批次分行列出,每行都有自己的编号、批次号、生产日期。顶部 KPI 卡里的「缺货预警」点一下即可只看缺货商品。</p>
|
||||
<table>
|
||||
<thead><tr><th style="width:24%">列</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>商品 / 系列 / 规格 / 批次号</td><td>商品身份信息,商品下方带编码</td></tr>
|
||||
<tr><td>库存</td><td>当前在库数量</td></tr>
|
||||
<tr><td>成本价</td><td>入库进价(单瓶),仅管理员可见;待定价商品显示「待定价」</td></tr>
|
||||
<tr><td>总价</td><td>= 库存 × 成本价,仅管理员可见</td></tr>
|
||||
<tr><td>生产日期 / 入库日期 / 供应商</td><td>批次追溯信息</td></tr>
|
||||
<tr><td>状态</td><td><span class="tag ok">在售</span> <span class="tag warn">预警</span> <span class="tag pk">缺货</span>(按数量与安全库存自动判定)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<ul>
|
||||
<li><b>搜索</b>:商品名(汉字 / 全拼 / 首字母)或商品编码,输入即搜。</li>
|
||||
<li><b>列设置</b>:工具栏「显示字段」按钮勾选要显示的列,选择会被记住。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>5.2 商品编辑抽屉</h3>
|
||||
<div class="card">
|
||||
<p>点击任意库存行,右侧滑出商品编辑抽屉:</p>
|
||||
<ul>
|
||||
<li><b>商品信息</b>:编码、系列、规格、成本价(单瓶)、总成本价(管理员)。</li>
|
||||
<li><b>商品图片</b>:3 个图片槽位,点击上传;<b>双击图片全屏预览</b>,支持双指缩放、左右翻页。</li>
|
||||
<li><b>商品介绍 / 卖点</b>:从介绍库搜索选择一篇介绍挂到商品上(介绍库在「基础数据 → 商品介绍」维护),顾客扫码时能看到。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>5.3 库存盘点</h3>
|
||||
<div class="card">
|
||||
<ol>
|
||||
<li>进入「库存盘点」页,系统自动生成盘点单号。</li>
|
||||
<li>选<b>盘点仓库</b>,账面库存自动加载成盘点明细。</li>
|
||||
<li>选盘点类型(全盘 / 循环盘点)。</li>
|
||||
<li>逐行填<b>实盘数量</b>——和账面一致的不用动,只改对不上的。</li>
|
||||
<li>点「提交盘点」完成。</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h3>5.4 标签打印与扫码防伪</h3>
|
||||
<div class="card">
|
||||
<p>每件商品都有唯一二维码。在库存行点「打印标签」(或入库/出库单里批量打),标签含:商品名、编码、系列、规格、批次号、生产日期、店名和联系方式、二维码。</p>
|
||||
<div class="callout ok">顾客用手机扫标签二维码,会打开这件商品的<b>公开页面</b>:品名、图片、介绍、门店信息一目了然——既是防伪,也是你的线上小名片。</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 6 财务管理 ═══════════════════════ -->
|
||||
<h2 id="ch6">6. 财务管理</h2>
|
||||
<div class="who"><b>谁用</b>:店长 / 管理员为主 · <b>入口</b>:左侧导航「财务管理」</div>
|
||||
|
||||
<h3>6.1 页面结构</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><b>时间范围 chips</b>:本月 / 近 3 个月等,切换后整页数据跟着变。</li>
|
||||
<li><b>KPI 四卡</b>:本月销售额、本月采购额、应收账款(客户欠我)、应付账款(我欠供应商),应收应付卡带「未结清 N 笔」提示。</li>
|
||||
<li><b>收支趋势图</b>:按日/月的收支柱状图,生意起伏一眼看清。</li>
|
||||
<li><b>应收 / 应付汇总表</b>:按往来单位汇总欠款,<b>点击任意一行</b>打开抽屉看该单位的明细流水和近期单据。</li>
|
||||
<li><b>收支流水表</b>:所有财务记录,按类型 chips(收款 / 付款 / 应收 / 应付)筛选。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>6.2 账款从哪来、怎么结清</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li>入库单审核通过 → 自动生成<b>应付</b>;出库单审核通过 → 自动生成<b>应收</b>。</li>
|
||||
<li><b>登记收支</b>:点「登记收支」手动记一笔收款或付款(类型 + 往来单位 + 金额 + 日期 + 备注),适合与单据无关的杂项收支。</li>
|
||||
<li><b>结清</b>:流水行点「结清」单笔结清;或在往来单位抽屉里整体处理某个单位的欠款;入库/出库单列表里也有结清入口。结清后状态从「未结清」变「已结清」。</li>
|
||||
</ul>
|
||||
<div class="callout warn">结清操作目前不支持撤销,点之前确认好钱确实到账/付出了。</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 7 往来单位 ═══════════════════════ -->
|
||||
<h2 id="ch7">7. 往来单位</h2>
|
||||
<div class="who"><b>谁用</b>:操作员及以上 · <b>入口</b>:左侧导航「往来单位」</div>
|
||||
<div class="card">
|
||||
<p>供应商和客户都在这里管。录入库单选的「供应商」、出库单选的「客户」,来源就是这个列表。</p>
|
||||
<ul>
|
||||
<li><b>列表</b>:名称、类型徽章(供应商 / 客户)、联系人、电话、<b>应收</b>、<b>应付</b>。顶部类型 chips 一键筛选,搜索支持拼音。</li>
|
||||
<li><b>新建 / 编辑</b>:名称必填;电话、地址、备注选填;<b>期初余额</b>用于把老账本上的旧欠款带进系统(正数记应收或应付,视单位类型)。</li>
|
||||
<li><b>详情抽屉</b>:点击任意一行,右侧滑出该单位的联系信息、应收应付余额、<b>近期单据</b>(入库应付、出库应收、收付款流水都在内),底部可打对账单或编辑。</li>
|
||||
</ul>
|
||||
<div class="callout">已经关联过入库/出库单的单位不能删除,避免历史单据变成无头账。</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 8 基础数据 ═══════════════════════ -->
|
||||
<h2 id="ch8">8. 基础数据</h2>
|
||||
<div class="who"><b>谁用</b>:操作员及以上 · <b>入口</b>:左侧导航「基础数据」</div>
|
||||
<div class="card">
|
||||
<p>五个标签页,管的是「录单时那些下拉框里的选项」:</p>
|
||||
<table>
|
||||
<thead><tr><th style="width:20%">标签</th><th>内容</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>商品档案</b></td><td>全店所有商品编号的档案库。<b>没有「新增商品」按钮</b>——商品档案由入库单自动建立(每录一行建一个),这里只做查看和编辑。</td></tr>
|
||||
<tr><td><b>商品介绍</b></td><td>介绍库:维护商品介绍/卖点文案,库存页商品编辑抽屉里「从介绍库选择」的来源。</td></tr>
|
||||
<tr><td><b>系列</b></td><td>型号/产品线字典,如「飞天 53°」。</td></tr>
|
||||
<tr><td><b>规格</b></td><td>版本/包装字典,如「500ml」。</td></tr>
|
||||
<tr><td><b>仓库</b></td><td>仓库列表维护。默认仓库在「系统设置 → 偏好设置」里选。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout">字典改了什么,入库录单的下拉候选就变什么。平时不用专门来这里建——录单时输入新名字选「新增」就自动进字典了;这里更多是<b>事后整理</b>(改错别字、删没用的选项)。</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 9 设备与设置 ═══════════════════════ -->
|
||||
<h2 id="ch9">9. 设备与设置</h2>
|
||||
<div class="who"><b>谁用</b>:管理员为主 · <b>入口</b>:左侧导航「设备管理」「系统设置」「关于我们」</div>
|
||||
|
||||
<h3>9.1 设备管理</h3>
|
||||
<div class="card">
|
||||
<ul>
|
||||
<li><b>登录设备</b>:本店当前所有登录会话——谁、什么平台、什么时候登的、最近活跃、是否在线。管理员可对任意设备<b>「强制下线」</b>(员工离职、电脑丢失时第一时间用它)。</li>
|
||||
<li><b>外设设备</b>:登记店里的标签打印机、小票打印机、扫码枪等(名称、型号、连接方式),方便交接与盘点资产。</li>
|
||||
<li><b>打印模板</b>:查看当前单据/标签的打印模板样式。</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>9.2 系统设置(五个面板)</h3>
|
||||
<div class="card">
|
||||
<table>
|
||||
<thead><tr><th style="width:20%">面板</th><th>说明</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td><b>门店信息</b></td><td>店名、地址、电话、负责人,管理员可编辑;门店编号不可改。这些信息会印在标签和单据上。</td></tr>
|
||||
<tr><td><b>用户管理</b></td><td>新增用户(账号+初始密码+角色)、编辑、重置密码、启用/停用。四级角色见第 2 章。仅管理员可操作。</td></tr>
|
||||
<tr><td><b>编号规则</b></td><td>入库单、出库单等单号的前缀与序号。<b>不要把序号调小</b>到已用过的范围,会撞号。</td></tr>
|
||||
<tr><td><b>授权兑换券</b></td><td>本系统按「时长兑换券」授权:输入形如 <code>JIUKU-XXXX-XXXX</code> 的短码点「兑换续期」,时长直接<b>叠加</b>到现有到期日上,无需订阅。新门店自带 30 天试用。</td></tr>
|
||||
<tr><td><b>偏好设置</b></td><td>三套主题切换 + <b>默认仓库</b>(新建单据时自动选中的仓库)。</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="callout warn"><b>授权到期会怎样?</b>过期 7 天内是宽限期,一切照常;过期 7–15 天进入<b>只读</b>,只能看不能录;满 15 天锁定登录。任何阶段兑换新券立即恢复,数据不会丢。</div>
|
||||
</div>
|
||||
|
||||
<h3>9.3 关于我们</h3>
|
||||
<div class="card">
|
||||
<p>版本信息(有新版可一键更新)、授权状态与到期日、更新日志时间线,以及「反馈 Bug / 功能建议」入口。</p>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ 10 FAQ ═══════════════════════ -->
|
||||
<h2 id="ch10">10. 常见问题 FAQ</h2>
|
||||
<div class="card">
|
||||
<p class="faq-q">Q1:忘记密码了怎么办?</p>
|
||||
<p class="faq-a">找本店管理员:「系统设置 → 用户管理」→ 你的账号 →「重置密码」。管理员自己忘了,联系技术支持处理。</p>
|
||||
|
||||
<p class="faq-q">Q2:单据已提交(或已审核)才发现录错了,怎么改?</p>
|
||||
<p class="faq-a">待审核的单点「撤回」退回草稿,改完重新提交(操作员只能撤自己的单)。已审核的单不能改,用「退单」把错的那几行退掉,再补一张正确的单。</p>
|
||||
|
||||
<p class="faq-q">Q3:库存数和实物对不上,从哪查起?</p>
|
||||
<p class="faq-a">先在库存页找到那件商品记下<b>商品编码</b>,再到入库/出库管理用「详细搜索 → 商品编码」把相关单据全部拉出来,逐单核对。查清后做一次盘点(5.3)把账调平。</p>
|
||||
|
||||
<p class="faq-q">Q4:为什么同一款酒在库存里有好多行?</p>
|
||||
<p class="faq-a">这是设计如此:系统按「件」记账,每次入库每行生成一个独立编号(见第 2 章)。不同行代表不同批次/不同来源的货,各自可追溯。</p>
|
||||
|
||||
<p class="faq-q">Q5:点打印没反应,或打出来发淡发糊?</p>
|
||||
<p class="faq-a">先升级到最新版(Windows 打印已改为直接调用系统接口,老版本可能无响应)。针式打印机版式已做纯黑加粗优化;打印前先用「打印预览」确认。仍有问题检查打印机驱动和连接。</p>
|
||||
|
||||
<p class="faq-q">Q6:顾客扫商品标签二维码打不开页面?</p>
|
||||
<p class="faq-a">早期版本的扫码报错(502 / 页面不存在)已修复。请确认手机有网络;很旧的标签可以在库存页重打一张新标签。</p>
|
||||
|
||||
<p class="faq-q">Q7:系统提示授权过期、变成只读了,怎么续?</p>
|
||||
<p class="faq-a">「系统设置 → 授权兑换券」输入 <code>JIUKU-</code> 开头的兑换码点「兑换续期」,立即恢复。没有兑换码请联系客服购买。过期 15 天内数据都还在,别慌。</p>
|
||||
|
||||
<p class="faq-q">Q8:进货时价格还没谈好,能先入库吗?</p>
|
||||
<p class="faq-a">能。进价留空提交即可(显示「待定价」),价格定了以后在入库单详情点「确认进价」补上,成本和应付自动补齐。出库同理,售价留空、事后「确认售价」。</p>
|
||||
|
||||
<p class="faq-q">Q9:出库审核提示「库存不足」?</p>
|
||||
<p class="faq-a">出库数量超过了该商品在库数量。检查数量是否录多了,或对应的进货入库单是否还没审核(没审核的入库不算库存)。</p>
|
||||
|
||||
<p class="faq-q">Q10:员工离职 / 店里电脑丢了,账号安全怎么处理?</p>
|
||||
<p class="faq-a">两步:①「设备管理」找到对应设备「强制下线」;②「系统设置 → 用户管理」停用该账号或重置密码。</p>
|
||||
</div>
|
||||
|
||||
<p class="top-link"><a href="#toc">↑ 返回目录</a></p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,207 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>pay 支付对接开发指南 — 酒库管理系统</title>
|
||||
<style>
|
||||
:root{
|
||||
--primary:#2563AC; --primary-dark:#154072; --danger:#D14343; --danger-bg:#FDECEC;
|
||||
--success:#2E8B57; --success-bg:#E6F3EC; --warn:#B45309; --warn-bg:#FFF4E5; --accent:#8B2331;
|
||||
--ink:#232934; --muted:#6E7888; --border:#DCE2EB; --paper:#F5F7FA; --head:#F0F4FF;
|
||||
}
|
||||
*{box-sizing:border-box;font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;}
|
||||
body{margin:0;background:var(--paper);color:var(--ink);line-height:1.65;padding:26px 30px;max-width:1000px;margin:0 auto;}
|
||||
h1{font-size:23px;margin:0 0 4px;}
|
||||
.sub{color:var(--muted);font-size:13px;margin-bottom:6px;}
|
||||
.meta{color:var(--muted);font-size:12px;margin-bottom:18px;}
|
||||
h2{font-size:17px;margin:32px 0 8px;color:var(--primary-dark);border-bottom:2px solid var(--head);padding-bottom:6px;}
|
||||
h3{font-size:14.5px;margin:20px 0 6px;color:var(--ink);}
|
||||
p{margin:8px 0;font-size:13.5px;}
|
||||
ul,ol{margin:8px 0;padding-left:22px;font-size:13.5px;} li{margin:4px 0;}
|
||||
code{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#EEF2F8;padding:1px 5px;border-radius:4px;color:var(--primary-dark);}
|
||||
pre{background:#0f1830;color:#d6e2f5;border-radius:8px;padding:13px 15px;overflow-x:auto;font-family:ui-monospace,Menlo,monospace;font-size:12px;line-height:1.6;margin:10px 0;}
|
||||
pre .c{color:#7f93b5;}
|
||||
.card{background:#fff;border:1px solid var(--border);border-radius:10px;padding:15px 18px;margin:12px 0;}
|
||||
table{width:100%;border-collapse:collapse;font-size:12.8px;margin:8px 0;}
|
||||
th{background:var(--head);color:var(--primary-dark);font-weight:600;text-align:left;padding:7px 10px;border-bottom:1px solid var(--border);}
|
||||
td{padding:6px 10px;border-bottom:1px solid #EEF1F6;vertical-align:top;}
|
||||
tr:last-child td{border-bottom:0;}
|
||||
.mono{font-family:ui-monospace,Menlo,monospace;}
|
||||
.callout{border-left:4px solid var(--primary);background:#F0F6FF;padding:10px 14px;border-radius:4px;margin:10px 0;font-size:12.8px;}
|
||||
.callout.danger{border-color:var(--danger);background:var(--danger-bg);}
|
||||
.callout.warn{border-color:var(--warn);background:var(--warn-bg);}
|
||||
.callout.ok{border-color:var(--success);background:var(--success-bg);}
|
||||
.tag{display:inline-block;font-size:10px;padding:1px 7px;border-radius:9px;font-weight:600;}
|
||||
.tag.jiu{background:var(--success-bg);color:var(--success);}
|
||||
.tag.pay{background:#E5EEF9;color:var(--primary);}
|
||||
.flow{background:#0f1830;color:#d6e2f5;border-radius:8px;padding:15px;overflow-x:auto;font-family:ui-monospace,Menlo,monospace;font-size:11.8px;line-height:1.7;white-space:pre;margin:10px 0;}
|
||||
a{color:var(--primary);text-decoration:none;} a:hover{text-decoration:underline;}
|
||||
.n{display:inline-block;width:22px;height:22px;border-radius:50%;background:var(--primary);color:#fff;text-align:center;line-height:22px;font-size:12px;font-weight:700;margin-right:6px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p style="font-size:12px;"><a href="index.html">← 文档索引</a></p>
|
||||
<h1>pay 支付对接开发指南</h1>
|
||||
<div class="sub">jiu 门店在应用内购买/续费授权:付款走 pay 收款服务,付成功后 pay 回调 jiu,jiu 直接给门店续期。</div>
|
||||
<div class="meta">v1.0 · 2026-07-03 · 面向 jiu 后端开发 · pay 侧已就绪(下单+签名+webhook 推送已实现验证)· 本文档 = jiu 侧要实现的部分</div>
|
||||
|
||||
<div class="callout ok"><b>给开发者:</b>pay 服务(收款、支付宝渠道、签名鉴权、支付成功 webhook 推送)<b>已开发并联调验证完毕</b>。你只需实现 jiu 这一侧的 4 块:① 购买接口 ② webhook 接收器 ③ 续期逻辑 ④ 查单兜底。接口契约、签名算法、权益映射见下,照着写即可。</div>
|
||||
|
||||
<h2>1. 名词与地址</h2>
|
||||
<table>
|
||||
<tr><th>项</th><th>值</th></tr>
|
||||
<tr><td>pay 服务地址</td><td class="mono">https://pay.51yanmei.com</td></tr>
|
||||
<tr><td>pay 下单接口</td><td class="mono">POST /api/v1/orders</td></tr>
|
||||
<tr><td>pay 查单接口</td><td class="mono">GET /api/v1/orders/{out_trade_no}</td></tr>
|
||||
<tr><td>pay 套餐列表</td><td class="mono">GET /api/v1/products(含 biz_code)</td></tr>
|
||||
<tr><td>jiu 回调接收器(<b>你要实现</b>)</td><td class="mono">POST /api/v1/pay/callback</td></tr>
|
||||
<tr><td>共享密钥</td><td>双向共用一把 HMAC 密钥,存 Bitwarden,两边经环境变量注入(pay: <code>BIZ_JIU_SECRET</code>;jiu 侧自定,同值)</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>2. 整体流程</h2>
|
||||
<div class="flow">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 / 刷新 → 已续期 ✓│ │ │</div>
|
||||
|
||||
<h2>3. 签名算法(两个方向都用它)</h2>
|
||||
<div class="card">
|
||||
<p><b>签名串</b>(4 段用换行 <code>\n</code> 连接,再 HMAC-SHA256,最后 base64):</p>
|
||||
<pre>sign = base64( HMAC_SHA256( secret, system + "\n" + timestamp + "\n" + nonce + "\n" + rawBody ) )</pre>
|
||||
<p><b>请求头</b>:</p>
|
||||
<table>
|
||||
<tr><th>Header</th><th>说明</th></tr>
|
||||
<tr><td class="mono">X-Pay-System</td><td>固定 <code>jiu</code></td></tr>
|
||||
<tr><td class="mono">X-Pay-Timestamp</td><td>Unix 秒(服务端校验 ±5 分钟窗口)</td></tr>
|
||||
<tr><td class="mono">X-Pay-Nonce</td><td>随机串(如 uuid)</td></tr>
|
||||
<tr><td class="mono">X-Pay-Sign</td><td>上面算出的签名</td></tr>
|
||||
</table>
|
||||
<p><code>rawBody</code> = HTTP 请求体的<b>原始字节</b>(验签/签名都对同一份原始 body,勿先反序列化再重拼)。</p>
|
||||
<div class="callout warn">方向:<b>jiu→pay 下单</b> 由 jiu <b>签名</b>、pay 验签;<b>pay→jiu 回调</b> 由 pay 签名、jiu <b>验签</b>。两边同一把密钥、同一算法。</div>
|
||||
</div>
|
||||
<h3>Go 参考实现(与 pay 侧 <code>util.HMACSign</code> 完全一致)</h3>
|
||||
<pre><span class="c">// 签名(下单时调用)/ 验签(收 webhook 时对比)</span>
|
||||
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))
|
||||
}
|
||||
<span class="c">// 验签:hmac.Equal(([]byte)hmacSign(secret, "jiu", ts, nonce, rawBody), got)</span></pre>
|
||||
|
||||
<h2>4. jiu 要实现的四块</h2>
|
||||
|
||||
<h3><span class="n">1</span>购买接口 <span class="tag jiu">jiu</span> <code>POST /api/v1/license/purchase</code></h3>
|
||||
<p>鉴权 = jiu 自己的登录态(当前用户/门店)。步骤:</p>
|
||||
<ol>
|
||||
<li>校验登录态,拿到当前 <code>shop_id</code>;入参为套餐(前端传 biz_code 或 plan 标识)。</li>
|
||||
<li>建 <code>Purchase</code> 记录(status=pending,存 shop_id / product_biz_code / amount)。</li>
|
||||
<li>调 pay 下单(见 §5.1),<code>biz_ref = purchase_id</code>,<code>return_url</code> = jiu 结果页。</li>
|
||||
<li>把 pay 返回的 <code>pay_url</code>(和 out_trade_no,回写 Purchase)返给客户端。</li>
|
||||
</ol>
|
||||
<div class="callout">建议新增表 <code>license_purchases</code>:<span class="mono">id · shop_id · product_biz_code · amount · out_trade_no(index) · status(pending/paid/failed) · paid_at · created_at</span>。<code>out_trade_no</code> 兼作对账键与幂等键。</div>
|
||||
|
||||
<h3><span class="n">2</span>webhook 接收器 <span class="tag jiu">jiu</span> <code>POST /api/v1/pay/callback</code>(核心)</h3>
|
||||
<ol>
|
||||
<li><b>读原始 body</b>(勿被框架提前解析掉),取 4 个签名头。</li>
|
||||
<li><b>验签</b>:<code>hmac.Equal(hmacSign(secret,"jiu",ts,nonce,rawBody), X-Pay-Sign)</code>;<b>校验时间戳</b> ±5 分钟。任一不过 → 401,不处理。</li>
|
||||
<li><b>幂等</b>:按 <code>out_trade_no</code> 查 Purchase;若已 paid → 直接回 <code>{"code":"SUCCESS"}</code>(pay 会重发,必须幂等)。</li>
|
||||
<li><b>金额校验</b>:payload.amount 与 Purchase.amount 一致(分级比较,防篡改)。</li>
|
||||
<li><b>续期</b>:按 <code>product_biz_code</code> 映射权益(§6),给该 shop 的 License <b>直接叠加 ExpiresAt</b>(§7)。</li>
|
||||
<li>标记 Purchase=paid;<b>回 HTTP 200 + <code>{"code":"SUCCESS"}</code></b>(pay 认响应体含 SUCCESS 才算成功,否则退避重试)。</li>
|
||||
</ol>
|
||||
<div class="callout danger">这是「钱已到账」的入口——<b>验签 + 幂等 + 金额校验</b>三者缺一不可。验签失败绝不能续期(防伪造「已付款」骗授权)。</div>
|
||||
|
||||
<h3><span class="n">3</span>续期逻辑 <span class="tag jiu">jiu</span></h3>
|
||||
<p>复用/参照现有 license 服务(见 <a href="architecture/license-design.md">授权体系设计</a>)。方式 = <b>直接叠加</b>(不生成兑换码):</p>
|
||||
<pre>base := license.ExpiresAt
|
||||
if base == nil || base.Before(now) { base = now } <span class="c">// 已过期从现在起算</span>
|
||||
license.ExpiresAt = base + duration(bizCode) <span class="c">// +30 或 +365 天</span>
|
||||
license.Tier = tier(bizCode) <span class="c">// standard / pro</span>
|
||||
license.Type = billType(bizCode) <span class="c">// monthly / annual</span>
|
||||
license.MaxDevices = maxDevices(bizCode) <span class="c">// 2 / 5</span>
|
||||
license.Features = features(bizCode) <span class="c">// 仓库数/图片额度/AI 分析</span>
|
||||
license.IsActive = true</pre>
|
||||
|
||||
<h3><span class="n">4</span>查单兜底 <span class="tag jiu">jiu</span></h3>
|
||||
<p>防 webhook 全丢:对 pending 超时(如 >5 分钟)的 Purchase,定时主动查 pay <code>GET /api/v1/orders/{out_trade_no}</code>,若返回 <code>status=paid</code> 则走与 webhook 相同的续期入账(同样幂等)。</p>
|
||||
|
||||
<h2>5. 与 pay 的接口契约</h2>
|
||||
|
||||
<h3>5.1 下单 <span class="tag pay">pay</span> <code>POST https://pay.51yanmei.com/api/v1/orders</code></h3>
|
||||
<pre><span class="c">// Headers: Content-Type: application/json + 4 个签名头(§3)</span>
|
||||
<span class="c">// Body(对这份原始 body 签名):</span>
|
||||
{
|
||||
"product_id": 3, <span class="c">// pay 套餐 id(见 §6,或先 GET /products 按 biz_code 查 id)</span>
|
||||
"biz_system": "jiu",
|
||||
"biz_ref": "<purchase_id>", <span class="c">// jiu 的购买记录 id,回调原样带回</span>
|
||||
"return_url": "https://jiu.51yanmei.com/license/result"
|
||||
}
|
||||
<span class="c">// 成功响应:</span>
|
||||
{ "data": { "pay_url": "https://openapi.alipay.com/...", "out_trade_no": "yanmei-...", "amount": "2999.00", "subject": "年付标准" } }</pre>
|
||||
<p>把 <code>data.pay_url</code> 交给客户端打开;<code>out_trade_no</code> 回写 Purchase。<b>金额以 pay 套餐表为准</b>,不要自己传金额。</p>
|
||||
|
||||
<h3>5.2 回调 <span class="tag pay">pay</span> → <span class="tag jiu">jiu</span>(pay 主动 POST 到你的接收器)</h3>
|
||||
<pre><span class="c">// Headers: 4 个签名头(§3),你要验签</span>
|
||||
{
|
||||
"out_trade_no": "yanmei-20260703...-xxxx",
|
||||
"biz_system": "jiu",
|
||||
"biz_ref": "<purchase_id>",
|
||||
"product_biz_code": "annual_standard",
|
||||
"amount": "2999.00",
|
||||
"trade_no": "2026070322001...", <span class="c">// 支付宝交易号</span>
|
||||
"channel": "alipay",
|
||||
"paid_at": "2026-07-03T14:36:30+08:00"
|
||||
}
|
||||
<span class="c">// 你必须回:HTTP 200 + {"code":"SUCCESS"}(否则 pay 每 60s 重试,24h 内)</span></pre>
|
||||
|
||||
<h2>6. 套餐 biz_code → 权益映射</h2>
|
||||
<table>
|
||||
<tr><th>套餐</th><th>biz_code</th><th>价格</th><th>时长</th><th>tier</th><th>客户端(MaxDevices)</th><th>权益(Features)</th></tr>
|
||||
<tr><td>月付·标准</td><td class="mono">monthly_standard</td><td>¥299</td><td>+30 天</td><td>standard</td><td>2</td><td>单店/单仓库 · 千张图片分享</td></tr>
|
||||
<tr><td>年付·标准</td><td class="mono">annual_standard</td><td>¥2999</td><td>+365 天</td><td>standard</td><td>2</td><td>单店/单仓库 · 千张图片分享</td></tr>
|
||||
<tr><td>月付·高级</td><td class="mono">monthly_pro</td><td>¥599</td><td>+30 天</td><td>pro</td><td>5</td><td>单店/多仓库 · 万张照片分享 · 免费 AI 周/月度商业数据分析</td></tr>
|
||||
<tr><td>年付·高级</td><td class="mono">annual_pro</td><td>¥5999</td><td>+365 天</td><td>pro</td><td>5</td><td>单店/多仓库 · 万张照片分享 · 免费 AI 周/月度商业数据分析</td></tr>
|
||||
</table>
|
||||
<p>建议 <code>Features</code>(License.Features JSON)编码:<code>{"max_warehouses": 1|0(0=不限), "image_quota": 1000|10000, "ai_analysis": false|true}</code>。具体键名由 jiu 侧按现有能力开关定义,本表给权益语义。</p>
|
||||
<div class="callout warn">jiu 当前 Tier 未做能力差异(仅 standard)。标准/高级的<b>实际功能开关</b>(多仓库限制、图片额度、AI 分析)需 jiu 侧后续按此权益表落地;先把 tier/max_devices/features 正确写入 License,能力 gating 逐步接。</div>
|
||||
|
||||
<h2>7. 安全红线</h2>
|
||||
<ul>
|
||||
<li><b>验签必过</b>才处理(防伪造付款回调)。</li>
|
||||
<li><b>幂等</b>:同一 out_trade_no 只续一次(pay 会重发)。</li>
|
||||
<li><b>金额核对</b>:回调 amount == Purchase.amount。</li>
|
||||
<li><b>时间戳窗口</b> ±5 分钟,防重放。</li>
|
||||
<li>密钥只走环境变量 / Bitwarden,<b>不写代码/配置/仓库</b>。</li>
|
||||
<li>全程 HTTPS(jiu.51yanmei.com 已有)。</li>
|
||||
</ul>
|
||||
|
||||
<h2>8. 客户端(Web + App)</h2>
|
||||
<ul>
|
||||
<li><b>Web 入口</b>:拿到 pay_url 直接 <code>window.location</code> 跳转;付完支付宝跳回 <code>return_url</code>(带 out_trade_no),结果页轮询 jiu 授权状态。</li>
|
||||
<li><b>App 入口(Flutter)</b>:用<b>内置 webview</b> 打开 pay_url。</li>
|
||||
<li>手机端 pay 会走支付宝 <code>wap.pay</code>(H5)自动拉起支付宝 App;webview 需<b>放行 <code>alipays://</code> / <code>alipay://</code> scheme 唤起</b>(拦截非 http(s) 跳转交系统打开),付完靠 <code>return_url</code> 回跳 webview 页面。</li>
|
||||
<li>到账<b>以 webhook 续期为准</b>,客户端 return 后应<b>查 jiu 授权状态</b>确认,而非只信 return。</li>
|
||||
</ul>
|
||||
|
||||
<h2>9. 联调 Checklist</h2>
|
||||
<ol>
|
||||
<li>约定并配置共享密钥(pay <code>BIZ_JIU_SECRET</code> = jiu 侧同值);pay 侧配 <code>BIZ_JIU_CALLBACK_URL</code> = jiu 接收器地址。</li>
|
||||
<li>pay 侧 seed 4 个真实套餐(biz_code 见 §6),jiu 记下对应 product_id(或用 GET /products 动态取)。</li>
|
||||
<li>jiu 实现购买接口 → 下单 → 拿 pay_url(先验证签名被 pay 接受)。</li>
|
||||
<li>jiu 实现 webhook 接收器 → 用 pay 真实 1 分钱订单触发(临时把某套餐改 0.01)→ 验签/幂等/续期全走通。</li>
|
||||
<li>验证幂等(pay 重发不重复续期)、查单兜底(模拟 webhook 丢失)。</li>
|
||||
<li>客户端 Web + App webview 两端各跑一遍付款→回跳→授权已续。</li>
|
||||
</ol>
|
||||
|
||||
<p style="margin-top:26px;color:var(--muted);font-size:12px;">相关:<a href="architecture/license-design.md">授权体系设计(兑换券模型)</a> · <a href="db-schema.html">数据库 Schema</a> · pay 侧设计见 pay 仓 <code>docs/pay接入jiu授权续费对接方案.html</code></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,5 @@
|
||||
> 本文档已迁移至 [manual/user-manual.html](manual/user-manual.html),本文件停止更新。
|
||||
|
||||
# 酒库管理系统 — 用户操作手册
|
||||
|
||||
> 适用版本:v1.x
|
||||
|
||||
Reference in New Issue
Block a user