docs: 更新项目总览(product=序列号模型/API/三条发版线) + CLAUDE.md 数据模型铁律
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
This commit is contained in:
@@ -206,11 +206,18 @@ cd client && flutter test
|
||||
- **绝不**从请求参数、URL 或请求体中读取 `shop_id`
|
||||
- 所有数据库查询必须带 `WHERE shop_id = ?` 条件
|
||||
|
||||
### 数据模型:product = 特有产品/序列号(核心铁律)
|
||||
- `products` 每行 = 一个**特有产品/序列号**(`code`=商品编号,同店唯一 `uk_shop_code`),**不是 SKU**;语义:`name`=品牌、`series`=型号、`spec`=版本。product 是商品信息的**单一来源**(序列号/品牌型号版本/生产日期 `production_date`/批次 `batch_no`/进价/图片/`public_id`)。
|
||||
- **入库每录一行 = 新建一个独立 product**(`createIndependentProduct` + `nextProductCode` max+1),**绝不**按名称/系列/规格复用(`findOrCreate` 复用已废弃)。同理任何"导入/建库存"逻辑:有商品编号时按编号匹配/建 product,**禁止**按名称合并(历史 bug 根因)。
|
||||
- 基础数据字典(品牌/型号/版本 + 产地/保质期/储存/介绍)在「基础数据」页管理,入库选文本后据此建 product。
|
||||
- 库存(`inventories`)/出入库明细 = 指向 product 的引用 + **快照列**(`product_code/name/series/spec/...`,导入/审核时拷贝,作历史保真+显示兜底)。显示优先 product、回退快照(`COALESCE(p.*, 快照)` / 前端 `lineOrProduct`)。**快照列保留,勿擅自删**。
|
||||
|
||||
### 库存变更
|
||||
- 入库/出库审核通过时,必须在同一事务中同时完成:
|
||||
1. 更新 `inventories` 表数量
|
||||
2. 写入 `inventory_logs` 流水记录
|
||||
- 出库前必须校验库存充足,不足时返回错误并回滚
|
||||
- 审核中(pending)单据支持**撤回**(`withdraw`)回 draft:管理员/超管任意单、操作员限本人单(`OperatorID==本人`,handler 内判权,非 AdminOnly 中间件)
|
||||
|
||||
### Schema 管理
|
||||
- 表结构变更:修改 `backend/schema/schema.sql` + `backend/internal/model/` 对应 model
|
||||
|
||||
+103
-75
@@ -81,38 +81,68 @@ jiu/
|
||||
|
||||
## 核心数据模型
|
||||
|
||||
### 关键语义:product = 「特有产品 / 序列号」(非 SKU)
|
||||
|
||||
**`products` 表的每一行 = 一个特有产品,以序列号(= 商品编码 `code`,如 ZXZ###### / P####)唯一标识,每个产品独一份、不复用**(像手机序列号,哪怕同款也各不相同)。一个产品的全部信息以 product 为**单一来源**:
|
||||
|
||||
- `code` 序列号(同店唯一,`UNIQUE KEY uk_shop_code (shop_id, code)` 防并发/重复)
|
||||
- `name`=**品牌**、`series`=**型号**、`spec`=**版本/规格**、`unit` 单位
|
||||
- `production_date` 生产日期、`batch_no` 批次、`purchase_price` 进价、`sale_price` 售价
|
||||
- `public_id`(公开页/二维码)、`product_images`(图片)、`name_pinyin`/`name_initials`(拼音搜索索引,写入时由 `util.ToPinyin` 自动生成)
|
||||
|
||||
**入库每录一行 = 新建一个独立 product(发新序列号)**,不按名称复用(旧 `findOrCreate` 复用逻辑已废弃);同名同型号录两次 = 两个独立序列号。`handler/product.go` 的 `nextProductCode`(max+1 自增)+ `createIndependentProduct` 负责。
|
||||
|
||||
### 基础数据字典(「基础数据」页 `products_screen.dart` 管理)
|
||||
|
||||
入库选择商品时从字典选取文本,再据此建 product:
|
||||
- `product_name_options`(商品名=**品牌**)、`product_series_options`(系列=**型号**)、`product_spec_options`(规格=**版本**)
|
||||
- 商品属性字典:`product_origin_options`(产地)、`product_shelf_life_options`(保质期)、`product_storage_options`(储存方式)、`product_description_docs`(介绍文档)——公开页展示用,product 以可空外键引用
|
||||
|
||||
### 库存与单据(库存/明细 = 指向 product 的引用 + 历史快照)
|
||||
|
||||
```yaml
|
||||
inventories: # 库存批次:每行 = 某 product 在某仓库的一批(product_id + warehouse_id + quantity)
|
||||
# + stock_in_item_id(来源入库明细,盘盈则 inventory_check_id)
|
||||
# 含「快照列」product_code/product_name/series/spec/unit/unit_price/
|
||||
# production_date/batch_no/supplier_name/warehouse_name —— 导入/审核时
|
||||
# 从源/product 拷贝,作历史保真 + 显示兜底
|
||||
stock_in_orders: # 入库单(status: draft→pending→approved/rejected;可由本人/管理员 withdraw 回 draft)
|
||||
stock_in_items: # 入库明细(product_id + 快照列 + quantity/unit_price/total_price/batch_no/production_date)
|
||||
stock_out_orders: # 出库单(同状态机)
|
||||
stock_out_items: # 出库明细(同上)
|
||||
inventory_logs: # 库存流水(每次变动自动记录 in/out + qty_before/after)
|
||||
inventory_checks / inventory_check_items: # 盘点单 / 盘点明细(FIFO 盘盈盘亏)
|
||||
```
|
||||
|
||||
> **显示策略**:库存列表 `inventory.go` 用 `COALESCE(NULLIF(p.code,''), 快照列)`——**优先 product,product 为空/被删才回退快照**。前端 `lineOrProduct` 同理。历史导入的出入库明细 `product_id` 多指向一个占位 product(`HIST-PLACEHOLDER`),真实信息存快照列。
|
||||
|
||||
### 其它主表
|
||||
|
||||
```yaml
|
||||
shops: # 门店(租户根节点)
|
||||
users: # 用户(含 shop_id,role: admin/operator/readonly)
|
||||
licenses: # 许可证(含 device_id 绑定,type: trial/monthly/annual/lifetime)
|
||||
product_categories: # 商品分类
|
||||
products: # 商品(含 custom_fields JSON 动态扩展)
|
||||
# UNIQUE KEY uk_product_code (shop_id, code) — 防并发重复
|
||||
users: # 用户(含 shop_id,role: superadmin/admin/operator/readonly)
|
||||
licenses / license_codes / license_devices: # 授权(时长兑换券短码 + 设备绑定)
|
||||
product_categories: # 商品分类(可空)
|
||||
warehouses: # 仓库
|
||||
partners: # 往来单位(type: supplier/customer)
|
||||
stock_in_orders: # 入库单(status: draft→pending→approved/rejected)
|
||||
stock_in_items: # 入库单明细
|
||||
stock_out_orders: # 出库单
|
||||
stock_out_items: # 出库单明细
|
||||
inventories: # 实时库存(唯一键: shop_id+warehouse_id+product_id)
|
||||
inventory_logs: # 库存流水(每次变动自动记录)
|
||||
inventory_checks: # 盘点单
|
||||
inventory_check_items: # 盘点明细
|
||||
finance_records: # 财务流水(receivable/payable/receipt/payment)
|
||||
number_rules: # 单号生成规则(前缀+日期+6位序号)
|
||||
feedbacks: # 意见反馈(bug/suggestion + 图片)
|
||||
```
|
||||
|
||||
## 关键业务规则
|
||||
|
||||
1. **多租户隔离**:`shop_id` 只从 JWT 提取(`middleware.GetShopID(c)`),绝不从请求参数读取
|
||||
2. **库存变更事务**:入库/出库审核时,同一事务内更新 `inventories` + 写 `inventory_logs`
|
||||
3. **出库前校验**:审核出库时校验库存充足,不足返回错误并回滚
|
||||
4. **单号生成**:通过 `number_rules` 表事务安全生成,格式 `{前缀}{YYYYMMDD}{6位序号}`
|
||||
5. **扩展字段**:所有业务主表含 `custom_fields JSON`,用于存储动态业务字段,避免频繁改表
|
||||
6. **并发安全**:
|
||||
- `GenerateOrderNo` 在事务内用 `FOR UPDATE` 锁住 `number_rules` 行,防止并发生成重复单号
|
||||
- `ApproveStockOut`/`updateInventory` 在事务内用 `FOR UPDATE` 锁住库存行,防止超卖竞态
|
||||
- 商品编码 `products.code` 有 UNIQUE 约束,Create 时重试最多 5 次
|
||||
2. **product = 序列号**:入库每行新建独立 product(不按名称复用);product 是商品信息单一来源,库存/明细只是指向它的引用 + 历史快照(见上节)
|
||||
3. **库存变更事务**:入库/出库审核时,同一事务内更新 `inventories` + 写 `inventory_logs`
|
||||
4. **出库前校验**:审核出库时校验库存充足,不足返回错误并回滚
|
||||
5. **单据状态机与撤回**:`draft→submit→pending→approve/reject`;审核中(pending)可 **withdraw 回 draft** 再改再提交——管理员/超管撤回任意单,操作员限本人单(`OperatorID==本人`,handler 内判权);已审核(approved)只读
|
||||
6. **单号生成**:通过 `number_rules` 表事务安全生成,格式 `{前缀}{YYYYMMDD}{6位序号}`
|
||||
7. **权限角色**:`superadmin`(超管,可清空数据/看反馈)> `admin`(管理员,管用户/店铺信息)> `operator`(操作员)> `readonly`(只读,所有写操作 403)。`middleware.ReadOnly()` 全局挂载、`AdminOnly()`(admin+superadmin) / `SuperAdminOnly()` 按需挂载
|
||||
8. **并发安全**:
|
||||
- `GenerateOrderNo` 在事务内用 `FOR UPDATE` 锁住 `number_rules` 行,防并发重复单号
|
||||
- `ApproveStockOut`/`updateInventory` 在事务内用 `FOR UPDATE` 锁库存行,防超卖竞态
|
||||
- `products.code` 有 `(shop_id,code)` UNIQUE 约束,建 product 按最大序号+1、ErrDuplicatedKey 重试最多 5 次(不复用软删号)
|
||||
|
||||
## 开发脚本(scripts/dev.sh)
|
||||
|
||||
@@ -153,68 +183,50 @@ sh scripts/dev.sh seed S001
|
||||
# 服务器: mysql,用户: root,密码: password,数据库: jiu_db
|
||||
```
|
||||
|
||||
## 已实现的 API 接口
|
||||
## 已实现的 API 接口(以 `router/router.go` 为准)
|
||||
|
||||
```yaml
|
||||
认证(无需 JWT):
|
||||
POST /api/v1/auth/login
|
||||
POST /api/v1/auth/refresh
|
||||
无需 JWT:
|
||||
GET /health /version
|
||||
POST /api/v1/auth/login /api/v1/auth/refresh /api/v1/public/register
|
||||
GET /api/v1/public/products/:public_id # 公开商品详情(扫码页)
|
||||
GET /api/v1/public/shops/:shop_code/products # 店铺公开商品列表(仅有库存 + 数量 + code)
|
||||
GET /api/v1/public/release # 版本 + download_urls
|
||||
GET /product/:public_id # 注入 OG 标签的分享页
|
||||
POST /api/v1/public/errors # 客户端异常上报
|
||||
|
||||
许可证:
|
||||
POST /api/v1/license/activate
|
||||
GET /api/v1/license/verify
|
||||
POST /api/v1/license/deactivate
|
||||
GET /api/v1/license/info # 返回当前许可证详情(含到期日、类型)
|
||||
会话/许可证:
|
||||
POST /api/v1/auth/logout /api/v1/auth/ping
|
||||
GET /api/v1/sessions ; DELETE /api/v1/sessions/:id (AdminOnly)
|
||||
GET /api/v1/license/info|verify|devices ; POST /api/v1/license/activate|deactivate
|
||||
|
||||
版本(无需 JWT):
|
||||
GET /version # 返回最新版本信息(读取 version.yaml,缺失返回 500)
|
||||
GET /api/v1/public/release # 版本 + download_urls(驱动客户端更新提示与下载页)
|
||||
商品(product=序列号):
|
||||
GET/POST /api/v1/products ; PUT/DELETE /api/v1/products/:id
|
||||
POST /api/v1/products/find-or-create
|
||||
GET /api/v1/products/:id/detail /:id/qrcode
|
||||
POST /api/v1/products/:id/images ; DELETE /api/v1/products/:id/images/:image_id
|
||||
|
||||
商品:
|
||||
GET/POST /api/v1/products
|
||||
PUT/DELETE /api/v1/products/:id
|
||||
基础数据字典 /api/v1/product-options:
|
||||
names | series | specs | origins | shelf-lives | storages | description-docs
|
||||
每组 GET(支持 ?keyword 服务端搜索)/ POST / PUT/:id / DELETE/:id
|
||||
|
||||
仓库:
|
||||
GET/POST /api/v1/warehouses
|
||||
PUT/DELETE /api/v1/warehouses/:id
|
||||
仓库 /warehouses · 往来单位 /partners(?type&keyword): GET/POST/PUT/DELETE
|
||||
|
||||
往来单位:
|
||||
GET/POST /api/v1/partners
|
||||
PUT/DELETE /api/v1/partners/:id
|
||||
入库 /stock-in/orders · 出库 /stock-out/orders:
|
||||
GET(列表,?status&keyword) /POST ; GET/PUT/DELETE/:id
|
||||
PUT /:id/submit | approve | reject | withdraw # withdraw=审核中撤回回草稿
|
||||
|
||||
入库:
|
||||
GET/POST /api/v1/stock-in/orders
|
||||
GET /api/v1/stock-in/orders/:id
|
||||
PUT /api/v1/stock-in/orders/:id/submit
|
||||
PUT /api/v1/stock-in/orders/:id/approve
|
||||
PUT /api/v1/stock-in/orders/:id/reject
|
||||
库存 /inventory:
|
||||
GET ""(?warehouse_id&keyword&series&spec&in_stock) ; GET /logs
|
||||
PUT /:id/remark ; POST /checks ; GET /checks/:id ; PUT /checks/:id/complete
|
||||
|
||||
出库:
|
||||
GET/POST /api/v1/stock-out/orders
|
||||
GET /api/v1/stock-out/orders/:id
|
||||
PUT /api/v1/stock-out/orders/:id/submit
|
||||
PUT /api/v1/stock-out/orders/:id/approve
|
||||
PUT /api/v1/stock-out/orders/:id/reject
|
||||
|
||||
库存:
|
||||
GET /api/v1/inventory
|
||||
GET /api/v1/inventory/logs
|
||||
GET /api/v1/inventory/batch-tracking # 批次追踪(已审核入库商品含销售状态)
|
||||
POST/GET /api/v1/inventory/checks
|
||||
|
||||
用户管理(仅 admin 角色):
|
||||
GET/POST /api/v1/users
|
||||
PUT/DELETE /api/v1/users/:id
|
||||
|
||||
数据导入:
|
||||
POST /api/v1/import/products (Excel/CSV)
|
||||
POST /api/v1/import/partners (Excel/CSV)
|
||||
|
||||
意见反馈:
|
||||
POST /api/v1/feedback # 提交反馈(bug/suggestion,文字+图片,认证)
|
||||
POST /api/v1/feedback/images # 上传反馈附图(认证)
|
||||
GET /api/v1/admin/feedback # 反馈列表(仅 superadmin)
|
||||
PATCH /api/v1/admin/feedback/:id # 标记处理状态(仅 superadmin)
|
||||
财务 /finance: GET /records /summary ; POST /records ; PUT /records/:id/close · /close-by-ref
|
||||
用户 /users(AdminOnly): GET/POST ; PUT/:id ; PUT /:id/reset-password
|
||||
店铺 /shop: GET /info ; PUT /info、POST /logo (AdminOnly)
|
||||
编号规则 /number-rules: GET ""; PUT /:id
|
||||
意见反馈 /feedback: POST ""、/images
|
||||
数据导入 /import: products | partners | product-names|series|specs|codes | stock-in | stock-out | inventory (Excel)
|
||||
超管 /admin(SuperAdminOnly): POST /clear-data ; GET /reconcile /errors /feedback ; PATCH /feedback/:id
|
||||
```
|
||||
|
||||
## Flutter 客户端结构
|
||||
@@ -386,7 +398,23 @@ DataTableCard(
|
||||
| Android | 签名 `jiu-android.apk` | 下载页 `/downloads/`(见 docs/android-signing.md) |
|
||||
| iOS | 签名 IPA | TestFlight(见 docs/ios-signing.md) |
|
||||
|
||||
版本清单:`backend/config/version.yaml` 的 `download_urls`(含各平台下载地址);客户端 `GET /api/v1/public/release` 读取,驱动更新提示与下载页。
|
||||
版本清单 `version.yaml`(归 **client**,写 version/build_number/release_notes/下载链接/changelog);后端 `GET /version` 与 `/api/v1/public/release` **每请求实时读取**,client 部署后立即生效,无需重启后端、无需重建官网。
|
||||
|
||||
### 三条独立发版流水线(互不影响,各有 tag 前缀 / CHANGELOG / workflow)
|
||||
|
||||
| part | 范围 | tag 前缀 | CHANGELOG | workflow |
|
||||
|------|------|---------|-----------|----------|
|
||||
| **client** | `client/` Flutter 全平台 + `version.yaml` | `client-v*` | `CHANGELOG-client.md` | `deploy-client.yml` |
|
||||
| **site** | `web/` Eleventy 营销宣传站(不含 Web 版 app) | `site-v*` | `CHANGELOG-site.md` | `deploy-site.yml` |
|
||||
| **server** | `backend/` Go 服务 + 共享基建(nginx/systemd) | `server-v*` | `CHANGELOG-server.md` | `deploy-server.yml` |
|
||||
|
||||
用 `/release <part> [version]` slash command:本地 build→test→更新 CHANGELOG→commit→tag→push;CI(Forgejo) 按 tag 前缀触发对应 workflow 自动编译/测试/发 Release/部署 EC2/Telegram 通知。**测试未过禁止发版**。
|
||||
|
||||
### 生产环境与运维
|
||||
|
||||
- 生产:EC2(`ec2-user@18.136.60.128`),jiu 为宿主 systemd 服务,MySQL 在容器 `jiu_mysql`(映射 127.0.0.1:3306)。配置 `/opt/jiu/config/production.env`(DATABASE_DSN)。
|
||||
- CI runner:mac runner=开发者本机(launchd + relay 绕 Shadowrocket,有看门狗自愈)、windows runner=另一台机(nssm 服务 `forgejo-runner`)、ubuntu=NAS docker。Forgejo 在 NAS(`git.51yanmei.com`)。
|
||||
- 一次性数据工具:`cmd/import-history`(旧系统进销存批量导入)、`cmd/fix-inventory-products`(修复历史库存 product_id 错指)。
|
||||
|
||||
## 文档索引
|
||||
|
||||
|
||||
Reference in New Issue
Block a user