# 商品管理 — 技术方案

> **依据：** 《商品管理功能需求.md》v1.3.3  
> **关联：** 《商户管理》《店铺管理》《商品分类》《商品服务管理》功能需求 v1.3.1 / v1.3.2 / v1.3.1 / **v1.0.1**；**《店铺管理技术方案.md》v1.2.1**、《商户管理技术方案.md》v1.3、《商品分类技术方案.md》v1.1  
> **范围：** 商品模块库表；**平台端 + 商家端** 接口；分类 ER 引用；**商品服务快照**（P18～P21）；C 端 `canPurchase`；商家端依赖店铺经营账号。  
> **原则：** 不考虑会员；**首期单规格**（P16）；分类仅二级；未上架不进平台列表；状态 **仅** 经 submit/audit/offShelf 变更（P17）。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 编号生成 | `goods_sn`：`G`+yyyyMMdd+6 位序号（Redis/DB） |
| 规格 | **首期单表** `sale_price`+`stock`；多规格见 `biz_goods_sku`（非本期） |
| 日志 | submit、audit、offShelf `@Log` |

### 1.1 模块落位

```text
baqing-shop
├── com.ruoyi.web.controller.agri.GoodsController
├── com.ruoyi.web.controller.agri.seller.SellerGoodsController
└── com.ruoyi.agri.interceptor.SellerShopContextInterceptor   （解析当前 shopId）
ruoyi-agri
├── domain.BizGoods / vo.GoodsListVO、GoodsDetailVO
├── service.IGoodsService、IGoodsAuditService
└── facade.GoodsFacadeImpl、GoodsPurchaseFacadeImpl
```

### 1.2 业务链

```text
biz_goods_service（平台目录，商品服务模块）
    └── biz_goods_service_snapshot（商品保存时快照，本模块）
biz_shop → biz_goods_category（二级）→ biz_goods → biz_order（《订单管理技术方案》v1.0）
```

### 1.3 跨模块 Facade

| 接口 | 提供方 | 方法 |
|------|--------|------|
| `IGoodsFacade` | 商品 | `hasBlockingGoodsForShopDelete`、`existsByCategoryId` |
| `IGoodsServiceFacade` | 商品服务 | `listAllEnabled`、`listDefaultShow`、`isServiceEnabled`、`existsGoodsRef` |
| `IShopFacade` | 店铺 | `isShopOpenForOrder(shopId)` |
| `ICategoryFacade` | 分类 | `isCategoryVisible(categoryId)` |
| `IGoodsPurchaseFacade` | 商品 | `canPurchase(goodsId)` → reason |
| `IOrderFacade` | 订单 | `deductStock`、`restoreStock`（《订单管理技术方案》v1.0） |

### 1.4 状态联动

| 事件 | goods_status |
|------|--------------|
| 商户冻结/注销、店铺停业/开业 | **不变** |
| `default_audit_pass` 变更（**全局**） | **不追溯** |
| 分类不显示 | **不变**；C 端类目不展示 |
| 店铺逻辑删除 | 平台 list 不展示（join shop） |

---

## 2. 数据库设计

### 2.1 本模块表

| 表名 | 说明 |
|------|------|
| `biz_goods` | 商品主表（**首期单规格**，P16） |
| `biz_goods_service_snapshot` | 商品已选服务的 **展示快照**（P18）；目录表 `biz_goods_service` 见商品服务技术方案 |

### 2.2 关联 `biz_goods_category`（ER）

`category_id` 须为 `parent_id != 0` 的二级节点，且 `shop_id` = 商品 `shop_id`。

### 2.3 `biz_goods` 字段摘要

| 字段 | 说明 |
|------|------|
| goods_id, shop_id※, category_id | shop_id 创建后不可改 |
| goods_sn | 系统生成，UK |
| goods_name, main_pic, detail_content | |
| sale_price, stock, sales_count | **单规格** 价/库存/销量 |
| goods_status | 0～4 |
| reject_reason, submit_time, audit_time, off_shelf_time | |
| del_flag, 审计字段 | |

### 2.4 索引

| 索引 | 字段 |
|------|------|
| uk_goods_sn | goods_sn |
| idx_shop_status | shop_id, goods_status, del_flag |
| idx_submit_time | submit_time |

### 2.5 DDL

```sql
CREATE TABLE `biz_goods` (
  `goods_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `shop_id` bigint(20) NOT NULL COMMENT '店铺ID',
  `category_id` bigint(20) NOT NULL COMMENT '二级分类ID',
  `goods_sn` varchar(32) NOT NULL COMMENT '商品编号',
  `goods_name` varchar(200) NOT NULL COMMENT '商品名称',
  `main_pic` varchar(512) NOT NULL COMMENT '主图',
  `detail_content` text COMMENT '详情',
  `sale_price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '销售价',
  `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存',
  `sales_count` int(11) NOT NULL DEFAULT '0' COMMENT '销量',
  `goods_status` char(1) NOT NULL DEFAULT '0' COMMENT '0未上架1待审核2出售中3审核失败4已下架',
  `reject_reason` varchar(500) DEFAULT NULL COMMENT '驳回原因',
  `submit_time` datetime DEFAULT NULL COMMENT '首次提交上架时间',
  `audit_time` datetime DEFAULT NULL COMMENT '审核时间',
  `off_shelf_time` datetime DEFAULT NULL COMMENT '下架时间',
  `del_flag` char(1) NOT NULL DEFAULT '0',
  `create_by` varchar(64) DEFAULT '',
  `create_time` datetime DEFAULT NULL,
  `update_by` varchar(64) DEFAULT '',
  `update_time` datetime DEFAULT NULL,
  `remark` varchar(500) DEFAULT NULL,
  PRIMARY KEY (`goods_id`),
  UNIQUE KEY `uk_goods_sn` (`goods_sn`),
  KEY `idx_shop_status` (`shop_id`,`goods_status`,`del_flag`),
  KEY `idx_category_id` (`category_id`,`del_flag`),
  KEY `idx_submit_time` (`submit_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品';
```

### 2.6 `goods_status`

| 值 | 含义 | 平台 list | 商家 list |
|:--:|------|:---------:|:---------:|
| 0 | 未上架 | ✗ | ✓ |
| 1 | 待审核 | ✓ | ✓ |
| 2 | 出售中 | ✓ | ✓ |
| 3 | 审核失败 | ✓ | ✓ |
| 4 | 已下架 | ✓ | ✓ |

---

## 3. 状态机（Service）

### 3.1 提交上架 `submit`（唯一入口：0/3/4 → 1 或 2）

| 当前 | 目标 |
|------|------|
| 0,3,4 | **全局** `default_audit_pass=0` → **1**；`=1` → **2** |
| 1,2 | 拒绝 |

- 首次 submit：`submit_time=now()`；已有则 **不更新**（P14）。  
- **禁止** 请求体或 PUT 保存直接设 `goods_status=2`（P17）。

**响应：**

```json
{
  "code": 200,
  "msg": "提交成功",
  "data": { "goodsId": 1001, "goodsStatus": "1", "goodsStatusLabel": "待审核" }
}
```

免审店 `goodsStatus` 为 `"2"`。

### 3.2 平台审核 `audit`（1 → 2 或 3）

`rejectReason` 驳回时必填。

### 3.3 下架 `offShelf`（2 → 4）

平台/商家；写 `off_shelf_time`。平台端建议前端二次确认（需求建议项）。

### 3.4 商家保存 `PUT /`（不改状态入口以外的 status）

| 当前 | 保存后 status | 可改字段 |
|------|---------------|----------|
| 0 | 0 | 全部业务字段 |
| 1 | 1 | 全部；**不** 更新 submit_time |
| 2 | **2** | 名称、主图、详情、**sale_price、stock** 等（P16 单规格） |
| 3 | 3 | 全部 |
| 4 | 4 | 全部 |

- **禁止** 保存为 status=0（P17，无撤回草稿）。  
- **禁止** Body 传入 `goodsStatus` 任意值（Mapper 不更新该列，Service 校验拒收）。

### 3.5 状态变更入口（P17）

| 允许 | 接口 |
|------|------|
| 提交上架 | `PUT .../submit` |
| 审核 | `PUT /agri/goods/audit` |
| 下架 | `PUT .../offShelf`（平台/商家） |
| **禁止** | 普通 `PUT` 修改 `goods_status`；禁止 2→0 |

---

## 4. 接口设计

### 4.1 平台端 `/agri/goods`

**权限：** `agri:goods:list|query|audit|offshelf`

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/list` | status≠0；shop 未删除 |
| GET | `/{goodsId}` | 详情 VO |
| PUT | `/audit` | 批量/单条；整批失败 |
| PUT | `/offShelf` | 仅 status=2；整批失败 |

**列表 rows（GoodsListVO）**

| 字段 | 说明 |
|------|------|
| goodsId, goodsSn, goodsName, mainPic | |
| salePrice, stock, salesCount | 单规格 |
| goodsStatus, goodsStatusLabel | |
| shopName, merchantName | join |
| categoryPath | `一级 > 二级` |

**详情（GoodsDetailVO）**：列表字段 + `detailContent`、`rejectReason`、`submitTime`、`auditTime`、`offShelfTime`、`shopId`、`categoryId`。

**审核 / 下架 Body**（同 v1.0）。

---

### 4.2 商家端 `/agri/seller/goods`

#### 4.2.1 当前店铺上下文（§8.6）

| 项 | 约定 |
|----|------|
| 登录 | `biz_merchant_account`（店铺模块维护） |
| 当前店 | Header `X-Shop-Id` 或 Session `sellerShopId` |
| 拦截器 | 校验该 `shopId` 属于登录账号的 `merchant_id` 且 shop 未删除 |
| 数据范围 | 所有 SQL 带 `shop_id = #{sellerShopId}` |

#### 4.2.2 接口一览

| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/list` | 含 status=0 |
| GET | `/{goodsId}` | 校验 shop 归属 |
| POST | `/` | status=0；生成 goods_sn |
| PUT | `/` | §3.4；**无 goodsStatus** |
| PUT | `/{goodsId}/submit` | §3.1 |
| PUT | `/{goodsId}/offShelf` | 商家下架 |

**POST 样例**

```json
{
  "categoryId": 10,
  "goodsName": "复合肥 50kg",
  "mainPic": "/profile/upload/g.jpg",
  "detailContent": "<p>...</p>",
  "salePrice": 128.00,
  "stock": 100
}
```

**PUT 出售中编辑样例**

```json
{
  "goodsId": 1001,
  "goodsName": "复合肥 50kg",
  "mainPic": "/profile/upload/g.jpg",
  "detailContent": "<p>...</p>",
  "salePrice": 118.00,
  "stock": 50,
  "categoryId": 10
}
```

保存后 `goodsStatus` 仍为 `2`。库存扣减与已下单关系由 **订单模块** 约束（本期商品不拦截「库存改小」除非产品另定）。

**PUT 已下架后重新提交**：先 `PUT` 改资料（可选），再 `PUT .../submit` → 按 §3.1 进 1 或 2。

---

### 4.3 `IGoodsFacade`

```text
hasBlockingGoodsForShopDelete: goods_status IN ('1','2') AND del_flag='0'
existsByCategoryId: category_id=? AND del_flag='0'
```

---

### 4.4 `canPurchase`（P15）

| 条件 | 失败 reason |
|------|-------------|
| goods_status=2 | 商品非出售中 |
| stock>0 | 库存不足 |
| isShopOpenForOrder | 店铺已停业 |
| isCategoryVisible | （可选展示类失败文案） |

---

## 5. 端到端流程

（同 v1.0：发品→审核→C端；删店/删类 Facade。）

**订单扣库存（占位）：**

```text
下单支付成功 @Transactional
  → IOrderFacade.deductStock(goodsId, qty)   // UPDATE stock WHERE stock>=qty
  → sales_count += qty
```

超卖防护：扣减 SQL `WHERE stock >= #{qty}`，影响行数 0 则失败。

---

## 6. 业务规则映射

| 编号 | 实现 |
|------|------|
| P1～P15 | （同 v1.0） |
| P16 | 单表 sale_price/stock；无 SKU 表 |
| P17 | §3.5 入口；PUT 拒收 goodsStatus |

---

## 7. 菜单权限

（同 v1.0。）

---

## 8. 实现补充

### 8.1 业务错误 msg

（同 v1.0，增补：）

| 场景 | msg |
|------|-----|
| 非法改状态 | 请通过提交上架、审核或下架变更商品状态 |
| 不可改回草稿 | 当前状态不可改为未上架 |
| 未选店铺 | 请先选择当前店铺 |

### 8.2～8.5

（审核 Body、待审编辑、校验表、列表 SQL — 同 v1.0。）

### 8.6 商家 `shopId` 上下文

```text
登录(biz_merchant_account)
  → 前端选店写入 Session / Header X-Shop-Id
  → SellerShopContextInterceptor 校验 merchant 归属
  → ThreadLocal 持有 currentShopId 供 Service 使用
```

### 8.7 平台详情响应片段

```json
{
  "goodsId": 1001,
  "goodsSn": "G20260526000001",
  "goodsName": "复合肥 50kg",
  "goodsStatus": "1",
  "rejectReason": null,
  "shopName": "张三农资店",
  "merchantName": "张三农资店",
  "categoryPath": "肥料 > 复合肥",
  "salePrice": 128.00,
  "stock": 100,
  "canAudit": true,
  "canOffShelf": false
}
```

`canAudit` / `canOffShelf` 由 status 计算，供前端显隐按钮。

### 8.8 `submit` 响应

见 §3.1。

### 8.9 字段校验（完整）

| 字段 | 规则 |
|------|------|
| goodsName | 非空，≤200 |
| mainPic | 非空 URL；jpg/png/jpeg；≤5MB |
| detailContent | 非空（按产品） |
| salePrice | ≥0，2 位小数 |
| stock | 整数 ≥0 |
| categoryId | 本店二级（`parent_id>0` 且 shop_id 一致） |

### 8.10 出售中改库存（说明）

- 商品模块 **允许** PUT 减小 `stock`。  
- 已支付未发货订单是否允许改小：**订单模块** 定义；商品侧不重复校验。

### 8.11 商品服务快照（P18～P21 · v1.2）

**保存商品（商家 POST/PUT）时：**

```text
1. 校验 serviceIds ⊆ IGoodsServiceFacade.listAllEnabled()
2. DELETE biz_goods_service_snapshot WHERE goods_id=?
3. 按勾选 INSERT 快照行（复制当时 name/intro/icon/sort_no）
4. 剔除 catalog 已逻辑删除的 service_id（P21，保存时不再插入）
```

**展示（详情/C 端）：** 读 `biz_goods_service_snapshot`；若 `service_id` 对应目录 `del_flag=2` → **不返回**（P20）。改目录文案 **不 UPDATE** 已有快照行（P19）。

**商家发品下拉：** `GET .../serviceOptions` → `listAllEnabled` + 前端对 `listDefaultShow` 预勾选。

**DDL（摘要）：**

```sql
CREATE TABLE `biz_goods_service_snapshot` (
  `snapshot_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `goods_id` bigint(20) NOT NULL,
  `service_id` bigint(20) NOT NULL COMMENT '来源目录ID',
  `service_name` varchar(64) NOT NULL,
  `service_intro` varchar(500) NOT NULL DEFAULT '',
  `service_icon` varchar(512) NOT NULL,
  `sort_no` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`snapshot_id`),
  KEY `idx_goods_id` (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品服务展示快照';
```

`existsGoodsRef(serviceId)`：`SELECT 1 FROM biz_goods_service_snapshot s JOIN biz_goods g ON g.goods_id=s.goods_id AND g.del_flag='0' WHERE s.service_id=? LIMIT 1`（商品服务模块删前校验）。

---

## 9. 文档索引

| 文档 | 版本 |
|------|------|
| 商品管理功能需求.md | **v1.3.3** |
| 商品服务管理功能需求/技术方案 | v1.0.1 / v1.0.1 |
| 商品管理测试用例.md | v1.0（P18～P21 用例待补） |
| 店铺管理功能需求/技术方案 | v1.3.2 / v1.2.1 |
| 商品分类功能需求/技术方案 | v1.3.1 / v1.1 |
| 商户管理功能需求/技术方案 | v1.3.1 / v1.3 |

---

## 10. 非本期实现

`biz_goods_sku` 多规格、商家删品 API、平台代发、导出、C 端商品列表 API、子管理员细粒度权限、商品恢复、按服务项检索全平台商品。

---

*文档版本：v1.2 · MySQL 5.7.39 · RuoYi v3.9.2-springboot2 · 关联《商品管理功能需求.md》v1.3.3、《订单管理技术方案.md》v1.0、《商品服务管理功能需求.md》v1.0.1、《商品管理测试用例.md》v1.0*
