# 店铺商品列表 — 技术方案

> **依据：** 《商品列表功能需求.md》v1.4  
> **关联：** 《关联需求分析.md》v1.6；平台《商品审核技术方案》v1.1、《商品分类技术方案》v1.3、《商品服务管理技术方案》v1.0.1；商家《店铺商品分类技术方案》v1.2、《属性模版技术方案》v1.3  
> **范围：** 商家端 **当前店铺** 商品列表、检索、发品、编辑、提交上架、下架、删除；**复用** 平台商品主表与状态机，**不另建** 商家商品表。  
> **原则：** `goods_status` 仅经 submit / audit / offShelf 变更（P17）；`X-Shop-Id` 店铺上下文；批量含非法状态 **整批失败**。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 店铺上下文 | `SellerShopContextInterceptor` + 请求头 **`X-Shop-Id`**；会话内记住所选店铺（Redis token） |
| 编号 | Redis 自增 `agri:goods:sn:{yyyyMMdd}` |
| 日志 | 增删改、提交上架、下架 `@Log` |

### 1.1 与平台侧边界

| 维度 | 平台 · 商品审核/管理 | 本模块 · 商家商品列表 |
|------|----------------------|----------------------|
| 表 | **`biz_goods` 同源** | 同表，`shop_id` 限定当前店 |
| 状态机 | `IGoodsService.auditGoods` / `offShelfPlatform` | `submitGoods` / `offShelfSeller` |
| 列表范围 | `goods_status <> '0'` | **含未上架（0）** 五态全展示 |
| 路径 | `/agri/goods` | **`/agri/seller/goods`** |
| 权限前缀 | `agri:goods:*` | **`agri:seller:goods:*`** |

> **定稿：** 商家端与平台 **同一套** `IGoodsService` 实现，避免双轨状态逻辑（对齐《商品审核技术方案》§1）。

### 1.2 业务链

```text
GET /agri/seller/context          → 前端缓存 shopId（X-Shop-Id）
        ↓
biz_shop_global_config.default_audit_pass
        ↓
biz_goods（shop_id = 当前店铺）
        ├── 保存 POST/PUT           → status=0 未上架
        ├── 提交上架 PUT .../submit → status=1 或 2
        ├── 平台 audit（/agri/goods/audit）→ 1→2|3
        └── 下架 PUT .../offShelf     → 2→4
        ↓
biz_goods_service_snapshot（保存商品时写快照）
biz_goods_pic（保存商品副图 subPics，可选最多 10 张）
        ↓
biz_goods_category
        ├── category_id      → shop_id IS NULL 的二级（平台「商品分类」）
        └── shop_category_id → shop_id = 当前店 的二级（「店铺商品分类」）
```

| 关联模块 | 协作 |
|----------|------|
| **商品审核** | 商家 submit 后进入平台待审队列；驳回原因商家详情可读 |
| **商品分类** | `category_id` ← `IPlatformCategoryService.selectPlatformLevel2Options()` |
| **店铺商品分类** | `shop_category_id` ← `IShopGoodsCategoryFacade.listOptionsByShopId()` |
| **商品服务** | 发品多选 ← `IGoodsServiceFacade`；保存写快照 |
| **属性模版** | 选用模版预填 `attributes`/`specs`；保存 `attr_template_id` + **先删后插** `biz_goods_attr` |
| **商品副图** | 发品 `subPics[]` → **先删后插** `biz_goods_pic`（可选，最多 10 张，**不含主图**） |
| **店铺设置** | `IShopGlobalConfigFacade.getDefaultAuditPass()` 决定 submit 目标态 |
| **订单管理** | 下架前校验 **O10 未完成订单**（**待实现**，`IGoodsOrderFacade` 占位） |
| **店铺管理** | `IGoodsShopFacade.hasBlockingGoodsForShopDelete`（`GoodsFacadeImpl`）删店前置 |

### 1.3 模块落位

**本模块包：** `com.ruoyi.web.modules.goods`

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/goods/
├── controller/
│   ├── SellerGoodsController.java           # /agri/seller/goods（本方案）
│   ├── GoodsController.java                 # /agri/goods（平台监管）
│   └── GoodsAuditController.java            # /agri/goodsAudit（平台审核菜单薄封装）
├── service/
│   ├── IGoodsService.java
│   └── impl/GoodsServiceImpl.java           # saveServiceSnapshots + saveGoodsAttrs + saveGoodsSubPics
├── domain/
│   ├── BizGoods.java
│   ├── BizGoodsServiceSnapshot.java
│   ├── BizGoodsAttr.java
│   └── BizGoodsPic.java
├── dto/
│   ├── GoodsSaveDTO.java
│   ├── GoodsAttrItemDTO.java                # attributes[] / specs[] 项
│   ├── GoodsAuditDTO.java
│   ├── GoodsBatchIdsDTO.java                # 批量提交 / 批量删除
│   └── GoodsOffShelfDTO.java                # 批量下架
├── vo/
│   ├── GoodsListVO.java
│   ├── GoodsDetailVO.java
│   ├── GoodsServiceSnapshotVO.java
│   └── GoodsPurchaseVO.java                 # C 端/采购 Facade 用
├── mapper/
│   ├── BizGoodsMapper.java
│   ├── BizGoodsServiceSnapshotMapper.java
│   ├── BizGoodsAttrMapper.java
│   └── BizGoodsPicMapper.java
├── facade/
│   ├── IGoodsOrderFacade.java               # 下架 O10（DefaultGoodsOrderFacade 占位）
│   ├── IGoodsPurchaseFacade.java
│   └── impl/
│       ├── GoodsFacadeImpl.java             # 实现 category.IGoodsFacade、store.IGoodsShopFacade
│       ├── DefaultGoodsOrderFacade.java
│       ├── GoodsPurchaseFacadeImpl.java
│       └── GoodsSnapshotFacadeImpl.java
├── support/
│   ├── GoodsSnGenerator.java
│   ├── GoodsStatusUtils.java
│   ├── GoodsAttrSupport.java                # 属性/规格校验与扁平化/聚合
│   └── GoodsBatchResponseSupport.java
├── exception/GoodsBatchOperationException.java
└── constant/GoodsConstants.java

baqing-shop/src/main/resources/mapper/goods/
├── BizGoodsMapper.xml
├── BizGoodsServiceSnapshotMapper.xml
├── BizGoodsAttrMapper.xml
└── BizGoodsPicMapper.xml

sql/biz_goods.sql                              # 含 shop_category_id、attr_template_id
sql/biz_goods_service_snapshot.sql
sql/biz_goods_attr.sql
sql/biz_goods_pic.sql                          # 商品副图 gallery
```

**跨模块依赖（接口与实现分离）：**

| 能力 | 接口位置 | 实现 / 调用 |
|------|----------|-------------|
| 删分类/模版/店前校验 | `category/facade/IGoodsFacade.java` | `goods/facade/impl/GoodsFacadeImpl.java` |
| 店铺商品分类下拉 | `category/facade/IShopGoodsCategoryFacade.java` | `category/facade/impl/ShopGoodsCategoryFacadeImpl.java` |
| 平台二级分类下拉 | `category/service/IPlatformCategoryService.java` | `SellerGoodsController` 或 `SellerPlatformCategoryController` |
| 分类路径 | `category/facade/ICategoryFacade.java` | `CategoryFacadeImpl` |
| 属性模版校验/预填 | `template/facade/IAttrTemplateFacade.java` | `SellerGoodsController` `/attrTemplateOptions`、`/attrTemplate/{id}`；亦可调 `/agri/seller/attrTemplate/*` |
| 商品服务目录 | `goodsservice/facade/IGoodsServiceFacade.java` | 发品 `serviceOptions` |
| 店铺上下文 | `category/support/SellerShopContext.java` | 拦截器 `SellerShopContextInterceptor` |
| 店铺上下文 API | `store/controller/SellerShopContextController.java` | `GET /agri/seller/context` |

**关联模块（并列包，非本模块源码）：** `modules/category`、`modules/template`、`modules/goodsservice`、`modules/store`。

---

## 2. 数据库设计

### 2.1 表复用与分工

| 表名 | 本模块用途 |
|------|------------|
| **`biz_goods`** | 商品主表；商家 CRUD + 状态字段 |
| **`biz_goods_service_snapshot`** | 发品勾选服务的 **展示快照** |
| **`biz_goods_attr`** | 发品属性/规格 **展示快照**（选用模版后可改） |
| `biz_goods_category` | join 分类路径；**不** 由本模块维护 |
| `biz_shop` | 店铺上下文、删店 join |
| `biz_shop_global_config` | 全局默认审核开关 |

**无商家独立商品表。**

### 2.2 `biz_goods` 字段（本模块相关）

| 字段 | 类型 | 说明 |
|------|------|------|
| goods_id | bigint PK | |
| shop_id | bigint NOT NULL | **创建时写入当前店铺**；不可 UPDATE 换店 |
| category_id | bigint NOT NULL | **平台二级分类**（`biz_goods_category`，`shop_id IS NULL`） |
| shop_category_id | bigint NULL | **店铺商品分类**（`biz_goods_category` 本店二级）；可空 |
| attr_template_id | bigint NULL | **属性模版**（`biz_goods_attr_template` 本店）；可空；删模版校验 |
| goods_sn | varchar(32) UK | Redis 生成，全平台唯一 |
| goods_name | varchar(200) | |
| main_pic | varchar(512) | 主图 URL |
| detail_content | text | 详情（首期 JSON/HTML 字符串） |
| sale_price | decimal(10,2) | **首期单规格** 售价 |
| stock | int | **首期单规格** 库存 |
| sales_count | int DEFAULT 0 | 销量（订单模块回写，本模块只读） |
| goods_status | char(1) | **0** 未上架 **1** 待审核 **2** 出售中 **3** 审核失败 **4** 已下架 |
| reject_reason | varchar(500) | 平台驳回原因 |
| submit_time | datetime | 首次 submit 时间 |
| audit_time | datetime | 审核时间 |
| off_shelf_time | datetime | 下架时间 |
| del_flag | char(1) | 0 存在 2 逻辑删除 |
| create_by / create_time / update_by / update_time | | 审计 |

**首期未落库、后续扩展（需求 §8 完整表单）：** 重量、条码、关键词、简述、多规格 SKU 等 → 见 §2.7；**`attr_template_id` + `biz_goods_attr` + `biz_goods_pic`（副图）+ `biz_goods_freight`（配送运费）已实现**。

### 2.3 增量 DDL

```sql
-- sql/biz_goods_shop_category_id.sql
ALTER TABLE `biz_goods`
  ADD COLUMN `shop_category_id` bigint(20) DEFAULT NULL COMMENT '店铺商品分类ID（本店二级 biz_goods_category）' AFTER `category_id`,
  ADD KEY `idx_shop_category_id` (`shop_category_id`, `del_flag`);

-- sql/biz_goods_attr_template.sql（商品侧扩展）
ALTER TABLE `biz_goods`
  ADD COLUMN `attr_template_id` bigint(20) DEFAULT NULL COMMENT '属性模版ID' AFTER `shop_category_id`,
  ADD KEY `idx_attr_template_id` (`attr_template_id`, `del_flag`);
```

完整建表见 `sql/biz_goods.sql`。

### 2.4 `biz_goods_attr`

| 字段 | 类型 | 说明 |
|------|------|------|
| attr_id | bigint PK | |
| goods_id | bigint NOT NULL | 商品 ID |
| attr_type | char(1) | **1** 属性项 **2** 规格项 |
| item_name | varchar(64) | 项名称 |
| value_text | varchar(128) | 单值一行；多项多行 |
| sort_no | int | 展示顺序 |

保存商品时 **先删后插** 该商品全部属性行；删除商品时物理删除关联行。模版编辑 **不追溯** 已保存商品（AT8）。

### 2.5 `biz_goods_service_snapshot`

| 字段 | 说明 |
|------|------|
| goods_id | FK 逻辑 |
| service_id | 来源 `biz_goods_service` |
| service_name / service_intro / service_icon | **快照** |
| sort_no | 展示顺序 |

保存商品时 **先删后插** 该商品全部快照行。

### 2.6 `biz_goods_pic`（商品副图）

| 字段 | 类型 | 说明 |
|------|------|------|
| pic_id | bigint PK | |
| goods_id | bigint NOT NULL | 商品 ID |
| pic_url | varchar(512) | 副图 URL（**不含** 主图 `main_pic`） |
| sort_no | int | 展示顺序，升序；从 0 起 |
| del_flag | char(1) | 0 正常（C 端只读 del=0） |
| create_by / create_time / … | | 审计 |

| 规则 | 说明 |
|------|------|
| 请求字段 | `GoodsSaveDTO.subPics`：`string[]`，**非必填** |
| 数量上限 | 最多 **10** 张（`GoodsConstants.MAX_SUB_PIC_COUNT`） |
| 保存策略 | **先删后插**（物理删）；不传或空数组 → 清空副图 |
| 删除商品 | 同步 `deleteByGoodsId` 副图行 |

**权威 DDL：** [`sql/biz_goods_pic.sql`](../../../../sql/biz_goods_pic.sql)

### 2.7 `biz_goods_freight`（配送运费）

| 字段 | 类型 | 说明 |
|------|------|------|
| goods_id | bigint PK/FK | 商品 ID（1:1） |
| freight_type | char(1) NOT NULL | **1** 固定运费 **2** 运费模版 |
| fixed_amount | decimal(10,2) NULL | `freight_type=1` 时金额 |
| freight_template_id | bigint NULL | `freight_type=2` 时模版 ID |
| del_flag | char(1) | 0 正常 |
| create_time / update_time | datetime | |

| 规则 | 说明 |
|------|------|
| 请求字段 | `GoodsSaveDTO.freightType`：**必填**；`fixedFreightAmount`（type=1）；`freightTemplateId`（type=2） |
| 互斥 | 固定运费与运费模版 **二选一**（FT12） |
| 模版校验 | `freightTemplateId` 须属 **当前店** 有效模版（`BizShopFreightTemplateMapper.selectById`） |
| 保存策略 | **upsert**（无行 insert，有行 update） |
| 删除商品 | 同步 `deleteByGoodsId` 运费行 |

**权威 DDL：** [`sql/biz_goods_freight.sql`](../../../../sql/biz_goods_freight.sql)

### 2.8 扩展表（非 v1.0 · 概要）

| 表（规划） | 用途 |
|------------|------|
| `biz_goods_sku` | 多规格 SKU（市场价、库存） |

v1.0 **使用** 主表 `sale_price`/`stock`/`main_pic`/`detail_content` + 服务快照 + **属性/规格快照** + **副图** + **配送运费**。

### 2.9 索引

| 索引 | 字段 | 用途 |
|------|------|------|
| uk_goods_sn | goods_sn | 编号唯一 |
| idx_shop_status | shop_id, goods_status, del_flag | 商家列表 + 状态 Tab |
| idx_category_id | category_id, del_flag | 分类检索 |
| idx_shop_category_id | shop_category_id, del_flag | 店铺分类删校验 |
| idx_attr_template_id | attr_template_id, del_flag | 属性模版删校验 |
| idx_goods_attr | goods_id, attr_type, sort_no | 详情聚合属性/规格 |
| idx_goods_gallery | goods_id, del_flag, sort_no | 副图列表 |
| idx_submit_time | submit_time | 待审核排序（平台侧） |

### 2.10 商家列表 SQL 约定

```sql
WHERE g.del_flag = '0'
  AND g.shop_id = #{shopId}          -- 强制当前店铺（GL1）
  -- 含 goods_status='0'，与平台列表不同
```

### 2.11 字典

| dict_type | 说明 |
|------|------|
| goods_status | 0～4 状态展示 |

---

## 3. 状态机（Service）

> 与《商品审核技术方案》§3、《关联需求分析》§5.4 **同源**。

| 操作 | 方法 | 执行方 | 前置 status | 结果 status |
|------|------|--------|-------------|-------------|
| 保存（新建） | `insertSellerGoods` | 商家 | — | **0** |
| 保存（编辑） | `updateSellerGoods` | 商家 | 0/1/2/3/4 | **不变** |
| 提交上架 | `submitGoods` | 商家 | 0/3/4 | **1** 或 **2**¹ |
| 审核通过 | `auditGoods` | 平台 | 1 | 2 |
| 审核驳回 | `auditGoods` | 平台 | 1 | 3 |
| 下架 | `offShelfSeller` / `offShelfPlatform` | 商家/平台 | 2 | 4 |
| 删除 | `deleteSellerGoodsBatch` | 商家 | 0/3/4 | del_flag=2 |

¹ `default_audit_pass='1'` → **2**，否则 **1**。  
² 出售中/待审核 **阻断**；O10 订单校验 **待实现**。

**禁止：** 请求体携带 `goodsStatus` 直接改态（`rejectGoodsStatusInBody`）。

**批量策略：** `submit` / `offShelf` / `delete` 均 **先全量校验** → 任一失败 `GoodsBatchOperationException`，**零行** 更新。

---

## 4. 跨模块 Facade

| 接口 | 提供方 | 本模块用法 |
|------|--------|------------|
| `IPlatformCategoryService` | 分类 | `selectPlatformLevel2Options()` — 发品「商品分类」 |
| `IShopGoodsCategoryFacade` | 店铺商品分类 | `listOptionsByShopId(shopId, visibleOnly)` — 「店铺商品分类」 |
| `IAttrTemplateFacade` | 属性模版 | 发品下拉 `listOptionsByShopId`；删模版时 `IGoodsFacade.existsByAttrTemplateId` |
| `IGoodsServiceFacade` | 商品服务 | `listAllEnabled` / `listDefaultShow`；删服务前 `existsByServiceId` |
| `IShopGlobalConfigFacade` | 店铺设置 | `getDefaultAuditPass()` |
| `IShopFacade` | 店铺 | 删店时 `assertShopMaintable` |
| `IGoodsFacade` | 商品 | `existsByShopCategoryId`；`existsByAttrTemplateId`（接口在 **category**，实现在 **goods**） |
| `IGoodsShopFacade` | 商品 | `hasBlockingGoodsForShopDelete`（`GoodsFacadeImpl`） |
| `IOrderFacade`³ | 订单 | `hasUnfinishedOrderByGoodsId(goodsId)` — 下架前置 |

³ 订单模块 Facade，下架 O10 校验 **待对接**。

---

## 5. 接口设计

**公共约定**

| 项 | 说明 |
|----|------|
| 基路径 | `/agri/seller/goods` |
| 请求头 | **`X-Shop-Id`**：当前店铺 ID（除 `/agri/seller/context` 外 `/agri/seller/**` 必传） |
| 分页 | `pageNum` / `pageSize` → `TableDataInfo` |
| 权限 | `@PreAuthorize("@ss.hasPermi('agri:seller:goods:…')")` |
| 批量失败 | `code!=200`，`data.reasons[]` 明细 |

---

### 5.1 商品列表 `GET /agri/seller/goods/list`

| 项 | 说明 |
|----|------|
| 权限 | `agri:seller:goods:list` |
| 店铺 | `SellerShopContext.getShopId()`，与 header 一致 |

**Query：**

| 参数 | 说明 |
|------|------|
| goodsSn | 编号模糊 |
| goodsName | 名称模糊 |
| categoryId | **平台** 二级分类 ID |
| goodsStatusQuery | 0/1/2/3/4；不传查全部 |
| keyword | 可选；sn+name 组合模糊 |

**`rows`（GoodsListVO）：**

| 字段 | 说明 |
|------|------|
| goodsId, goodsSn, goodsName, mainPic | |
| salePrice, salesCount | 售价、销量 |
| stock | 可选列 |
| goodsStatus, goodsStatusLabel | 字典 goods_status |
| categoryPath | 一级 > 二级（平台分类） |
| shopCategoryName | 可选；店铺商品分类名 |

**排序：** 默认 `create_time DESC`（待审核 Tab 可前端传参或扩展 `orderBy=submitTime`）。

---

### 5.2 商品详情 `GET /agri/seller/goods/{goodsId}`

| 项 | 说明 |
|----|------|
| 权限 | `agri:seller:goods:query` |
| 校验 | `goods_id` 须属 **当前 shop_id** |

**`data`（GoodsDetailVO）：**

| 字段 | 说明 |
|------|------|
| 列表字段 | 同 §5.1 |
| shopId, categoryId, shopCategoryId | |
| attrTemplateId, attrTemplateName | 选用模版 ID 与名称（详情回显） |
| attributes[], specs[] | 属性/规格快照（`GoodsAttrItemDTO`：`itemName` + `values[]`） |
| detailContent, stock | |
| subPics[] | 商品 **副图** URL 列表（按 sort_no 升序；**不含** mainPic） |
| freightType | **1** 固定运费 **2** 运费模版 |
| fixedFreightAmount | 固定运费金额（type=1） |
| freightTemplateId, freightTemplateName | 运费模版 ID 与名称（type=2） |
| freightDesc | 运费展示摘要（固定：「运费 ¥X.XX」；模版：委托 `IFreightTemplateFacade.buildFreightDesc`） |
| rejectReason, submitTime, auditTime, offShelfTime | |
| services[] | 服务快照列表 |
| canSubmit, canOffShelf, canDelete, canEdit | 前端按钮态（可选） |

---

### 5.3 添加商品 `POST /agri/seller/goods`

| 项 | 说明 |
|----|------|
| 权限 | `agri:seller:goods:add` |
| 日志 | title=商家商品 |

**Body（GoodsSaveDTO）：**

```json
{
  "categoryId": 1002,
  "shopCategoryId": 2005,
  "attrTemplateId": 4,
  "goodsName": "复合肥 50kg",
  "mainPic": "/profile/upload/2026/05/01/a.jpg",
  "subPics": [
    "/profile/upload/2026/05/01/b.jpg",
    "/profile/upload/2026/05/01/c.jpg"
  ],
  "detailContent": "<p>详情</p>",
  "salePrice": 128.00,
  "stock": 500,
  "serviceIds": [1, 3],
  "attributes": [
    { "itemName": "品牌", "values": ["华为"] }
  ],
  "specs": [
    { "itemName": "内存", "values": ["8G", "16G"] }
  ],
  "freightType": "1",
  "fixedFreightAmount": 10.00
}
```

或选用运费模版：

```json
{
  "freightType": "2",
  "freightTemplateId": 1
}
```

| 校验 | 说明 |
|------|------|
| categoryId | 须为 **平台二级** 有效分类 |
| shopCategoryId | 若传，须属 **当前店** 二级分类 |
| attrTemplateId | 若传，须属 **当前店** 有效属性模版 |
| freightType | **必填**；**1** 固定运费 / **2** 运费模版 |
| fixedFreightAmount | `freightType=1` 时 **必填**，≥0 |
| freightTemplateId | `freightType=2` 时 **必填**，须本店有效模版 |
| attributes / specs | 可选；若传则项名非空、每项至少一值、同段项名/值不重复、属性与规格项名不可相同 |
| subPics | **可选**；最多 **10** 张；空白项忽略；超出 →「商品副图最多10张」 |
| serviceIds | 须全部存在于未删除服务目录 |
| goodsStatus | **禁止** 传入 |

**响应：** `{ "goodsId": 10001 }`；落库 `goods_status='0'`，生成 `goods_sn`。

---

### 5.4 编辑商品 `PUT /agri/seller/goods`

| 项 | 说明 |
|----|------|
| 权限 | `agri:seller:goods:edit` |
| Body | 同 §5.3 + **goodsId** 必填 |

- **不修改** `goods_status`、`shop_id`、`goods_sn`。  
- 各状态均可编辑（P14）；保存后刷新服务快照、**属性/规格快照**、**副图**与 **配送运费**。

---

### 5.5 提交上架

#### 5.5.1 单独 `PUT /agri/seller/goods/{goodsId}/submit`

| 项 | 说明 |
|----|------|
| 权限 | `agri:seller:goods:edit` |
| 前置 | status ∈ {0, 3, 4} |

**响应 data：**

```json
{
  "goodsId": 10001,
  "goodsStatus": "1",
  "goodsStatusLabel": "待审核"
}
```

#### 5.5.2 批量 `PUT /agri/seller/goods/submit`（已实现）

**Body：** `{ "goodsIds": [10001, 10002] }`（`GoodsBatchIdsDTO`）  
规则：含非 {0,3,4} → **整批失败**。

---

### 5.6 下架

#### 5.6.1 单独 `PUT /agri/seller/goods/{goodsId}/offShelf`

| 项 | 说明 |
|----|------|
| 权限 | `agri:seller:goods:offshelf` |
| 前置 | status=2；**无 O10 未完成订单**（**待实现**，当前 `DefaultGoodsOrderFacade` 占位） |

#### 5.6.2 批量 `PUT /agri/seller/goods/offShelf`

| 项 | 说明 |
|----|------|
| 权限 | `agri:seller:goods:offshelf` |
| Body | 复用 `GoodsOffShelfDTO`：`{ "goodsIds": [10001] }` |

失败：`GoodsBatchOperationException` → `{ "reasons": ["仅出售中商品可下架：G20260501001"] }`。

---

### 5.7 删除

#### 5.7.1 单独 / 批量 `DELETE /agri/seller/goods/{goodsIds}`（已实现）

| 项 | 说明 |
|----|------|
| 权限 | `agri:seller:goods:remove` |
| 前置 | status ∈ {0, 3, 4} |
| 路径 | 单条 `1001`；批量 `1001,1002` |

逻辑删除：`del_flag='2'`；含不可删状态 → **整批失败**（`GoodsBatchIdsDTO`）。  
同步物理删除：`biz_goods_service_snapshot`、`biz_goods_attr`、**`biz_goods_pic`**、**`biz_goods_freight`** 关联行。

---

### 5.8 发品辅助下拉

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/agri/seller/goods/categoryOptions` | `list` / `add` / `edit` | 平台二级「商品分类」`IPlatformCategoryService.selectPlatformLevel2Options()` |
| GET | `/agri/seller/goods/shopCategoryOptions` | `list` | 本店二级「店铺商品分类」`IShopGoodsCategoryFacade.listOptionsByShopId(shopId, false)` |
| GET | `/agri/seller/category/platformLevel2Options` | 同上 | 与上同源（分类模块只读入口，可选） |
| GET | `/agri/seller/goods/serviceOptions` | `list` | `{ all: [], defaultShow: [] }` |
| GET | `/agri/seller/goods/attrTemplateOptions` | `list` / `add` / `edit` | 本店属性模版下拉；委托 `IAttrTemplateFacade.listOptionsByShopId` |
| GET | `/agri/seller/goods/attrTemplate/{templateId}` | 同上 | 选用模版预填；返回 `attributes`、`specs`（委托 `getDetailForGoods`） |
| GET | `/agri/seller/goods/freightTemplateOptions` | `list` / `add` / `edit` | 本店运费模版下拉；委托 `IFreightTemplateFacade.listOptionsByShopId` |
| GET | `/agri/seller/goods/freightDefault` | 同上 | 默认模版预填；有默认时返回 `{ freightType:"2", freightTemplateId, templateName, ... }` |
| GET | `/agri/seller/goods/freightTemplate/{templateId}` | 同上 | 选用模版回显；返回模版名称、发货承诺、计价规则等 |
| GET | `/agri/seller/attrTemplate/options` | 属性模版模块 | 与 `attrTemplateOptions` **同源**（亦可直调） |

---

### 5.9 与平台接口对照

| 能力 | 商家（本方案） | 平台 |
|------|----------------|------|
| 列表 | `GET /agri/seller/goods/list`（含 status=0） | `GET /agri/goods/list`（status≠0） |
| 详情 | `GET /agri/seller/goods/{id}` | `GET /agri/goods/{id}` |
| 保存 | POST/PUT `/agri/seller/goods` | — |
| 提交上架 | PUT `.../submit` | — |
| 审核 | — | PUT `/agri/goods/audit` |
| 下架 | PUT `.../offShelf` | PUT `/agri/goods/offShelf` |
| 删除 | DELETE `.../{id}` | — |

---

## 6. Service 核心逻辑摘要

```text
insertSellerGoods
  → requireShopId
  → validateSaveFields + assertPlatformCategoryLevel2(categoryId)
  → assertShopCategoryIfPresent(shopCategoryId)
  → assertAttrTemplateIfPresent(attrTemplateId)
  → validateFreightConfig(freightType / fixedFreightAmount / freightTemplateId)
  → goodsSn = GoodsSnGenerator.next()
  → insert status=0（含 attr_template_id）
  → saveServiceSnapshots
  → saveGoodsAttrs（先删后插 biz_goods_attr）
  → saveGoodsFreight（upsert biz_goods_freight）

updateSellerGoods
  → 同上校验（goodsId 属当前店）
  → updateSeller + saveServiceSnapshots + saveGoodsAttrs + saveGoodsFreight

submitGoods
  → status in (0,3,4)
  → target = defaultAuditPass ? 2 : 1
  → updateSubmitStatus（首次写 submit_time）

offShelfSeller
  → offShelfBatch（shopId 限定）
  → [待] orderFacade 校验 O10
  → status 2 → 4，写 off_shelf_time

deleteSellerGoodsBatch
  → status in (0,3,4)
  → logic delete + 整批失败策略
  → delete service snapshot + goods_attr rows
  → [待] orderFacade 校验 O10（下架时）
```

**分类校验口径：**

| 字段 | SQL 条件 |
|------|----------|
| category_id | `biz_goods_category.category_id`，`shop_id IS NULL`，`parent_id > 0` |
| shop_category_id | `biz_goods_category.category_id`，`shop_id = #{shopId}`，`parent_id > 0` |
| attr_template_id | `biz_goods_attr_template.template_id`，`shop_id = #{shopId}`，`del_flag = '0'` |

**错误 msg（属性/规格）：** 项名为空 / 重复 / 与对端重名 / 值为空 / 值重复 → 见 `GoodsConstants.MSG_ATTR_*`。

**错误 msg（属性模版）：** 无效或非本店模版 → `请选择本店有效属性模版`（`GoodsConstants.MSG_ATTR_TEMPLATE_INVALID`）。

**错误 msg（配送运费）：** 未选方式 → `请选择配送运费方式`；固定运费未填/为负 → `请输入固定运费` / `固定运费不能为负数`；模版未选/无效 → `请选择运费模版` / `请选择本店有效运费模版`。

---

## 7. 权限与菜单

| 权限标识 | 说明 |
|----------|------|
| agri:seller:goods:list | 列表、下拉 |
| agri:seller:goods:query | 详情 |
| agri:seller:goods:add | 新增 |
| agri:seller:goods:edit | 编辑、提交上架 |
| agri:seller:goods:offshelf | 下架 |
| agri:seller:goods:remove | 删除 |

**菜单 SQL：** `sql/agri_seller_goods_menu.sql`  
**组件路径：** `agri/seller/goods/index`（发品/编辑可 `goods/form`）。

---

## 8. 实现分期

| 阶段 | 内容 | 状态 |
|------|------|------|
| **v1.0** | 列表/详情/单规格 CRUD/单独 submit·offShelf/服务快照/平台分类与服务下拉 | **已实现** |
| **v1.0 补全** | `shop_category_id` + 店铺分类下拉 + 删除 + 批量 submit/offShelf/delete | **已实现** |
| **v1.0 补全·模版引用** | `attr_template_id` 保存/详情回显 + 本店模版校验 | **已实现** |
| **v1.0 补全·属性快照** | `biz_goods_attr` 写入/详情回显；预填接口；`GoodsAttrSupport` | **已实现** |
| **v1.0 补全·商品副图** | `biz_goods_pic` + `subPics` 保存/详情回显；最多 10 张、非必填 | **已实现** |
| **v1.0 补全·配送运费** | `biz_goods_freight` + `freightType` 必填；固定/模版二选一；下拉与默认预填 | **已实现** |
| **v1.0 补全·O10** | 下架前未完成订单校验（`IGoodsOrderFacade`） | **待实现** |
| **v1.x** | 多规格 SKU、关键词检索扩展 | 规划 |

---

## 9. 测试要点

| 编号 | 场景 |
|------|------|
| T1 | 未传 `X-Shop-Id` → 业务失败 |
| T2 | 列表仅返回当前店商品 |
| T3 | 保存 → status=0；body 带 goodsStatus → 拒收 |
| T4 | submit：免审关→1，免审开→2 |
| T5 | 待审核/出售中 edit 保存 → 状态不变 |
| T6 | 批量 submit 含 status=2 → 整批失败 |
| T7 | 下架非出售中 → 失败 |
| T8 | 删除出售中 → 失败 |
| T9 | 跨店 goodsId → MSG_NOT_OWNER |
| T10 | 服务快照：保存后详情与目录变更隔离 |
| T11 | attrTemplateId：本店有效模版写入；跨店/无效 ID → 失败 |
| T12 | attributes/specs：保存后详情回显；校验项名/值 |
| T13 | subPics：0～10 张保存/回显；第 11 张拒收；删商品同步清副图 |
| T14 | freightType 必填；固定/模版互斥校验；模版须本店有效；详情回显 freightDesc |

---

## 10. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：商家商品列表技术方案；复用 biz_goods 与平台状态机；接口/DDL 与实现对齐并标注待办 |
| **v1.1** | 同步代码：v1.0 补全已实现；统一平台/店铺分类口径；O10 仍标待实现 |
| **v1.2** | 同步 `attr_template_id`：GoodsSaveDTO/详情/Mapper/校验；下拉走属性模版模块 `/options` |
| **v1.3** | §1.3 模块落位与代码对齐：`IGoodsFacade` 跨包、`template` 依赖、快照/订单 Facade |
| **v1.4** | `biz_goods_attr` 保存/回显；`attributes`/`specs` 请求体；`/goods/attrTemplateOptions` 与预填接口 |
| **v1.5** | **`biz_goods_pic` 商品副图**：`subPics` 最多 10 张非必填；`BizGoodsPicMapper`；删商品同步清副图 |
| **v1.6** | **`biz_goods_freight` 配送运费**：`freightType` 必填；固定/模版保存与详情回显；`/freightTemplateOptions`、`/freightDefault` |

---

*文档版本：v1.6 · 关联《商品列表功能需求.md》v1.4、《属性模版技术方案.md》v1.3、《运费模版技术方案.md》v1.1 · 技术栈 RuoYi v3.9.2-springboot2 + MySQL 5.7.39。*
