# 角色管理 — 技术方案

> **依据：** 《角色管理功能需求.md》v1.1  
> **关联：** 《员工管理技术方案》v1.1、《管理员列表技术方案》v1.1、《关联需求分析.md》§9.2、店铺上下文 `/agri/seller/context`  
> **范围：** 商家端 `/agri/seller/role` **当前店铺** 员工角色 CRUD、商家端菜单权限树；为 **员工管理** 提供 `roleOptions` 数据源。  
> **原则：** **无新建表**；复用 `sys_role`、`sys_role_menu`、`biz_shop_role`；`role_key` 与平台 `admin/merchant` **隔离**；依赖 **`X-Shop-Id`**；**仅商户经营账号** 可维护。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi v3.9.2（springboot2 分支） |
| 数据库 | MySQL 5.7.39 |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 角色 / 菜单 | `ISysRoleService` 写 `sys_role`、`sys_role_menu`；店铺归属写 `biz_shop_role` |
| 店铺上下文 | `MerchantEmployeeSupport.requireCurrentShopId()` + `X-Shop-Id` |
| 日志 | 增删改 `@Log` |

### 1.1 模块落位

```text
com.ruoyi.web.modules.account/
├── controller/MerchantRoleController.java         # /agri/seller/role
├── service/IMerchantRoleService.java
├── service/impl/MerchantRoleServiceImpl.java
├── support/MerchantRoleSupport.java
├── constant/MerchantRoleConstants.java
├── domain/MerchantRoleQuery.java
├── domain/MerchantRoleRow.java
├── dto/MerchantRoleCreateDTO.java
├── dto/MerchantRoleUpdateDTO.java
├── vo/MerchantRoleListVO.java
├── vo/MerchantRoleDetailVO.java
└── mapper/MerchantRoleMapper.java

resources/mapper/account/MerchantRoleMapper.xml
```

### 1.2 关联模块协作

```text
【登录】GET /agri/seller/context → 前端设置 X-Shop-Id
            ↓
【本模块 · 当前店铺员工角色】
    insert sys_role（role_key=seller_s{shopId}_*）
            ↓
    insert biz_shop_role（shop_id, role_id）
            ↓
    insert sys_role_menu（仅 agri:seller:* 菜单）
            ↓
【员工管理】selectRoleOptions / countValidShopRoles
            ↓
    sys_user_role ← 员工 user_id
```

| 关联 | 方式 |
|------|------|
| **员工管理** | 读 `biz_shop_role(shopId)`；停用角色不出现在下拉 |
| **店铺上下文** | 列表/CRUD 均按 `X-Shop-Id` |
| **平台角色管理** | `admin/merchant` **不** 经本模块 |

### 1.3 菜单与权限

| 项 | 值 |
|----|-----|
| 路由 | `/agri/seller/role` |
| 权限前缀 | `agri:seller:role` |

| 权限 | 说明 |
|------|------|
| `list` | 列表、菜单树 |
| `query` | 详情 |
| `add` | 创建 |
| `edit` | 编辑 |
| `remove` | 删除 |

---

## 2. 数据库设计

**无新建表。** 沿用 [`sql/biz_shop.sql`](../../../../sql/biz_shop.sql) 中 `biz_shop_role` 及 RuoYi `sys_role` / `sys_role_menu`。

### 2.1 核心表

| 表 | 用法 |
|----|------|
| `sys_role` | `role_name`、`role_key`、`status`、`remark`（角色描述） |
| `sys_role_menu` | 商家端 `menu_id` |
| `biz_shop_role` | `(shop_id, role_id)` **店铺归属** |
| `sys_user_role` | 员工绑定；删除前统计 **当前店铺** 引用 |

### 2.2 role_key 约定

| 规则 | 说明 |
|------|------|
| 前缀 | `seller_s{shopId}_` + 8 位随机串 |
| 禁止 | `admin`、`merchant`、`member` |
| 唯一 | `sys_role.role_key` 全局唯一 |

### 2.3 列表 SQL（对齐草稿 §5）

```sql
SELECT r.role_id, r.role_name, r.remark, r.status, r.create_time, bind_count
FROM sys_role r
INNER JOIN biz_shop_role sr ON r.role_id = sr.role_id
WHERE sr.shop_id = #{shopId}
  AND r.del_flag = '0'
ORDER BY r.create_time DESC
```

`bind_count`：统计 **当前店铺** 已绑定该角色的员工数。

### 2.4 删除校验 SQL

```sql
SELECT COUNT(DISTINCT ur.user_id)
FROM sys_user_role ur
INNER JOIN biz_merchant_account a ON ur.user_id = a.account_id AND a.shop_id = #{shopId}
INNER JOIN biz_shop_role sr ON sr.role_id = ur.role_id AND sr.shop_id = #{shopId}
WHERE ur.role_id = #{roleId}
```

---

## 3. 接口设计

**基路径：** `/agri/seller/role`  
**公共请求头：** `X-Shop-Id`

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/list` | list | 当前店铺角色列表；Query: `roleName` |
| GET | `/menuTree` | list | 商家端可分配菜单树 |
| GET | `/{roleId}` | query | 详情（含 `menuIds`、`bindCount`） |
| POST | `/` | add | 创建（归属当前店铺） |
| PUT | `/` | edit | 编辑 |
| DELETE | `/{roleIds}` | remove | 删除 |

---

## 4. 核心流程

### 4.1 创建

```text
requireMerchantAdmin() → merchantId
requireCurrentShopId(merchantId) → shopId
assert roleName 店铺内唯一
assert menuIds ⊆ 商家端可分配菜单
    → insert sys_role
    → insert biz_shop_role(shopId, roleId)
    → insert sys_role_menu
```

### 4.2 删除

```text
requireRoleOfShop(shopId, roleId)
assert 当前店铺 bind_count = 0
    → delete biz_shop_role
    → deleteRoleById（sys_role_menu + sys_role）
```

---

## 5. 校验与错误码

| 场景 | 处理 |
|------|------|
| 未携带 X-Shop-Id | 「请先选择当前店铺」 |
| 非经营账号 | 「仅商户经营账号可管理角色」 |
| 角色名重复 | 「该角色名称已存在」（**店铺内**） |
| menuIds 为空 | 「请至少选择一项功能权限」 |
| 删除已绑定 | 「该角色已分配给员工，请先解除绑定」 |
| 跨店铺 roleId | 「角色不存在」 |

---

## 6. 测试要点

- `MerchantRoleSupportTest` — 名称唯一、菜单、删除阻断
- `MerchantRoleServiceImplTest` — 创建/删事务
- `MerchantRoleControllerTest` — MockMvc CRUD

---

## 7. 修订记录

| 版本 | 说明 |
|------|------|
| v1.0 | 首版：`biz_merchant_role` 商户范围 |
| **v1.1** | 对齐草稿 §5～§6：`biz_shop_role` 店铺范围；`X-Shop-Id`；`role_key=seller_s{shopId}_*` |

---

*文档版本：v1.1 · 关联《角色管理功能需求.md》v1.1*
