# 商品分类 — 技术方案（C 端）

> **依据：** 《商品分类功能需求.md》v1.0.1  
> **关联：** 平台《商品分类技术方案.md》v1.2、《商品管理技术方案.md》v1.3、《店铺管理技术方案.md》v1.2.6、《商城首页技术方案.md》v1.1、《关联需求分析.md》v1.6  
> **范围：** C 端 **`/api/category/**`** 只读（全部分类树、一级下二级 Tab、二级下商品分页列表）；**无** 平台/店铺分类 CRUD、**无** 搜索执行、**无** 新建业务主表。  
> **原则：** **无新建表**；复用 `biz_goods_category`、`biz_goods`、`biz_shop`；全部 **`@Anonymous`**；搜索栏 **无接口**（GC-S1）。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 响应 | MyBatis；分类树 `AjaxResult`；商品列表 `TableDataInfo`（分页） |
| 鉴权 | C 端 **`@Anonymous`**（访客/会员均可读） |
| 分页 | RuoYi **`PageHelper`** + `startPage()`（`/goods`） |
| 缓存 | **本期不做** Redis；读库即时生效 |

### 1.1 模块落位

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/home/
├── controller/CategoryAppController.java     # /api/category/**
├── service/ICategoryAppService.java
├── service/impl/CategoryAppServiceImpl.java
├── mapper/CategoryAppMapper.java             # 二级 Tab、分类商品 SQL
├── dto/CategoryGoodsQuery.java
├── constant/CategoryAppConstants.java        # sortBy 枚举值
└── vo/HomeHotGoodsVO.java                    # 商品卡片（与首页热销共用）

已有（category 模块 · Facade）：
└── com.ruoyi.web.modules.category.facade.ICategoryFacade
        listVisibleByShopId(null)               # 平台可见分类树（A 页 / tree）
        isCategoryVisible(categoryId)         # 二级校验（/goods）

已有（home 模块 · 首页一级导航，勿混用）：
└── HomeAppController GET /api/home/categories
        仅一级扁平列表，供首页横向导航；**不含** children

resources/mapper/home/
└── CategoryAppMapper.xml
```

> **说明：** C 端分类与 C 端首页 **同属 `home` 包**、**不同基路径**（`/api/category` vs `/api/home`）。平台分类 CRUD 仍在 `/agri/category`（`PlatformCategoryController`）。

### 1.2 协作链

```text
biz_goods_category（shop_id IS NULL）
    ├── GET /api/category/tree              → A 页 · 一级+二级树
    ├── GET /api/category/{level1Id}/level2-tabs → B 页 · 二级 Tab
    └── （校验）isCategoryVisible

biz_goods + biz_shop
    └── GET /api/category/goods?categoryId=&sortBy=&pageNum=&pageSize=
            → B 内嵌列表 / C 独立列表（共用）

【页面与接口】
    首页 HM7 ──一级──► B：level2-tabs + goods（默认首 Tab）
    首页「更多」──► A：tree ──点二级──► C：goods（同 goods 接口）
    搜索栏：纯前端 GC-S1（无接口）
```

| 关联模块 | 协作 |
|----------|------|
| **平台 · 商品分类** | 维护 `biz_goods_category`；C 端只读 `show_flag` / 排序 |
| **商品分类 Facade** | `listVisibleByShopId(null)` 过滤空壳一级、父隐子隐（GC6） |
| **商城首页** | `/api/home/categories` 一级快捷导航；点击进入本模块 B 页 |
| **商品 / 审核** | 列表 `goods_status=2`（出售中） |
| **店铺管理** | JOIN `shop_name` 实时；**不过滤** 停业/库存（GC13） |
| **会员管理** | **不校验** Token |
| **商品详情** | 列表点击跳转；可购四条件在详情/Facade 校验（GC12） |

### 1.3 跨模块 Facade

| 接口 | 本模块用法 |
|------|------------|
| **`ICategoryFacade.listVisibleByShopId(null)`** | `/tree` 平台全量可见树（含 children） |
| **`ICategoryFacade.isCategoryVisible`** | `/goods` 校验二级 `categoryId` |
| **`IPlatformCategoryService`** | **不直接** 暴露 C 端 HTTP |
| **`IGoodsPurchaseFacade` / 详情** | 本模块 **不调用**；下单层校验 |

---

## 2. 数据库设计

### 2.1 原则

**无新建表。** 只读下列已有表；DDL 见 `sql/`。

| 表 | 本模块用途 | 权威 DDL |
|----|------------|----------|
| `biz_goods_category` | 平台一/二级导航 | `sql/biz_goods_category.sql` |
| `biz_goods` | 二级下商品列表 | `sql/biz_goods.sql` |
| `biz_shop` | 商品卡片店铺名 | `sql/biz_shop.sql` |

### 2.2 本模块读字段与条件

**`biz_goods_category`** — 平台分类（`shop_id IS NULL`）

| 场景 | 条件 | 输出字段 |
|------|------|----------|
| **tree**（Facade 内存组装） | 一级 `show=1` 且下有可见二级；二级 `isCategoryVisible` | `categoryId, categoryName, categoryPic, sortNo, hotFlag` + `children[]` |
| **level2-tabs** | `level=2`, `parent_id=#{level1Id}`, `show=1`, `del=0`；且一级可见 | 同上（无 children） |

**`biz_goods` + `biz_shop`** — 分类商品列表

| 字段 | 条件 / 说明 |
|------|-------------|
| g.category_id | **=** 所选二级 ID |
| g.goods_status | **`2`** 出售中（GC8） |
| g.del_flag | **`0`** |
| s.del_flag | **`0`**（JOIN） |
| g.sales_count, g.sale_price, g.goods_sn | 排序键（GC9） |
| g.goods_id, g.goods_sn, g.goods_name, g.main_pic, g.sale_price | 输出 |
| s.shop_id, s.shop_name | JOIN；**实时**（GC10） |

> **浏览层：** SQL **不过滤** `shop_status`、**不过滤** `stock`（GC13）。

### 2.3 与首页 `/api/home/categories` 差异

| 项 | `/api/home/categories` | `/api/category/tree` |
|----|------------------------|----------------------|
| 层级 | 仅 **一级** 扁平 | **一级 + 二级** 树 |
| 空壳一级 | SQL 仅 `show=1`（首页导航宜与 GC6 对齐：无可见二级的不展示） | Facade **排除** 无可见二级的一级 |
| 用途 | 首页横向导航 +「更多」 | A 全部分类页 |

### 2.4 建议索引（可选）

```sql
-- 可选：与首页热销共用
-- idx_category_id on biz_goods(category_id)
-- idx_shop_status 等现有索引一般够用
```

---

## 3. C 端接口设计

**基路径：** `/api/category`  
**鉴权：** 全部 **`@Anonymous`**

### 3.1 接口一览

| 方法 | 路径 | 页面 | 说明 |
|------|------|------|------|
| GET | `/tree` | **A** 全部分类 | 平台可见分类树 |
| GET | `/{level1Id}/level2-tabs` | **B** Tab 区 | 指定一级下二级列表 |
| GET | `/goods` | **B/C** 商品列表 | 二级下出售中商品 **分页** |

**本期不提供：** 搜索 API、聚合 `/api/category/index`、店铺分类读接口。

### 3.2 可见分类树 `GET /api/category/tree`

| 项 | 说明 |
|----|------|
| Query | **无** |
| 实现 | `ICategoryFacade.listVisibleByShopId(null)` |

**`data`：** `CategoryVisibleVO[]`（`category` 模块已有 VO）

| 字段 | 说明 |
|------|------|
| categoryId | 一级 ID |
| categoryName | 名称 |
| categoryPic | 图片 |
| sortNo | 排序 |
| hotFlag | 是否热门 |
| children | `CategoryVisibleChildVO[]`（二级） |

**children 元素：** `categoryId, categoryName, categoryPic, sortNo, hotFlag`

**前端（A 页）：** 左列 = `data`；右列 = 当前选中一级的 `children`；点 child → 路由 C 页并带 `categoryId`。

### 3.3 一级下二级 Tab `GET /api/category/{level1Id}/level2-tabs`

| 项 | 说明 |
|----|------|
| Path | `level1Id` — 平台一级 ID |
| 校验 | 一级存在、`shop_id IS NULL`、`level=1`、`show=1`；否则 `ServiceException` |

**`data`：** `CategoryVisibleChildVO[]`（按 `sort_no, create_time` 升序）

**SQL 片段：**

```sql
SELECT category_id, category_name, category_pic, hot_flag, sort_no
FROM biz_goods_category
WHERE shop_id IS NULL
  AND category_level = '2'
  AND parent_id = #{level1Id}
  AND show_flag = '1'
  AND del_flag = '0'
ORDER BY sort_no ASC, create_time ASC
```

**前端（B 页）：** 首页带入 `level1Id` → 本接口渲染 Tab → 默认首 Tab 调 `/goods`；切换 Tab（**C1**）仅改 `categoryId` 重新请求 `/goods`。

### 3.4 分类商品列表 `GET /api/category/goods`

| 项 | 说明 |
|----|------|
| Query | 见下表 |
| 分页 | `pageNum`、`pageSize`（RuoYi 默认） |
| 响应 | **`TableDataInfo`**：`{ code, msg, rows, total }` |

**Query：**

| 参数 | 必填 | 说明 |
|------|:----:|------|
| categoryId | 是 | **平台二级** 分类 ID |
| sortBy | 否 | 默认 `sales_desc`（`CategoryAppConstants.DEFAULT_SORT`） |
| pageNum | 否 | 页码 |
| pageSize | 否 | 每页条数 |

**sortBy 合法值：**

| 值 | 排序 |
|----|------|
| `sales_desc` | 销量降序 → `goods_sn` 升序 |
| `sales_asc` | 销量升序 → `goods_sn` 升序 |
| `price_desc` | 售价降序 → `goods_sn` 升序 |
| `price_asc` | 售价升序 → `goods_sn` 升序 |

**校验：**

1. `categoryId` 非空  
2. `categoryFacade.isCategoryVisible(categoryId)` 为 true，否则「分类不存在或不可见」  
3. `sortBy` 非法 →「排序参数无效」

**`rows` 元素：** `HomeHotGoodsVO`（与 `/api/home/hot-goods` 同结构）

| 字段 | 类型 | 说明 |
|------|------|------|
| goodsId | long | |
| goodsSn | string | |
| goodsName | string | |
| mainPic | string | |
| salePrice | decimal | |
| shopId | long | |
| shopName | string | 实时 |

**SQL 片段：**

```sql
SELECT g.goods_id, g.goods_sn, g.goods_name, g.main_pic, g.sale_price,
       s.shop_id, s.shop_name
FROM biz_goods g
INNER JOIN biz_shop s ON g.shop_id = s.shop_id AND s.del_flag = '0'
WHERE g.goods_status = '2'
  AND g.del_flag = '0'
  AND g.category_id = #{categoryId}
ORDER BY
  -- 动态 sortBy，同键 goods_sn ASC
```

### 3.5 搜索栏

| 项 | 说明 |
|----|------|
| 占位 | 前端写死「搜索兽药、饲料、店铺」（GC3） |
| 接口 | **无** |

---

## 4. Service 分层

```text
CategoryAppController
    → ICategoryAppService.listCategoryTree()
    → ICategoryAppService.listLevel2Tabs(level1Id)
    → ICategoryAppService.listCategoryGoods(query)   # 分页由 Controller startPage

CategoryAppServiceImpl
    → ICategoryFacade.listVisibleByShopId(null)
    → ICategoryFacade.isCategoryVisible(categoryId)
    → BizGoodsCategoryMapper.selectById(level1Id, null)   # 一级校验
    → CategoryAppMapper.selectPlatformLevel2Tabs(level1Id)
    → CategoryAppMapper.selectGoodsByCategory(categoryId, sortBy)
```

| 方法 | 说明 |
|------|------|
| `listCategoryTree()` | 透传 Facade；`shopId=null` → 平台树 |
| `listLevel2Tabs()` | 校验一级可见 → Mapper 查二级 |
| `listCategoryGoods()` | 校验二级可见 + sortBy → Mapper；**不** 校验可购四条件 |

---

## 5. 与平台后台对照

| 平台操作 | 影响的 C 端接口 |
|----------|-----------------|
| 平台分类改 show/排序/名称/图 | `/tree`、`/level2-tabs` |
| 平台分类改 show=0 | 树/Tab 即时隐藏；`/goods` 校验失败 |
| 商品上下架 | `/goods` 增删行 |
| 店铺改名 | `/goods` 的 `shopName` |
| 店铺停业 | **仍出现在** `/goods`；下单在详情拦截 |
| 会员禁用 | **不影响** 三接口 |

---

## 6. 业务规则映射

| 规则 | 实现 |
|------|------|
| GC1 | 仅 `shop_id IS NULL` / Facade 平台树 |
| GC2 | `@Anonymous` |
| GC3～GC-S2 | 无搜索 API |
| GC4 | `/tree` + 前端 A→C 路由 |
| GC5 | `/level2-tabs` + `/goods`；Tab 切换前端 C1 |
| GC5a | `/goods` + 前端面包屑（路径非接口字段） |
| GC6 | Facade `listVisibleByShopId` + `isCategoryVisible` |
| GC7 | SQL / Facade 排序 |
| GC8～GC10 | `/goods` SQL + `HomeHotGoodsVO` |
| GC9 | `sortBy` 四档 + 默认 `sales_desc` |
| GC12 | Service **不** 调可购 Facade |
| GC13 | SQL 不过滤店态/库存 |
| GC14 | 三接口 **独立**；前端模块级空态 |
| GC15 | 无写接口 |

---

## 7. 前端接口调用建议

| 页面 | 调用顺序 |
|------|----------|
| **A** 全部分类 | `GET /tree` → 点二级 → `GET /goods?categoryId=` |
| **B** 一级分类 | `GET /{level1Id}/level2-tabs` → `GET /goods?categoryId=首Tab` → 切换 Tab 仅重调 `/goods` |
| **C** 独立列表 | `GET /goods?categoryId=`（`level1Id` 由路由/状态展示面包屑） |

> **C1：** Tab 切换时前端 **列表回顶** 后再请求 `/goods`（产品层，非后端字段）。

---

## 8. 实现状态

| 项 | 状态 |
|----|------|
| `GET /api/category/tree` | **待实现** |
| `GET /api/category/{level1Id}/level2-tabs` | **待实现** |
| `GET /api/category/goods` | **待实现** |
| `CategoryAppController` / `CategoryAppServiceImpl` | **待创建** |
| `CategoryAppMapper.xml` | **待创建** |
| C 端分类前端 | **已实现**（见《商品分类前端技术方案.md》v1.0） |

---

## 9. 非本期

| 项 | 说明 |
|------|------|
| `GET /api/category/index` 聚合 | 前端分调三接口即可 |
| Redis 缓存 | — |
| `/api/search/**` | 搜索模块 |
| 店铺商品分类 C 端读 | GC1 排除 |
| 列表返回销量/库存字段 | GC10 卡片不展示 |
| 商品详情 API | 商品详情模块 |

---

## 10. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版；无新建表；`/api/category` 三读接口；关联平台分类 Facade、商城首页 v1.1 |

---

*文档版本：v1.0 · MySQL 5.7.39 · RuoYi v3.9.2-springboot2 · 关联《商品分类功能需求.md》v1.0.1、《商品分类技术方案.md》v1.2（平台）、《商城首页技术方案.md》v1.1*
