# 店铺商品分类 — 技术方案

> **依据：** 《店铺商品分类功能需求.md》v1.1  
> **关联：** 《关联需求分析.md》§5.1/§6.3；平台《商品分类技术方案》v1.2、《店铺管理技术方案》v1.2.4；商家《商品列表功能需求.md》；《角色管理技术方案》v1.1  
> **范围：** 商家端 **当前店铺** 下 **两级** 店铺商品分类 CRUD；为 **商品列表 · 店铺商品分类** 字段提供数据源。  
> **原则：** 复用 `biz_goods_category`（`shop_id` 非空）；与平台/商家 **商品分类**（`shop_id IS NULL` 或 `/agri/seller/category`）**数据隔离**；复用 `X-Shop-Id` 店铺上下文；**平台不维护** 本模块数据。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 店铺上下文 | `SellerShopContextInterceptor`（`X-Shop-Id`）；`SellerShopContext.getShopId()` |
| 删店拦截 | 复用 `IShopFacade.assertShopMaintable(shopId)` |
| 文件 | 分类图片走 `CommonController` 上传 |
| 日志 | 增删改 `@Log` |

### 1.1 与平台侧「商品分类」的边界

| 维度 | 平台 · 商品分类 | 本模块 · 店铺商品分类 |
|------|-----------------|----------------------|
| 表 | `biz_goods_category`（`shop_id IS NULL`） | **`biz_goods_category`（`shop_id = 当前店铺`）** |
| 结构 | 一级 + 二级 | **一级 + 二级**（同表、同层级规则） |
| 商家路径 | `/agri/seller/shopCategory` | **`/agri/category`**（平台维护）+ **`/agri/seller/category/platformLevel2Options`**（只读） |
| 商品字段 | `biz_goods.category_id`（平台二级） | **`biz_goods.shop_category_id`（本店二级）** |
| 平台端 | 可维护平台树 + 监管只读 | **不参与** |

> **口径说明：** 平台分类与店铺商品分类 **共用一张表**，以 `shop_id` 区分数据范围；商家端两个菜单/Controller **独立**，避免混用 ID。

### 1.2 模块落位

> **说明：** 平台分类与店铺商品分类 **共用** `category` 包；本方案 CRUD 入口为 `SellerShopGoodsCategoryController`。

**本模块包：** `com.ruoyi.web.modules.category`（店铺分类相关子集）

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/category/
├── controller/
│   ├── SellerShopGoodsCategoryController.java    # /agri/seller/shopCategory（本方案 CRUD）
│   ├── SellerPlatformCategoryController.java     # /agri/seller/category（平台分类只读）
│   └── PlatformCategoryController.java           # /agri/category（平台商品分类 CRUD）
├── service/
│   ├── IShopGoodsCategoryService.java              # 店铺商品分类 Service 接口
│   ├── ICategoryService.java                       # 平台分类 Service 接口
│   ├── IPlatformCategoryService.java               # 平台二级下拉
│   └── impl/CategoryServiceImpl.java               # 平台 + 店铺分类共用实现
├── domain/BizGoodsCategory.java
├── mapper/BizGoodsCategoryMapper.java
├── facade/
│   ├── IShopGoodsCategoryFacade.java
│   ├── ICategoryFacade.java                        # 分类路径（商品详情 join）
│   ├── IGoodsFacade.java                           # 删分类前商品引用校验（实现在 goods 模块）
│   ├── IShopFacade.java
│   └── impl/
│       ├── ShopGoodsCategoryFacadeImpl.java        # IShopGoodsCategoryFacade 实现
│       ├── CategoryFacadeImpl.java
│       ├── DefaultShopFacade.java
│       └── DefaultGoodsFacade.java                 # IGoodsFacade 占位（可被 GoodsFacadeImpl 覆盖）
├── support/
│   ├── SellerShopContext.java
│   └── SellerShopContextInterceptor.java           # X-Shop-Id 拦截
├── config/CategoryWebConfig.java                   # 注册店铺上下文拦截器
├── exception/CategoryBatchDeleteException.java
├── constant/CategoryConstants.java
└── vo/
    ├── CategoryRowVO.java、CategoryTreeVO.java、CategoryLevel2OptionVO.java
    └── ShopGoodsCategoryOptionVO.java              # 商品发品下拉

baqing-shop/src/main/resources/mapper/category/BizGoodsCategoryMapper.xml
sql/biz_goods_category.sql
sql/agri_seller_shop_category_menu.sql
```

**关联包（非本模块 CRUD，但协作）：**

| 包 | 协作 |
|----|------|
| `modules/goods` | `GoodsFacadeImpl` 实现 `IGoodsFacade.existsByShopCategoryId` |
| `modules/store` | `SellerShopContextController` — `GET /agri/seller/context` |

**前端页面：** `agri/seller/shopCategory/index`

### 1.3 关联协作链

```text
【登录】GET /agri/seller/context → 前端设置 X-Shop-Id
            ↓
【本模块】biz_goods_category（shop_id = 当前店铺；最多两级）
            ↓
【商品列表】发品/编辑 → shop_category_id（仅二级）
            ↓
【平台商品管理】只读引用商品档案，不维护本表
```

| 关联模块 | 协作方式 |
|----------|----------|
| **店铺管理** | 上游 `shop_id`；`del_flag=2` 时接口阻断 |
| **商品分类**（平台二级） | **同表不同 shop_id**；发品时「商品分类」「店铺商品分类」两字段并列 |
| **商品列表** | `GET /level2Options` 或 `IShopGoodsCategoryFacade.listOptionsByShopId` |
| **角色管理** | 菜单权限 `agri:seller:shopCategory:*` |

### 1.4 跨模块 Facade

| 接口 | 提供方 | 调用方 | 方法 |
|------|--------|--------|------|
| **`IShopGoodsCategoryFacade`** | **本模块** | 商品、C 端（可选） | `listOptionsByShopId(shopId, visibleOnly)` — **仅二级**，label=`一级 > 二级` |
| **`ICategoryFacade`** | 分类 | Facade 内部 | `listLevel2ByShopId(shopId)` |
| **`IGoodsFacade`** | 商品 | **本模块** | `existsByShopCategoryId(shopCategoryId)` — 删类前置 |
| **`IShopFacade`** | 店铺 | 本模块 | `assertShopMaintable` / `getShopName` |

---

## 2. 数据库设计

### 2.1 本模块表

| 表名 | 说明 |
|------|------|
| `biz_goods_category` | 商品分类（平台 + 店铺共用；本模块 `shop_id` **非空**） |

DDL 见 `sql/biz_goods_category.sql`。

### 2.2 字段 `biz_goods_category`（本模块使用子集）

| 字段 | 类型 | 说明 |
|------|------|------|
| category_id | bigint PK | 分类 ID |
| shop_id | bigint NOT NULL | **当前店铺 ID**（本模块写入时必填） |
| parent_id | bigint | **0=一级**；二级=父级 `category_id` |
| category_level | char(1) | **1** 一级 **2** 二级 |
| category_name | varchar(64) | 同 `shop_id`、同 `parent_id` 下唯一（`del_flag=0`） |
| category_pic | varchar(512) | 分类图 URL；**必填** |
| sort_no | int | 同层排序，越小越靠前；默认 0 |
| show_flag | char(1) | 0 否 1 是；默认 **1** |
| hot_flag | char(1) | 0 否 1 是；默认 **0** |
| del_flag | char(1) | 0 存在 2 删除 |
| create_by/time, update_by/time | | |

**层级规则（对齐平台 C1～C3）：**

| category_level | parent_id |
|:--------------:|-----------|
| 1 | 0 |
| 2 | 本店某一级 `category_id`（且父节点 `category_level=1`） |

**禁止：** `parent_id` 指向二级节点（三级）。

### 2.3 索引

| 索引 | 字段 |
|------|------|
| idx_shop_parent | shop_id, parent_id, del_flag |
| idx_shop_level | shop_id, category_level, del_flag, sort_no |

名称唯一：**应用层**校验 `(shop_id, parent_id, category_name)` where `del_flag='0'`。

### 2.4 商品表扩展（商品列表模块）

| 表 | 字段 | 说明 |
|----|------|------|
| `biz_goods` | `shop_category_id` bigint，**可空** | 指向 **本店二级** `biz_goods_category.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`);
```

> 商品 **仅挂二级**；一级不直接挂商品。删类校验依赖 `IGoodsFacade.existsByShopCategoryId`。

### 2.5 字典

复用 `sys_dict`：**`yes_no`**（show_flag、hot_flag）；**`category_level`**（1 一级 / 2 二级）。

---

## 3. 接口设计

**基路径：** `/agri/seller/shopCategory`  
**公共请求头：** `X-Shop-Id`（必填；未携带返回「请先选择当前店铺」）  
**数据范围：** `shop_id = 当前店铺` AND `del_flag='0'`；店铺已逻辑删除则 `assertShopMaintable` 拦截。

### 3.1 接口一览

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/scope` | list | 当前店铺 `shopId`、`shopName` |
| GET | `/list` | list | **分页**扁平列表（含一级/二级行；`TableDataInfo`） |
| GET | `/tree` | list | 树形 `children` 嵌套（不分页） |
| GET | `/level1Options` | list | 一级下拉（添加二级时用） |
| GET | `/level2Options` | list | **仅二级** 下拉（商品发品复用；label=`一级 > 二级`） |
| GET | `/{categoryId}` | query | 详情（编辑回显） |
| POST | `/` | add | 新增一级或二级 |
| PUT | `/` | edit | 编辑 |
| DELETE | `/{categoryIds}` | remove | 删除（**支持批量**；整批失败） |

**列表查询参数：** `pageNum`、`pageSize`、`keyword`（分类名称模糊，可选）。

### 3.2 列表 `GET /list`

**排序：** 一级分组；同层 `sort_no ASC, create_time ASC`。  
**过滤：** `shop_id = 当前店铺` AND `del_flag = '0'`。

**响应示例（扁平行）：**

```json
{
  "code": 200,
  "msg": "查询成功",
  "total": 5,
  "rows": [
    {
      "categoryId": 1,
      "categoryName": "店内专区",
      "categoryLevel": "1",
      "parentId": 0,
      "parentName": null,
      "categoryPic": "/profile/upload/sc1.jpg",
      "showFlag": "1",
      "hotFlag": "0",
      "sortNo": 0,
      "canAddChild": true
    },
    {
      "categoryId": 2,
      "categoryName": "热销推荐",
      "categoryLevel": "2",
      "parentId": 1,
      "parentName": "店内专区",
      "categoryPic": "/profile/upload/sc2.jpg",
      "showFlag": "1",
      "hotFlag": "0",
      "sortNo": 0,
      "canAddChild": false
    }
  ]
}
```

### 3.3 树形 `GET /tree`

返回 `CategoryTreeResultVO`：`shopId`、`shopName`、`tree[]`；一级节点含 `children[]` 二级。

### 3.4 新增 / 编辑

**POST `/`、PUT `/` 请求体：**

```json
{
  "categoryId": 2,
  "categoryLevel": "2",
  "parentId": 1,
  "categoryName": "热销推荐",
  "categoryPic": "/profile/upload/sc2.jpg",
  "sortNo": 0,
  "showFlag": "1",
  "hotFlag": "0"
}
```

| 校验 | 说明 |
|------|------|
| categoryLevel | **1** 或 **2**；一级时 `parentId=0` |
| parentId | 二级须指向本店有效一级 |
| categoryName | 非空，≤64；**同级**唯一（编辑排除自身） |
| categoryPic | 非空 |
| sortNo | 整数 ≥ 0；默认 0 |
| showFlag / hotFlag | 0 / 1 |
| categoryId | PUT 必填；须属于当前店铺 |

**业务约束（SC7）：** 修改 `show_flag` / `hot_flag` / `sort_no` / 名称 **不 UPDATE** `biz_goods`。

### 3.5 下拉 `GET /level2Options`

供 **商品列表 · 店铺商品分类** 字段；**不分页**；**仅二级**。

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": [
    { "categoryId": 2, "label": "店内专区 > 热销推荐", "categoryName": "热销推荐" }
  ]
}
```

商品模块亦可调用 `IShopGoodsCategoryFacade.listOptionsByShopId(shopId, visibleOnly)`；`visibleOnly=true` 时过滤 `show_flag=0`。

### 3.6 删除 `DELETE /{categoryIds}`

```text
校验 categoryIds 均属于当前店铺
    → 若含一级：expand 其下全部二级 ID
    → 对每个目标 ID 调用 IGoodsFacade.existsByShopCategoryId
    ├── 任一有商品 → CategoryBatchDeleteException；整批取消；data.reasons[]
    └── 全无商品 → logicDeleteByIds（一级级联子级）
```

- **逻辑删除**；支持 **批量**；**任一条不通过则整批失败**。  
- 前端 **二次确认**；删一级须提示级联删除下属二级。

### 3.7 `GET /scope`

返回当前 `X-Shop-Id` 对应 `shopId`、`shopName`；列表页展示 **当前店铺名称**（只读）。

---

## 4. 对外协作

### 4.1 商品模块（下游）

| 场景 | 接口 / Facade |
|------|----------------|
| 发品/编辑下拉 | `GET /level2Options` 或 `IShopGoodsCategoryFacade.listOptionsByShopId` |
| 删类前置 | `IGoodsFacade.existsByShopCategoryId(categoryId)` |
| 平台监管展示 | join `biz_goods_category`（`shop_id=商品.shop_id`）或商品冗余名称 |

### 4.2 `existsByShopCategoryId` 口径

```sql
EXISTS (
  SELECT 1 FROM biz_goods g
  WHERE g.shop_category_id = #{shopCategoryId}
    AND g.del_flag = '0'
)
```

- **不统计** 已逻辑删除商品。  
- 商品 **仅挂二级**；删一级时须校验 **全部下属二级**。

### 4.3 平台侧

- **无** 平台 CRUD 接口。  
- 平台 **商品管理** 检索/详情 **只读** 商品上的 `shop_category_id`；**不** 代维商家分类数据（对齐 SC8）。

### 4.4 C 端（可选）

若前台按店铺商品分类导航：`IShopGoodsCategoryFacade.listOptionsByShopId(shopId, true)`；**show_flag=0** 不参与展示；**不自动** 改商品上下架（需求 §10.2）。

---

## 5. 核心流程

### 5.1 Service 伪代码

```text
requireShopId()                    // SellerShopContext；null →「请先选择当前店铺」
assertShopMaintable(shopId)        // 删店后阻断
normalizeLevelOnSave()             // 一级 parentId=0；二级 validateParentForLevel2
countByName(shopId, parentId, name) // 同级唯一
expandDeleteIds()                  // 删一级 → 含全部子二级
existsByShopCategoryId()           // 任一有商品 → CategoryBatchDeleteException
logicDeleteByIds()
```

### 5.2 端到端

```text
【平台】店铺管理 ── 开店（分类初始为空）
            ↓
【商家】登录 → X-Shop-Id → POST /shopCategory ── 添加一级 → 添加二级
            ↓
【商家】商品列表 ── shop_category_id（二级）
            ↓
【平台】商品监管 ── 只读
```

---

## 6. 业务规则映射

| 编号 | 实现 |
|------|------|
| SC1 | SQL `shop_id = X-Shop-Id` AND `del_flag='0'` |
| SC2 | `biz_goods_category`；`category_level` 1/2；禁止三级 |
| SC3 | `(shop_id, parent_id, category_name)` 同级唯一 |
| SC4 | POST/PUT 校验 name、pic、sortNo、showFlag、hotFlag、level |
| SC5 | 同层 ORDER BY sort_no ASC, create_time ASC |
| SC6 | `existsByShopCategoryId` 阻断删除（含一级校验子级） |
| SC7 | 无 UPDATE biz_goods |
| SC8 | 无平台写接口；店间 shop_id 隔离；与平台分类 shop_id IS NULL 隔离 |
| SC9 | 拦截器 + Service 双重 shop_id 校验 |
| SC10 | `/level2Options` + Facade 供商品列表 |
| SC11 | AjaxResult / TableDataInfo |
| SC12 | 商品 **仅挂本店店铺商品二级** |

---

## 7. 菜单权限

| 权限 | 标识 |
|------|------|
| 列表 / 树 / 下拉 | `agri:seller:shopCategory:list` |
| 详情 | `agri:seller:shopCategory:query` |
| 新增 | `agri:seller:shopCategory:add` |
| 修改 | `agri:seller:shopCategory:edit` |
| 删除 | `agri:seller:shopCategory:remove` |

**菜单 SQL：** `sql/agri_seller_shop_category_menu.sql`（组件 `agri/seller/shopCategory/index`）。

---

## 8. 校验与错误 msg

| 场景 | msg |
|------|-----|
| 未选店铺 | 请先选择当前店铺 |
| 同级名称重复 | 同级分类名称已存在 |
| 名称为空 | 请输入分类名称 |
| 图片为空 | 请上传分类图片 |
| 二级上级无效 | 二级分类的上级必须是一级分类 / 上级分类不存在 |
| 层级无效 | 分类层级无效 / 不支持三级分类 |
| 删类有商品 | 分类「{name}」下存在商品，请先修改商品分类或处理商品 |
| 批量删失败 | 部分分类不满足删除条件，整批操作已取消 |
| 跨店 ID | 分类不存在 |
| 店铺已关闭 | 店铺已关闭，无法维护分类 |

---

## 9. 测试要点

| 类型 | 覆盖 |
|------|------|
| 单元 | `CategoryServiceImpl`（店铺 scope）；同级唯一；级联 expandDeleteIds；批量删整批失败 |
| 接口 | `/list`、`/tree`、`/level1Options`、`/level2Options`、CRUD、批量 DELETE |
| 协作 | `existsByShopCategoryId`；与 `/agri/seller/category` 数据隔离 |
| UI | 树形/分级列表；一级「添加子分类」；删一级二次确认 |

---

## 10. 非本期范围

批量导入、分类恢复、导出、平台代维、C 端完整 REST、与平台分类合并展示页。

---

## 11. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：独立表 `biz_shop_goods_category` 单层方案（已废弃） |
| **v1.1** | **与实现对齐**：复用 `biz_goods_category`（`shop_id` 非空）；两级结构；`/agri/seller/shopCategory`；批量删除；商品挂二级 |
| **v1.2** | §1.2 模块落位与代码对齐：Facade 在 `impl/`、店铺上下文 support、跨包 `IGoodsFacade` |

---

*文档版本：v1.2 · 关联《店铺商品分类功能需求.md》v1.1 · 技术栈 RuoYi v3.9.2-springboot2 + MySQL 5.7.39。*
