# 店铺设置 — 技术方案

> **依据：** 《店铺设置功能需求.md》v1.0  
> **关联：** 平台《店铺管理技术方案》v1.2.5、《商户管理技术方案》、平台《店铺设置技术方案》v1.1.1（**平台全局**）、平台《关联需求分析.md》§9；商家《员工管理技术方案》v1.2、《商家端店铺上下文接口说明》  
> **范围：** 商家端 **当前店铺** 资料查询/编辑、员工数量概览；**不** 维护平台全局策略、**不** 做员工 CRUD。  
> **原则：** **无新建表**；店资料 **同源** `biz_shop`；联系人只读读 `biz_merchant`；员工统计复用 `biz_merchant_account` ⋈ `sys_user`；配额读 **`IShopGlobalConfigFacade`**。  
> **命名区分：** 本方案为 **商家端 · 店铺设置**；平台 **`/agri/shopSetting`** 为全平台策略，见平台专册。  
> **v1.1：** 后端已实现；`SellerShopProfileVO` 扩展、`GET /employeeStats`、`BizShopMapper.selectSellerProfileById`、`BizMerchantAccountMapper.countEmployeesByShopId`；Controller `requireShopId()` 校验上下文。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` |
| 店铺上下文 | `SellerShopContextInterceptor` + 请求头 **`X-Shop-Id`**；`SellerShopContext.getShopId()` |
| 头像上传 | `CommonController` 上传 URL → `biz_shop.shop_avatar` |
| 全局配额 | `IShopGlobalConfigFacade.getMaxSubAdmin()`（平台 · 店铺设置） |
| 日志 | 资料修改 `@Log` |

### 1.1 与平台侧模块的边界

| 维度 | 平台 · 店铺设置 | 平台 · 店铺管理 | 商家端 · 店铺设置（本方案） |
|------|-----------------|-----------------|---------------------------|
| 路径 | `/agri/shopSetting` | `/agri/shop/**` | **`/agri/seller/shop/**`** |
| 数据 | `biz_shop_global_config` | `biz_shop` 全字段 | **读/写** `biz_shop` 部分字段 |
| 策略 | 免审、子管理员上限 | 开业/停业、经营账号 | **只读消费** 上限（展示用） |
| 同步 | — | 列表读 `biz_shop` | PUT 后平台 list **直接一致**（§3.2） |

### 1.2 模块落位

**包：** `com.ruoyi.web.modules.store`（店资料）；员工统计 SQL 可放 `store` 或复用 `account` Mapper。

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/store/
├── controller/
│   ├── SellerShopController.java              # /agri/seller/shop（本方案）
│   └── SellerShopContextController.java       # /agri/seller/context（上下文，已存在）
├── service/
│   ├── IShopService.java
│   └── impl/ShopServiceImpl.java              # selectSellerShopProfile / update / employeeStats
├── dto/SellerShopProfileDTO.java
├── vo/
│   ├── SellerShopProfileVO.java               # 资料展示（含商户联系人只读字段）
│   ├── SellerShopEmployeeStatsVO.java       # 员工概览
│   └── ShopEmployeeCountRow.java            # 员工统计 SQL 映射
├── mapper/
│   ├── BizShopMapper.java                   # selectSellerProfileById
│   └── BizMerchantAccountMapper.java        # countEmployeesByShopId / countEmployeesByMerchantId
├── support/SellerShopContextSupport.java      # 商户/店铺归属校验
└── constant/ShopConstants.java

关联包：
├── modules/merchant/mapper/BizMerchantMapper.java   # contact_name / contact_phone
├── modules/store/facade/IShopGlobalConfigFacade.java
└── modules/category/support/
    ├── SellerShopContext.java
    └── SellerShopContextInterceptor.java

sql/
├── biz_shop.sql
├── biz_merchant.sql
├── biz_merchant_account.sql（见 biz_shop.sql）
├── biz_shop_global_config.sql
└── agri_seller_shop_setting_menu.sql            # 菜单（待配置）
```

**测试（已实现）：**

```text
src/test/java/com/ruoyi/web/modules/store/
├── service/ShopServiceImplTest.java           # 资料/员工统计单测
└── controller/SellerShopControllerTest.java   # MockMvc 接口测
```

**前端（建议）：**

| 项 | 值 |
|----|-----|
| 页面 | `ruoyi-ui/src/views/agri/seller/shop/index.vue` |
| API | `ruoyi-ui/src/api/agri/seller/shop.js` |

### 1.3 协作链

```text
【登录】GET /agri/seller/context → 前端写入 X-Shop-Id
            ↓ SellerShopContextInterceptor.assertShopAccessible
【本模块】GET /agri/seller/shop              → biz_shop + biz_merchant（只读联系人）
           PUT /agri/seller/shop              → UPDATE biz_shop（名称/头像/简介/电话）
           GET /agri/seller/shop/employeeStats → 当前店员工数 + 商户配额
            ↓
【平台】GET /agri/shop/list                  → 读同一 biz_shop，无需同步任务
【员工管理】/agri/seller/employee/**         → 明细 CRUD；配额逻辑同源 Facade
【商品 submit】                               → 读 IShopGlobalConfigFacade（非本模块配置）
```

| 关联模块 | 协作 |
|----------|------|
| **平台 · 店铺管理** | 同源 `biz_shop`；平台改状态/账号，商家 **不可** 通过本接口改 |
| **平台 · 商户管理** | `contact_name`、`contact_phone` **只读** join |
| **平台 · 店铺设置** | `max_sub_admin` 经 Facade 只读展示 |
| **员工管理** | 统计口径一致；跳转 `/agri/seller/employee` |
| **店铺上下文** | 与分类/商品/员工共用 `X-Shop-Id` |

### 1.4 跨模块 Facade

| 接口 | 提供方 | 本模块用法 |
|------|--------|------------|
| `IShopGlobalConfigFacade` | 平台 · 店铺设置 | `getMaxSubAdmin()` → 员工概览 `maxCount` |
| `IShopFacade` | 店铺管理 | C 端/删店等 **非** 本模块直接调用 |

---

## 2. 数据库设计

**无新建表。** 沿用现有 DDL：

| 脚本 | 用途 |
|------|------|
| [`sql/biz_shop.sql`](../../../sql/biz_shop.sql) | 店铺资料 **读写** |
| [`sql/biz_merchant.sql`](../../../sql/biz_merchant.sql) | 联系人 **只读** |
| [`sql/biz_shop.sql`](../../../sql/biz_shop.sql)（含 `biz_merchant_account`） | 员工绑定与计数 |
| [`sql/biz_shop_global_config.sql`](../../../sql/biz_shop_global_config.sql) | 全局子管理员上限 |

### 2.1 本模块读写字段

**`biz_shop`（按 `shop_id = X-Shop-Id`）**

| 字段 | 本模块 | 说明 |
|------|:------:|------|
| shop_id | 读 | 当前店铺 |
| merchant_id | 读 | 归属校验、join 商户 |
| shop_name | **读写** | 全平台未删除店名唯一 |
| shop_avatar | **读写** | 必填 |
| shop_desc | **读写** | 选填，≤1000 |
| shop_phone | **读写** | 商家电话；界面「客服电话」；选填 |
| shop_status | 读 | 开业/停业；**不可** 经本模块修改 |
| create_time | 读 | 展示「创建时间」 |
| del_flag | 读 | `=0` 有效 |

**`biz_merchant`（按 `merchant_id`，只读）**

| 字段 | 界面名 | 说明 |
|------|--------|------|
| contact_name | 负责人姓名 | 未填返回 null → 前端「—」 |
| contact_phone | 联系电话 | 只读；可按项目规则脱敏 |
| merchant_name | 所属商户名称 | 建议只读展示 |

> **边界：** `biz_merchant.service_phone`（商户客服电话）**不在** 本模块编辑；可编辑电话为 **`biz_shop.shop_phone`**。

### 2.2 员工统计（当前店铺）

**已有员工数 / 停用员工数：**

```sql
SELECT COUNT(1) AS total_count,
       SUM(CASE WHEN u.status = '1' THEN 1 ELSE 0 END) AS disabled_count
FROM biz_merchant_account a
INNER JOIN sys_user u ON a.account_id = u.user_id AND u.del_flag = '0'
WHERE a.merchant_id = #{merchantId}
  AND a.shop_id = #{shopId}
  AND a.shop_id IS NOT NULL
  AND a.del_flag = '0'
```

| 口径 | 说明 |
|------|------|
| 范围 | **当前店铺** `shop_id` |
| 含停用 | `total_count` 含 `status=0/1` |
| 不含 | 经营账号（`shop_id IS NULL`）、已删绑定 |

**商户配额（只读，与员工管理 `/quota` 一致）：**

```sql
-- usedCount（商户级）
SELECT COUNT(1) FROM biz_merchant_account
WHERE merchant_id = #{merchantId} AND shop_id IS NOT NULL AND del_flag = '0'

-- maxCount
IShopGlobalConfigFacade.getMaxSubAdmin()  -- 默认 5，范围 1～99
```

### 2.3 店资料查询（扩展 join）

```sql
SELECT s.shop_id, s.merchant_id, s.shop_name, s.shop_avatar, s.shop_desc,
       s.shop_phone, s.shop_status, s.create_time,
       m.contact_name, m.contact_phone, m.merchant_name
FROM biz_shop s
INNER JOIN biz_merchant m ON s.merchant_id = m.merchant_id AND m.del_flag = '0'
WHERE s.shop_id = #{shopId} AND s.del_flag = '0'
```

### 2.4 店资料更新

```sql
UPDATE biz_shop
SET shop_name = #{shopName},
    shop_avatar = #{shopAvatar},
    shop_desc = #{shopDesc},
    shop_phone = #{shopPhone},
    update_by = #{updateBy},
    update_time = sysdate()
WHERE shop_id = #{shopId} AND del_flag = '0'
```

**名称唯一（排除当前店）：**

```sql
SELECT COUNT(1) FROM biz_shop
WHERE shop_name = #{shopName} AND shop_id <> #{shopId} AND del_flag = '0'
```

### 2.5 Mapper 扩展建议

| Mapper | 方法 | 说明 |
|--------|------|------|
| `BizShopMapper` | `selectSellerProfileById` | §2.3 join 查询 |
| `BizMerchantAccountMapper` | `countEmployeesByShopId` | §2.2 当前店 total/disabled |
| `BizMerchantAccountMapper` | `countEmployeesByMerchantId` | 已有；配额 usedCount |

---

## 3. 接口设计

**基路径：** `/agri/seller/shop`  
**公共请求头：** `Authorization` + **`X-Shop-Id`**（须与当前登录商户归属一致）  
**拦截：** `/agri/seller/**` → `SellerShopContextInterceptor`；Controller **`requireShopId()`** 强制 `X-Shop-Id` 非空（msg：`请先选择当前店铺`）

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/` | `agri:seller:shop:query` | 当前店铺资料 |
| PUT | `/` | `agri:seller:shop:edit` | 修改店资料 |
| GET | `/employeeStats` | `agri:seller:shop:query` | 员工数量概览 + 配额 |

> **实现类：** `SellerShopController`；业务在 `ShopServiceImpl.selectSellerShopProfile` / `updateSellerShopProfile` / `selectSellerEmployeeStats`。

### 3.1 获取店铺资料 `GET /agri/seller/shop`

**成功 `data`（SellerShopProfileVO）：**

| 字段 | 类型 | 说明 |
|------|------|------|
| shopId | Long | 当前店铺 ID |
| merchantId | Long | 所属商户 |
| shopName | String | 店铺名称 |
| shopAvatar | String | 头像 URL |
| shopDesc | String | 店铺简介 |
| shopPhone | String | 商家电话（界面「客服电话」） |
| shopStatus | String | `0` 开业 / `1` 停业，只读 |
| createTime | datetime | 创建时间 |
| contactName | String | 负责人姓名（商户联系人，只读） |
| contactPhone | String | 联系电话（只读；`MemberMobileUtils.mask` 脱敏） |
| merchantName | String | 所属商户名称（建议） |

**示例：**

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "shopId": 101,
    "merchantId": 10,
    "shopName": "张三农资店",
    "shopAvatar": "/profile/upload/2026/05/avatar.png",
    "shopDesc": "主营化肥农药",
    "shopPhone": "400-800-1234",
    "shopStatus": "0",
    "createTime": "2026-01-15 10:00:00",
    "contactName": "张三",
    "contactPhone": "138****8000",
    "merchantName": "张三农资"
  }
}
```

**Service 流程：**

```text
shopId = SellerShopContext.getShopId() → 非空校验
assertShopAccessible(当前用户, shopId)
selectSellerProfileById(shopId) → 组装 VO
```

### 3.2 修改店铺资料 `PUT /agri/seller/shop`

**Body（SellerShopProfileDTO）：**

| 字段 | 必填 | 规则 |
|------|:----:|------|
| shopName | 是 | 非空，≤128；未删除店铺内唯一 |
| shopAvatar | 是 | 非空 URL |
| shopDesc | 否 | ≤1000 |
| shopPhone | 否 | 有值：`^[0-9\-+()\s]{5,20}$` |

**成功：** `code=200`，`msg=操作成功`，无业务 data。

**Service 流程：**

```text
requireShop(shopId) → validateSellerShopProfile(dto)
→ countByShopName(name, shopId) = 0
→ UPDATE biz_shop（仅四字段）
```

**与平台同步：** 平台 `GET /agri/shop/list`、`GET /agri/shop/{id}` **直接读** `biz_shop`，无消息队列或批任务。

### 3.3 员工概览 `GET /agri/seller/shop/employeeStats`

**成功 `data`（SellerShopEmployeeStatsVO）：**

| 字段 | 类型 | 说明 |
|------|------|------|
| shopId | Long | 当前店铺 |
| shopName | String | 当前店铺名称 |
| totalCount | int | **已有员工数**（含停用） |
| disabledCount | int | **停用员工数** |
| usedCount | int | 商户级已占用名额（各店员工合计） |
| maxCount | int | 平台全局上限 |

**示例：**

```json
{
  "code": 200,
  "data": {
    "shopId": 101,
    "shopName": "张三农资店",
    "totalCount": 3,
    "disabledCount": 1,
    "usedCount": 4,
    "maxCount": 5
  }
}
```

> **协作：** 与 `GET /agri/seller/employee/quota` 的 `usedCount`/`maxCount` **同源**；`totalCount`/`disabledCount` 为 **当前店** 维度，员工管理列表无此聚合时可 **仅** 在本接口提供。

### 3.4 错误与校验

| 场景 | msg 要点 |
|------|----------|
| 未携带 X-Shop-Id / shopId 无效 | 请先选择当前店铺 / 当前店铺无效或不属于该商户 |
| 店铺不存在 | 店铺不存在 |
| 店名重复 | 店铺名称已存在 |
| 头像为空 | 请上传店铺头像 |
| 电话格式 | 商家电话格式不正确 |
| 简介过长 | 店铺描述过长 |
| 无权限 | RuoYi 403 |

---

## 4. Service 要点

### 4.1 `selectSellerShopProfile(shopId)`

```text
requireShop(shopId)
→ selectSellerProfileById（biz_shop ⋈ biz_merchant）
→ 映射 SellerShopProfileVO（含 createTime、contactName、contactPhone）
```

### 4.2 `updateSellerShopProfile(shopId, dto, operator)`

```text
@Transactional
requireShop(shopId)
validateSellerShopProfile（名称/头像/电话/简介）
countByShopName → 0
UPDATE biz_shop 四字段
```

### 4.3 `selectSellerEmployeeStats(shopId, merchantId)`

```text
requireShop(shopId) + assert merchantId 归属
countEmployeesByShopId → totalCount, disabledCount
usedCount = countEmployeesByMerchantId(merchantId)
maxCount = shopGlobalConfigFacade.getMaxSubAdmin()
→ SellerShopEmployeeStatsVO
```

### 4.4 店铺上下文校验（复用）

```text
SellerShopContextSupport.requireSellerAccount(userId)
SellerShopContextSupport.assertShopAccessible(account, shopId)
  ├── 员工：shop_id 须等于 X-Shop-Id
  └── 经营账号：shop 须属于 merchant_id
```

---

## 5. 业务规则映射（ST）

| 编号 | 实现 |
|------|------|
| ST1 | 所有接口 `shopId = SellerShopContext.getShopId()` |
| ST2 | PUT 仅更新 `shop_name/avatar/desc/phone` |
| ST3 | VO 中 `contactName/contactPhone` 来自 `biz_merchant`，PUT **忽略** |
| ST4 | `countByShopName` 排除当前店 |
| ST5 | 单表 UPDATE，平台 list 直接一致 |
| ST6 | §2.2 SQL：`totalCount` 含停用，`disabledCount` 子集 |
| ST7 | `shop_id IS NOT NULL` 过滤经营账号 |
| ST8 | 无员工 CRUD 接口 |
| ST9 | `maxCount` 只读 Facade |
| ST10 | 前端切换店铺后重新请求 GET `/` + GET `/employeeStats` |
| ST11 | 不调用会员模块 |

---

## 6. 菜单与权限

| 菜单 | 路由组件 | 权限字符 |
|------|----------|----------|
| 店铺设置 | `agri/seller/shop/index` | `agri:seller:shop:query` |

| 按钮权限 | 说明 |
|----------|------|
| `agri:seller:shop:query` | 查看资料、员工概览 |
| `agri:seller:shop:edit` | 保存店资料 |

**菜单 SQL 示例（`sql/agri_seller_shop_setting_menu.sql`）：**

```sql
-- 父菜单「店铺设置」+ 按钮 query/edit；挂载商家端根菜单下
-- perms: agri:seller:shop:query / agri:seller:shop:edit
-- component: agri/seller/shop/index
```

> 平台 **`agri:shop:setting`** 与商家 **`agri:seller:shop:*`** **不可混用**。

---

## 7. 测试要点

| 类型 | 类 / 用例 |
|------|-----------|
| 单测 | `ShopServiceImplTest`：`selectSellerShopProfile_joinMerchantAndMaskContactPhone`、`updateSellerShopProfile_*`、`selectSellerEmployeeStats_*` |
| 接口 | `SellerShopControllerTest`：`getProfile_*`、`updateProfile_*`、`employeeStats_*` |
| 协作 | PUT 成功后平台 `GET /agri/shop/list` 字段一致（SHP-API-034） |
| 协作 | `usedCount/maxCount` 与 `GET /agri/seller/employee/quota` 一致 |
| 详册 | 《店铺设置测试用例.md》SS-UT / SS-API / SS-UI |

---

## 8. 实现状态（v1.1）

| 项 | 状态 |
|----|------|
| `GET/PUT /agri/seller/shop` | **已实现** |
| `GET /agri/seller/shop/employeeStats` | **已实现** |
| `SellerShopProfileVO` 扩展字段 | **已实现** |
| `selectSellerProfileById`（join 商户） | **已实现** |
| `countEmployeesByShopId` | **已实现** |
| 联系人电话脱敏 | **已实现**（`MemberMobileUtils.mask`） |
| 单元 / MockMvc 测试 | **已实现**（22 用例，`ShopServiceImplTest` + `SellerShopControllerTest`） |
| 菜单 SQL `agri_seller_shop_setting_menu.sql` | **待配置** |
| 前端页 `agri/seller/shop/index.vue` | **未实现**（本期不做） |

---

## 9. 修订记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版；对齐功能需求 v1.0 |
| **v1.1** | 同步后端实现：扩展 VO、employeeStats 接口、Mapper SQL、Controller 上下文校验、单测/接口测 |

---

*文档版本：v1.1 · 关联《店铺设置功能需求.md》v1.0 · 平台侧见《店铺管理技术方案.md》《店铺设置技术方案.md》v1.1.1*
