# 商品分类 — 技术方案(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*