# 店铺主页 — 技术方案（C 端）

> **依据：** 《店铺主页功能需求.md》v1.0  
> **关联：** 平台《店铺管理技术方案》v1.3.6、《商品管理技术方案》v1.3、《关联需求分析.md》v1.6；商家《店铺商品分类技术方案》v1.1；C 端《搜索页技术方案》v1.0、《商品详情内页技术方案》v1.0、《商品分类技术方案（C 端）》v1.0  
> **范围：** C 端 **`/api/shop/**`** 只读（店档案、店铺分类树、商品列表、店内搜索）+ **关注/取消关注**（须会员 Token）；**不维护** 店铺/分类/商品数据。  
> **原则：** 复用 `biz_shop`、`biz_goods_category`（`shop_id` 非空）、`biz_goods`；**新建** 关注表 + 店铺统计表；浏览层 **不过滤** 停业/库存/平台分类可见（SH8、对齐 SRH7/GC13）。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 响应 | MyBatis；读接口 `AjaxResult` / `TableDataInfo`（分页） |
| 鉴权 | 读接口 **`@Anonymous`**；关注 **`POST/DELETE`** 须 **会员 Token**（`MemberContext`） |
| 分页 | **`PageHelper`** + `startPage()`（商品列表、店内搜索） |
| 缓存 | **本期不做** Redis |

### 1.1 模块落位

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/home/
├── controller/ShopAppController.java          # /api/shop/**
├── service/IShopAppService.java
├── service/impl/ShopAppServiceImpl.java
├── mapper/ShopAppMapper.java
├── dto/ShopGoodsQuery.java                    # shopCategoryId / keyword / 分页
├── constant/ShopAppConstants.java
└── vo/ShopProfileAppVO.java                   # 店介绍 + 统计 + 关注态

account/（关注写操作协作）
├── domain/BizMemberShopFollow.java
├── mapper/BizMemberShopFollowMapper.java
└── support/MemberContext.java                 # 已有 · 解析当前会员

category/（已有 · Facade）
└── ICategoryFacade.listVisibleByShopId(shopId)  # 店铺可见分类树

store/（已有 · Facade）
└── IShopFacade.getShopStatus / isShopOpenForOrder  # 详情/下单用；本模块浏览 **不调用** isShopOpenForOrder

resources/mapper/home/ShopAppMapper.xml
sql/biz_member_shop_follow.sql                 # 新建
sql/biz_shop_stats.sql                           # 新建
```

**复用 VO：**

| VO | 用途 |
|----|------|
| `CategoryVisibleVO` / `CategoryVisibleChildVO` | 店铺分类树、二级 Tab |
| `HomeHotGoodsVO` | 商品卡片（与首页/分类/搜索 **一致**） |

### 1.2 协作链

```text
【上游入口】
    搜索页 SRH12 ──shopId──► 本模块
    商品详情 GD13 ──shopId──► 本模块

【店铺主页 A】并行请求：
    GET /api/shop/{shopId}                         → 店介绍 + 评分/粉丝 + followed
    GET /api/shop/{shopId}/categories              → 一级+二级树（含「全部」由前端虚拟 Tab）
    GET /api/shop/{shopId}/goods                   → 默认全店出售中 · 销量降序

【切换分类】
    GET /api/shop/{shopId}/categories/{level1Id}/level2-tabs
    GET /api/shop/{shopId}/goods?shopCategoryId=   → 按本店二级筛选

【店内搜索 B/C】
    GET /api/shop/{shopId}/goods?keyword=          → 本店商品名模糊 · 销量降序

【关注】（会员 Token）
    POST   /api/shop/{shopId}/follow
    DELETE /api/shop/{shopId}/follow

biz_shop ──────────────────► 店档案（名称/头像/描述/状态/电话）
biz_goods_category(shop_id) ► 店内导航（show_flag=1）
biz_goods + shop_category_id ► 列表/搜索（goods_status=2）
biz_member_shop_follow ────► 关注关系
biz_shop_stats ────────────► rating / fans_count（与详情进店区 **同源**）

点击商品 → GET /api/goods/{goodsId}（可购四条件在详情/Facade）
```

| 关联模块 | 协作 |
|----------|------|
| **平台 · 店铺管理** | 维护 `biz_shop`；`del_flag=2` → C 端 **404**；停业 **仍可浏览** |
| **商家 · 店铺商品分类** | 维护 `biz_goods_category`（`shop_id=本店`）；C 端只读 `show_flag` |
| **平台 · 商品管理/审核** | 列表/搜索 **仅** `goods_status=2` |
| **C 端 · 搜索页** | 店铺 Tab 带 `shopId` 跳转；**无** 店内搜索 |
| **C 端 · 商品详情** | `shop.rating/fansCount` 读 **`biz_shop_stats`**；口径与本模块 **一致** |
| **C 端 · 商品分类** | **平台** `category_id` 导航 **不用**；可购第 3 条在详情校验 |
| **会员管理** | 关注须 **启用** 会员；禁用会员 **不可** 关注 |

### 1.3 跨模块 Facade

| 接口 | 本模块用法 |
|------|------------|
| **`ICategoryFacade.listVisibleByShopId(shopId)`** | `/categories` 店铺可见树 |
| **`ICategoryFacade.isCategoryVisible`** | 校验 `shopCategoryId` 属于本店且可见（**二级**） |
| **`IShopFacade.isShopOpenForOrder`** | **不调用**（浏览层） |
| **`IGoodsPurchaseFacade`** | **不调用**；详情/结算校验 |
| **`IMemberFacade` / 会员状态** | 关注写操作校验会员 **启用** |

---

## 2. 数据库设计

### 2.1 复用表（无 DDL 变更）

| 表 | 本模块用途 | 权威 DDL |
|----|------------|----------|
| `biz_shop` | 店档案 | `sql/biz_shop.sql` |
| `biz_goods_category` | 店铺商品分类（`shop_id = #{shopId}`） | `sql/biz_goods_category.sql` |
| `biz_goods` | 列表/搜索（`shop_id`、`shop_category_id`） | `sql/biz_goods.sql` |

**读条件（列表 / 店内搜索）：**

| 字段 / 条件 | 说明 |
|-------------|------|
| `g.shop_id` | **=** 路径 `shopId` |
| `g.goods_status` | **`'2'`** 出售中（SH2） |
| `g.del_flag` / `s.del_flag` | **`'0'`** |
| `shop_category_id` | 可选；**二级** ID；不传 = 全店 |
| `goods_name` | 店内搜索：`LIKE %keyword%`（trim 后非空） |
| 店态 / 库存 / 平台分类 | **不过滤**（浏览层） |

**排序：** 固定 **`sales_count DESC, goods_sn ASC`**（SH4；**无** 价格 sortBy）。

**分类导航：**

| 场景 | 条件 |
|------|------|
| 树 `/categories` | `ICategoryFacade.listVisibleByShopId(shopId)` — 一级 `show=1` 且含 **至少一个可见二级** |
| 二级 Tab | `shop_id=#{shopId}`, `level=2`, `parent_id=#{level1Id}`, `show=1`, `del=0` |

> **说明：** 某一级 **无可见二级** 时 Facade **不返回该一级**；若产品需「按一级聚合商品」，前端可 **单独** 用 `level1Id` 扩展查询（本期 SQL 以 **`shop_category_id` 二级** 为主路径，与商家发品字段一致）。

### 2.2 新建 · 会员关注店铺 `biz_member_shop_follow`

| 字段 | 类型 | 说明 |
|------|------|------|
| follow_id | bigint PK | 自增 |
| member_id | bigint | 会员 ID（`biz_member.member_id`） |
| shop_id | bigint | 店铺 ID |
| create_time | datetime | 关注时间 |

| 约束 | 说明 |
|------|------|
| `uk_member_shop (member_id, shop_id)` | 同一会员对同一店铺 **至多一条**（SH-F4） |
| `idx_shop_id (shop_id)` | 统计粉丝、删店后清理 |

**写规则：**

| 操作 | 行为 |
|------|------|
| 关注 | 店 `del_flag=0`；会员 **启用**；INSERT；`biz_shop_stats.fans_count + 1`（无统计行则 **INSERT** 行，`fans_count=1`） |
| 取消关注 | DELETE 关系；`fans_count - 1`（**不低于 0**） |
| 重复关注 | 幂等成功或业务提示「已关注」 |
| 店已删 | **拒绝** 关注（SH-F5） |

**权威 DDL：** `sql/biz_member_shop_follow.sql`

```sql
CREATE TABLE IF NOT EXISTS `biz_member_shop_follow` (
  `follow_id` bigint(20) NOT NULL AUTO_INCREMENT,
  `member_id` bigint(20) NOT NULL COMMENT '会员ID',
  `shop_id` bigint(20) NOT NULL COMMENT '店铺ID',
  `create_time` datetime DEFAULT NULL COMMENT '关注时间',
  PRIMARY KEY (`follow_id`),
  UNIQUE KEY `uk_member_shop` (`member_id`, `shop_id`),
  KEY `idx_shop_id` (`shop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员关注店铺';
```

### 2.3 新建 · 店铺统计 `biz_shop_stats`

| 字段 | 类型 | 说明 |
|------|------|------|
| shop_id | bigint PK | 与 `biz_shop.shop_id` 1:1 |
| rating | decimal(3,2) | 综合评分；**评价模块未建设时为 NULL**（SH9） |
| fans_count | int | 粉丝数；关注/取关 **同事务** 维护 |
| update_time | datetime | 最后更新 |

**rating 来源（后续）：** 评价模块 `biz_goods_review` 按 `shop_id` 聚合后回写；**未建设前** C 端输出 **null**，前端可隐藏/占位。

**fans_count：** 以本表为准；与 `COUNT(biz_member_shop_follow)` 定期对账为 **运维可选**，非首期。

**权威 DDL：** `sql/biz_shop_stats.sql`

```sql
CREATE TABLE IF NOT EXISTS `biz_shop_stats` (
  `shop_id` bigint(20) NOT NULL COMMENT '店铺ID',
  `rating` decimal(3,2) DEFAULT NULL COMMENT '综合评分',
  `fans_count` int(11) NOT NULL DEFAULT '0' COMMENT '粉丝数',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`shop_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='店铺统计';
```

### 2.4 列表 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.shop_id = #{shopId}
  AND g.goods_status = '2'
  AND g.del_flag = '0'
  /* 可选 */ AND g.shop_category_id = #{shopCategoryId}
ORDER BY g.sales_count DESC, g.goods_sn ASC
```

**店内搜索（同结构 + 名称条件）：**

```sql
  AND g.goods_name LIKE CONCAT('%', #{keyword}, '%')
```

---

## 3. C 端接口设计

**基路径：** `/api/shop`  
**读接口：** `@Anonymous`  
**写接口（关注）：** 须 Header `Authorization: Bearer {token}`

### 3.1 接口一览

| 方法 | 路径 | 页面 | 说明 |
|------|------|------|------|
| GET | `/{shopId}` | **A** 介绍区 | 店档案 + 统计 + 是否已关注 |
| GET | `/{shopId}/categories` | **A** 分类导航 | 可见店铺分类树 |
| GET | `/{shopId}/categories/{level1Id}/level2-tabs` | **A** 二级 Tab | 指定一级下可见二级 |
| GET | `/{shopId}/goods` | **A/C** 商品列表 | 分类浏览 / 店内搜索（Query 区分） |
| POST | `/{shopId}/follow` | **A** | 关注（会员） |
| DELETE | `/{shopId}/follow` | **A** | 取消关注（会员） |

### 3.2 店铺介绍 `GET /api/shop/{shopId}`

| 项 | 说明 |
|----|------|
| Path | `shopId` |
| 校验 | 店铺存在且 `del_flag=0`；否则 **404**，msg「店铺不存在或已关闭」（SH1） |
| Token | **可选**；有 Token 时计算 `followed` |

**`data`：** `ShopProfileAppVO`

| 字段 | 类型 | 说明 |
|------|------|------|
| shopId | long | |
| shopName | string | |
| shopAvatar | string | |
| shopDesc | string | 可 null |
| shopStatus | string | `0` 开业 / `1` 停业 |
| shopPhone | string | 可 null |
| rating | decimal | `biz_shop_stats.rating`；无行或未建设 → **null** |
| fansCount | int | 无统计行 → **0** |
| followed | boolean | 未登录 → **false**；已登录 → 是否已关注 |

### 3.3 店铺分类树 `GET /api/shop/{shopId}/categories`

| 项 | 说明 |
|----|------|
| 校验 | 店铺未删 |
| 实现 | `ICategoryFacade.listVisibleByShopId(shopId)` |

**`data`：** `CategoryVisibleVO[]`（含 `children[]`，字段同 C 端商品分类 `/tree`）

**前端：** 虚拟 Tab **「全部商品」** 不调本接口；选中一级后再调 `level2-tabs`。

### 3.4 二级 Tab `GET /api/shop/{shopId}/categories/{level1Id}/level2-tabs`

| 项 | 说明 |
|----|------|
| 校验 | 一级属于 **该 shopId**、`level=1`、`show=1` |
| 排序 | `sort_no ASC, create_time ASC`；`hot_flag=1` 可前端靠前 |

**`data`：** `CategoryVisibleChildVO[]`

### 3.5 商品列表 / 店内搜索 `GET /api/shop/{shopId}/goods`

| 项 | 说明 |
|----|------|
| 响应 | **`TableDataInfo`**：`{ code, msg, rows, total }` |
| rows | `HomeHotGoodsVO[]` |

**Query：**

| 参数 | 必填 | 说明 |
|------|:----:|------|
| shopCategoryId | 否 | **本店二级** 分类 ID；不传 = 全店出售中 |
| keyword | 否 | 店内搜索关键词；**trim 后非空** 才生效（SH-S2） |
| pageNum | 否 | 页码 |
| pageSize | 否 | 每页条数 |

**参数互斥：**

| 场景 | 参数 |
|------|------|
| 主页 · 全部 | 无 |
| 主页 · 某二级 | `shopCategoryId` |
| 店内搜索 | `keyword`（**忽略** `shopCategoryId`） |

**校验：**

- `shopCategoryId` 有值 → `isCategoryVisible` 且分类 `shop_id = shopId`
- `keyword` 仅空白 → `ServiceException`「请输入搜索关键词」
- 店铺已删 → 404

**排序：** 固定销量降序（**无** `sortBy` 参数）。

### 3.6 关注 `POST /api/shop/{shopId}/follow`

| 项 | 说明 |
|----|------|
| 鉴权 | **必须** 会员 Token |
| 校验 | 店未删；会员 **启用**（SH-F2） |
| 事务 | INSERT follow + UPSERT/UPDATE `biz_shop_stats.fans_count` |

**响应：** `AjaxResult.success()`；前端刷新 `followed=true`、`fansCount+1`。

### 3.7 取消关注 `DELETE /api/shop/{shopId}/follow`

| 项 | 说明 |
|----|------|
| 鉴权 | **必须** 会员 Token |
| 事务 | DELETE follow + `fans_count-1` |

无关注记录 → 幂等成功。

### 3.8 空结果与错误

| 情形 | 行为 |
|------|------|
| 分类/全店无出售中商品 | `total=0`，`rows=[]`；前端「暂无商品」 |
| 店内搜索无匹配 | 同上；前端「暂无相关」 |
| 店不存在/已删 | HTTP 200 + 业务 **404** 或 `code=500` + msg（与现有 C 端 **一致**） |
| 关注失败 | 不改变前端按钮态（SH-F 失败提示） |

### 3.9 本期不提供

| 项 | 说明 |
|----|------|
| 全站搜索 | **搜索页** `/api/search` |
| 平台分类导航 | **商品分类** `/api/category` |
| 加购/下单 | **商品详情** + `IGoodsPurchaseFacade` |
| 关注列表页 | **我的服务** 另册 |
| 搜索历史服务端 | 店内 **可不落库**（SH-S 与搜索页 SRH4 对齐） |

---

## 4. Service 分层

```text
ShopAppController
    → IShopAppService

getProfile(shopId)
    → 查 biz_shop；LEFT JOIN biz_shop_stats
    → 可选 Token → 查 biz_member_shop_follow → followed

listCategories(shopId) / listLevel2Tabs(shopId, level1Id)
    → ICategoryFacade / ShopAppMapper

pageGoods(shopId, ShopGoodsQuery)
    → 校验 shop / category / keyword
    → PageHelper → ShopAppMapper.selectGoods

follow(shopId, memberId) / unfollow(shopId, memberId)
    → 校验会员启用 + 店可访问
    → 事务：follow 表 + biz_shop_stats.fans_count
```

---

## 5. 与平台/商家后台对照

| 平台/商家操作 | 影响的 C 端店铺主页 |
|---------------|---------------------|
| 改店名/头像/描述/电话 | 刷新 `/api/shop/{shopId}` 即更新 |
| 开业→停业 | **仍可** 进主页、看商品；下单在 **详情** 拦截 |
| 逻辑删店 | **404**；不可关注 |
| 商家改店铺分类 show/排序 | 刷新 `/categories` 更新导航 |
| 商品上架（出售中） | 出现在列表/店内搜索 |
| 商品下架/待审核 | **不出现** |
| 改商品名称 | 店内搜索命中变化 |
| 商户冻结/注销 | **不自动** 隐藏主页（需求 §8.2）；运营策略可选扩展 |

---

## 6. 兄弟模块改造（口径统一）

| 模块 | 改造 |
|------|------|
| **商品详情** `GoodsAppServiceImpl` | `shop.rating/fansCount` ← LEFT JOIN **`biz_shop_stats`**（替代 v1 恒 null） |
| **搜索页** `SearchAppMapper` | 店铺卡片 `rating/fansCount` ← LEFT JOIN **`biz_shop_stats`** |
| **搜索页文档** | 店铺 Tab 跳转 **`/api/shop/{shopId}`**（替换「待建设」） |

---

## 7. 业务规则映射

| 规则 | 实现 |
|------|------|
| SH1 未删店可访问 | `biz_shop.del_flag=0` |
| SH2 仅出售中 | `goods_status='2'` |
| SH3 店铺分类 + show=1 | Facade + Tab SQL |
| SH4 销量降序 | ORDER BY 固定 |
| SH5 关注须登录 | POST/DELETE 要 Token |
| SH6 店内搜本店名、空不搜 | `shop_id` + keyword 校验 |
| SH7 不重复全站搜索 | 独立 Query，无 `/api/search` |
| SH8 停业可浏览 | SQL 不过滤 `shop_status` |
| SH9 评分/粉丝与详情一致 | 同源 **`biz_shop_stats`** |
| SH10 进详情 | 前端 `goodsId` → `/api/goods/{id}` |
| SH-F4 唯一关注 | `uk_member_shop` |
| 可购四条件 | **本模块不校验**；详情 `can-purchase` |

---

## 8. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：店介绍、店铺分类、商品列表、店内搜索、关注；新建 `biz_member_shop_follow`、`biz_shop_stats`；关联平台店铺/商家店铺分类/商品与 C 端搜索/详情 |

---

*文档版本：v1.0 · 关联《店铺主页功能需求.md》v1.0、《关联需求分析.md》v1.6、《店铺管理技术方案》v1.3.6、《店铺商品分类技术方案》v1.1 · 技术栈 RuoYi v3.9.2-springboot2 + MySQL 5.7.39*
