# 搜索页 — 技术方案（C 端 · 全站）

> **依据：** 《搜索页功能需求.md》v1.0  
> **关联：** 平台《商品管理技术方案》v1.3、《店铺管理技术方案》v1.3.6、《商品分类技术方案（C 端）》v1.0、《商城首页技术方案》v1.1、《商品详情内页技术方案》v1.0、《关联需求分析.md》v1.6  
> **范围：** C 端 **`GET /api/search`** 只读（**同一关键词** 并行检索 **商品 + 店铺**）；**无** 搜索历史服务端、**无** 店铺内搜索、**无** 新建业务主表。  
> **原则：** **无新建表**；复用 `biz_goods`、`biz_shop`；历史 **纯前端本地**（SRH4）；全部 **`@Anonymous`**；浏览层 **不过滤** 店态/库存/分类可见（SRH7）。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 响应 | MyBatis；**`AjaxResult`** 聚合商品 + 店铺两路分页 |
| 鉴权 | **`@Anonymous`** |
| 分页 | **`PageHelper`**；商品/店铺 **各自** `pageNum` / `pageSize` |
| 搜索历史 | **客户端 localStorage**（首期 **无** 后端 API） |
| 缓存 | **本期不做** Redis |

### 1.1 模块落位

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/home/
├── controller/SearchAppController.java       # GET /api/search
├── service/ISearchAppService.java
├── service/impl/SearchAppServiceImpl.java
├── mapper/SearchAppMapper.java
├── dto/SearchQuery.java                      # keyword + sortBy + 双路分页
├── constant/SearchAppConstants.java
├── vo/SearchResultAppVO.java                 # goods + shops 聚合
├── vo/SearchGoodsPageVO.java                 # { total, rows }
├── vo/SearchShopPageVO.java                  # { total, rows }
├── vo/HomeHotGoodsVO.java                    # 商品行（与首页/分类共用）
└── vo/SearchShopAppVO.java                   # 店铺行

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

> **说明：** **一个搜索动作 = 一次 HTTP**；后端 **同一 keyword** 下 **同时** 查商品表与店铺表，**不是** 两个独立接口分调。

### 1.2 协作链

```text
【前端 · A 搜索输入页】
    本地维护搜索历史（trim、去重、最多 20 条）
    非空关键词 → 路由 B 页

【前端 · B 搜索结果页】
    任意 Tab（按销量 / 按价格 / 店铺）均调：
    GET /api/search?keyword=&sortBy=&goodsPageNum=&goodsPageSize=&shopPageNum=&shopPageSize=

    响应同时含 goods、shops 两路结果
    Tab 切换 → 仅切换展示块，**可复用** 已返回数据；改 sortBy / 翻页 → 再调同一接口

biz_goods + biz_shop ──► 商品结果（名称模糊、出售中）
biz_shop            ──► 店铺结果（店名模糊）

点击商品 → GET /api/goods/{goodsId}
点击店铺 → 店铺主页（待建设）
```

| 关联模块 | 协作 |
|----------|------|
| **商城首页 / 商品分类** | 顶栏 **仅跳转** A 页 |
| **C 端商品分类** | 商品 `sortBy` **同枚举** |
| **商品详情** | 商品结果带 `goodsId` 跳转 |
| **店铺主页** | 店铺结果带 `shopId` 跳转 |
| **IGoodsPurchaseFacade** | **不调用** |

### 1.3 跨模块 Facade

| 接口 | 本模块用法 |
|------|------------|
| **`ICategoryFacade.isCategoryVisible`** | **不调用**（SRH7） |
| **`IAgriShopFacade.isShopOpenForOrder`** | **不调用** |
| **`IGoodsPurchaseFacade`** | **不调用** |

---

## 2. 数据库设计

### 2.1 原则

**无新建表。** 搜索历史 **不落库**（SRH4）。

| 表 | 本模块用途 | 权威 DDL |
|----|------------|----------|
| `biz_goods` | 商品关键词检索 | `sql/biz_goods.sql` |
| `biz_shop` | 店铺关键词检索；商品 JOIN 店名 | `sql/biz_shop.sql` |

一次搜索 **两条 SQL**（同一 `keyword`，**无** 跨表 UNION）：

| 查询 | 匹配字段 | 数据范围 |
|------|----------|----------|
| 商品 | `goods_name LIKE` | 出售中 + 未删 + 店未删 |
| 店铺 | `shop_name LIKE` | 店未删 |

### 2.2 商品检索

| 字段 / 条件 | 说明 |
|-------------|------|
| `g.goods_status` | **`'2'`** 出售中（SRH6） |
| `g.del_flag` / `s.del_flag` | **`'0'`** |
| `g.goods_name` | **`LIKE %keyword%`**（首期 **仅名称**） |
| 店态 / 库存 / 分类 | **不过滤**（SRH7） |

**输出：** `goodsId, goodsSn, goodsName, mainPic, salePrice, shopId, shopName`

**排序（`sortBy`）：**

| 值 | Tab | ORDER BY |
|----|-----|----------|
| `sales_desc`（默认） | 按销量 | `sales_count DESC, goods_sn ASC` |
| `price_asc` | 按价格 · 低→高 | `sale_price ASC, goods_sn ASC` |
| `price_desc` | 按价格 · 高→低 | `sale_price DESC, goods_sn ASC` |

```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.goods_name LIKE CONCAT('%', #{keyword}, '%')
ORDER BY /* 动态 sortBy */
```

### 2.3 店铺检索

| 字段 / 条件 | 说明 |
|-------------|------|
| `del_flag` | **`'0'`** |
| `shop_name` | **`LIKE %keyword%`** |
| `shop_status` | **不过滤** |

**输出：** `shopId, shopName, shopAvatar, shopStatus`；`rating` / `fansCount` v1 **`null`**

**排序：** `create_time ASC, shop_id ASC`（SRH10）

```sql
SELECT shop_id, shop_name, shop_avatar, shop_status
FROM biz_shop
WHERE del_flag = '0'
  AND shop_name LIKE CONCAT('%', #{keyword}, '%')
ORDER BY create_time ASC, shop_id ASC
```

### 2.4 后续扩展（非首期）

| 项 | 说明 |
|----|------|
| `search_keyword` 字段 | `OR g.search_keyword LIKE ...` |
| `biz_search_history` | 会员云端历史 |
| 全文索引 | `FULLTEXT(goods_name)` / `FULLTEXT(shop_name)` |

---

## 3. C 端接口设计

**路径：** `GET /api/search`  
**鉴权：** **`@Anonymous`**  
**响应：** **`AjaxResult`** → `data` 为 **`SearchResultAppVO`**

### 3.1 统一搜索（商品 + 店铺）

| 项 | 说明 |
|----|------|
| 语义 | **一次搜索** 返回 **商品列表 + 店铺列表**；三 Tab **共用同一 keyword**（SRH §6.3） |
| 展示 | 前端按 Tab **择一展示**（按销量/按价格 → `goods`；店铺 → `shops`） |

**Query：**

| 参数 | 必填 | 默认 | 说明 |
|------|:----:|------|------|
| keyword | 是 | — | trim 后非空 |
| sortBy | 否 | `sales_desc` | 仅影响 **商品** 排序 |
| goodsPageNum | 否 | `1` | 商品分页页码 |
| goodsPageSize | 否 | 系统默认 | 商品每页条数 |
| shopPageNum | 否 | `1` | 店铺分页页码 |
| shopPageSize | 否 | 系统默认 | 店铺每页条数 |

**校验：**

1. `keyword` trim 后非空 →「请输入搜索内容」（SRH3）  
2. `sortBy` 非法 →「排序参数无效」

**响应 `data` 结构：**

```json
{
  "goods": {
    "total": 128,
    "rows": [ /* HomeHotGoodsVO */ ]
  },
  "shops": {
    "total": 6,
    "rows": [ /* SearchShopAppVO */ ]
  }
}
```

| 块 | rows 元素 | 说明 |
|----|-----------|------|
| `goods` | `HomeHotGoodsVO` | 同 `/api/category/goods` 卡片 |
| `shops` | `SearchShopAppVO` | 见下表 |

**`SearchShopAppVO`：**

| 字段 | 类型 |
|------|------|
| shopId | long |
| shopName | string |
| shopAvatar | string |
| shopStatus | string |
| rating | decimal / null |
| fansCount | integer / null |

### 3.2 Tab 与参数映射

| UI Tab | 展示块 | 触发再请求场景 |
|--------|--------|----------------|
| 按销量 | `data.goods` | 首次进 B 页；商品翻页 |
| 按价格 | `data.goods` | 切换升/降序（改 `sortBy`）；商品翻页 |
| 店铺 | `data.shops` | 首次已随搜索返回；店铺翻页（改 `shopPageNum`） |

> 切换 Tab **不修改 keyword**；若首次响应已含两路数据，**切 Tab 不必再请求**（除非需刷新）。

### 3.3 空结果

| 情形 | 响应 |
|------|------|
| 商品无匹配 | `goods.total=0`，`goods.rows=[]` |
| 店铺无匹配 | `shops.total=0`，`shops.rows=[]` |
| 均无匹配 | 两路皆空；前端展示「暂无相关」（SRH13） |

### 3.4 本期不提供

| 项 | 说明 |
|----|------|
| `/api/search/goods`、`/api/search/shops` 拆分 | **不采用** |
| 搜索历史 CRUD | 前端 localStorage |
| 店铺内搜索 | 店铺主页模块 |
| 联想 / 热搜 | 非本期 |

---

## 4. Service 分层

```text
SearchAppController.search(SearchQuery)
    → ISearchAppService.search(query)
        → 校验 keyword、sortBy
        → PageHelper(goodsPageNum, goodsPageSize)
        → SearchAppMapper.selectGoodsByKeyword(keyword, sortBy)
        → 组装 SearchGoodsPageVO
        → PageHelper(shopPageNum, shopPageSize)
        → SearchAppMapper.selectShopsByKeyword(keyword)
        → 组装 SearchShopPageVO
        → 返回 SearchResultAppVO
```

| 要点 | 说明 |
|------|------|
| 一次 Service 调用 | **两条 SQL**，同一 `keyword` |
| 分页 | 商品、店铺 **独立** PageHelper 作用域（顺序执行） |
| 店铺排序 | **固定** `create_time ASC`，**无** sortBy |

---

## 5. 与平台后台对照

| 平台/商家操作 | 影响的 C 端搜索 |
|---------------|-----------------|
| 商品上架（出售中） | **可** 出现在 `goods.rows` |
| 商品下架 | **不出现** |
| 改商品名称 | 商品命中词变化 |
| 店铺改名 | `shops.rows` 与商品行 `shopName` 变化 |
| 店铺停业 | **仍可出现** |
| 分类 hide | **不剔除** 出售中商品（SRH7） |

---

## 6. 业务规则映射

| 规则 | 实现 |
|------|------|
| SRH1 全站 | 无 `shopId` 限定 |
| SRH3 空关键词 | Service 校验拒绝 |
| SRH4 历史 | **无后端** |
| SRH6 出售中 | 商品 SQL `goods_status='2'` |
| SRH7 浏览层 | **不过滤** 店态/库存/分类 |
| SRH8～SRH9 Tab | 前端择块展示；**一次接口** 供两路数据 |
| SRH10 店铺排序 | 店铺 SQL `create_time ASC` |
| SRH11～SRH12 | 前端路由 goodsId / shopId |
| SRH14 可购 | **不校验** |

---

## 7. 前端调用建议

| 场景 | 调用 |
|------|------|
| A 页提交 / B 页首次进入 | `GET /api/search?keyword=xxx` |
| 按价格切换升/降序 | 同接口，`sortBy=price_asc` / `price_desc` |
| 商品 load more | 同接口，递增 `goodsPageNum` |
| 店铺 load more | 同接口，递增 `shopPageNum` |
| Tab 切换（已有数据） | **可不请求**，直接切 `goods` / `shops` 展示 |
| 搜索历史 | localStorage；**不调** 后端 |

---

## 8. 实现状态

| 项 | 状态 |
|----|------|
| `GET /api/search` | **已实现** |
| `SearchAppController` / `SearchAppServiceImpl` | **已创建**（`modules/home/`） |
| `SearchAppMapper.xml` | **已创建** |
| 单元/API 测试 | **已通过**（`SearchAppServiceImplTest`、`SearchAppControllerTest`） |
| C 端搜索前端 | **未实现** |

---

## 9. 非本期

| 项 | 说明 |
|------|------|
| 拆分 goods/shops 接口 | — |
| 搜索历史服务端 | 本地存储 |
| 店铺内搜索 | 店铺主页 |
| `search_keyword` OR 匹配 | §2.4 |

---

## 10. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版 |
| **v1.1** | **统一搜索**：`GET /api/search` 一次返回商品 + 店铺；取消 `/goods`、`/shops` 拆分 |
| **v1.2** | 对齐后端实现：`SearchApp*` 已落库；§8 实现状态更新 |

---

*文档版本：v1.2 · MySQL 5.7.39 · RuoYi v3.9.2-springboot2 · 关联《搜索页功能需求.md》v1.0*
