# 员工管理 — 技术方案

> **依据：** 《员工管理功能需求.md》v1.2  
> **关联：** 《账号管理技术方案》v1.1、《店铺管理技术方案》v1.2.6、《店铺设置技术方案》v1.1.1（**平台全局**）、商家端《店铺设置技术方案》v1.1（**员工概览**）、《关联需求分析.md》§9、店铺上下文 `/agri/seller/context`  
> **范围：** 商家端 `/agri/seller/employee` 员工 CRUD、改密、配额校验；列表为 **当前店铺员工**。  
> **原则：** **无新建表**；复用 `sys_user`、`biz_merchant_account`、`biz_shop_role`、`sys_user_role`；写操作 **商户 + 店铺** 数据隔离；**仅商户经营账号** 可管理员工；依赖请求头 **`X-Shop-Id`**。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi v3.9.2（springboot2 分支） |
| 数据库 | MySQL 5.7.39 |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 密码 | `SecurityUtils.encryptPassword`（BCrypt） |
| 头像 | `CommonController` 上传 URL → `sys_user.avatar` |
| 店铺上下文 | `SellerShopContext` + `SellerShopContextInterceptor`；请求头 `X-Shop-Id` |
| 日志 | 增删改、改密 `@Log` |

### 1.1 模块落位

```text
com.ruoyi.web.modules.account/
├── controller/MerchantEmployeeController.java       # /agri/seller/employee
├── service/IMerchantEmployeeService.java
├── service/impl/MerchantEmployeeServiceImpl.java
├── support/MerchantEmployeeSupport.java           # 商户/店铺上下文、配额、归属校验
├── constant/MerchantEmployeeConstants.java
├── domain/MerchantEmployeeQuery.java
├── domain/MerchantEmployeeRow.java
├── dto/MerchantEmployeeCreateDTO.java
├── dto/MerchantEmployeeUpdateDTO.java
├── dto/MerchantEmployeeProfileDTO.java
├── dto/MerchantEmployeePasswordDTO.java
├── vo/MerchantEmployeeListVO.java
├── vo/MerchantEmployeeDetailVO.java
├── vo/MerchantEmployeeQuotaVO.java
├── vo/MerchantEmployeeRoleOptionVO.java
└── mapper/MerchantEmployeeMapper.java

resources/mapper/account/MerchantEmployeeMapper.xml
```

### 1.2 关联模块协作

```text
【登录】GET /agri/seller/context → 前端设置 X-Shop-Id
            ↓ SellerShopContextInterceptor 校验
【平台】biz_shop_global_config.max_sub_admin
            ↓ IShopGlobalConfigFacade.getMaxSubAdmin()
【本模块】创建/启用前 count 员工绑定（商户级）
            ↓
sys_user ←── account_id ── biz_merchant_account（shop_id=当前店铺）
            ↓
sys_user_role ← role_id ← biz_shop_role（shop_id 隔离当前店铺角色）

【店铺 · 角色管理】角色来源 biz_shop_role ⋈ sys_role；本模块 **选用**
【平台 · 账号管理】同表只读统览；员工删改以本模块为准
```

| 关联 | 方式 |
|------|------|
| **平台 · 店铺设置** | `IShopGlobalConfigFacade.getMaxSubAdmin()`（全局配额） |
| **商家 · 店铺设置** | `GET /agri/seller/shop/employeeStats` 展示当前店员工数 + 配额；明细 CRUD 在本模块 |
| **店铺管理** | `biz_merchant_account`；员工 `shop_id = X-Shop-Id` |
| **店铺上下文** | `MerchantEmployeeSupport.requireCurrentShopId()` |
| **角色管理（待建设）** | 角色来源 `biz_shop_role` ⋈ `sys_role` |
| **商家端其他模块** | 路径前缀 `/agri/seller/**` + `X-Shop-Id` |

### 1.3 菜单与权限

| 项 | 值 |
|----|-----|
| 路由 | `/agri/seller/employee` |
| 权限前缀 | `agri:seller:employee` |
| 前端组件 | `agri/seller/employee/index`（本期可仅后端） |

| 权限 | 说明 |
|------|------|
| `list` | 列表、配额、角色下拉 |
| `query` | 详情 |
| `add` | 创建 |
| `edit` | 编辑 / 修改账号 |
| `resetPwd` | 修改密码 |
| `remove` | 删除 |

> **访问约束：** 除 `@PreAuthorize` 外，Service 层 **强制** 当前用户为 **商户经营账号**，且请求须携带有效 **`X-Shop-Id`**（属当前商户），否则拒绝。

---

## 2. 数据库设计

**无新建表。** 沿用 [`sql/biz_shop.sql`](../../../../sql/biz_shop.sql)、[`sql/biz_shop_global_config.sql`](../../../../sql/biz_shop_global_config.sql)。

### 2.1 核心表与员工语义

| 表 | 员工相关用法 |
|----|--------------|
| `sys_user` | 登录凭证与资料：`user_name`=手机号，`nick_name`=员工名称，`status` 0正常/1停用 |
| `biz_merchant_account` | **员工行：** `account_id=user_id`，`merchant_id`=当前商户，`shop_id`=当前店铺，`del_flag=0` |
| `biz_shop_role` | `(shop_id, role_id)` 定义 **当前店铺可用角色** |
| `sys_user_role` | 员工绑定的 `role_id` |
| `sys_role` | 角色元数据；**禁止** 绑定 `role_key in ('admin','merchant')` |
| `biz_shop_global_config` | `max_sub_admin` 全局配额（`config_id=1`） |

### 2.2 列表范围

- `WHERE a.merchant_id = #{merchantId} AND a.shop_id = #{shopId}`
- **仅员工**（`shop_id IS NOT NULL`）；**不含** 经营账号（`shop_id IS NULL`）

### 2.3 配额计数 SQL（商户级，不变）

```sql
SELECT COUNT(1)
FROM biz_merchant_account
WHERE merchant_id = #{merchantId}
  AND shop_id IS NOT NULL
  AND del_flag = '0'
```

### 2.4 列表 SQL（对齐功能需求 EM1、草稿 §5）

```sql
SELECT u.user_id, u.user_name, u.nick_name, u.phonenumber, u.email,
       u.status, u.avatar, u.create_time,
       a.merchant_id, a.shop_id, 'EMPLOYEE' AS account_type
FROM sys_user u
INNER JOIN biz_merchant_account a
        ON u.user_id = a.account_id AND a.del_flag = '0'
WHERE a.merchant_id = #{merchantId}
  AND a.shop_id = #{shopId}
  AND u.del_flag = '0'
  AND (#{employeeName} IS NULL OR #{employeeName} = ''
       OR u.nick_name LIKE CONCAT('%', #{employeeName}, '%'))
ORDER BY u.create_time DESC
```

### 2.5 当前店铺角色校验 SQL

```sql
SELECT COUNT(1)
FROM biz_shop_role sr
INNER JOIN sys_role r ON sr.role_id = r.role_id AND r.del_flag = '0'
WHERE sr.shop_id = #{shopId}
  AND sr.role_id IN ( ... roleIds ... )
  AND r.role_key NOT IN ('admin', 'merchant')
```

返回值须等于 `roleIds.length`。

### 2.6 Mapper 扩展（`BizMerchantAccountMapper`）

| 方法 | 说明 |
|------|------|
| `countEmployeesByMerchantId` | 配额计数（§2.3，商户级） |
| `logicDeleteEmployeeByUserId` | `shop_id IS NOT NULL` 逻辑删绑定 |

---

## 3. 接口设计

**基路径：** `/agri/seller/employee`  
**公共请求头：** `X-Shop-Id`（当前店铺 ID，由 `/agri/seller/context` 初始化或切换店铺后更新）

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/list` | list | 分页列表；Query: `employeeName` |
| GET | `/quota` | list | 配额：`merchantName`、`shopName`、`usedCount`、`maxCount` |
| GET | `/roleOptions` | list | 当前店铺可选角色（`biz_shop_role` ⋈ `sys_role`） |
| GET | `/{userId}` | query | 员工详情（含 `roleIds`，按当前店铺过滤） |
| POST | `/` | add | 创建员工（归属当前店铺） |
| PUT | `/` | edit | 编辑员工 |
| PUT | `/profile` | edit | 修改账号 |
| PUT | `/resetPwd` | resetPwd | 修改密码 |
| DELETE | `/{userIds}` | remove | 删除员工 |

### 3.1 列表 VO（MerchantEmployeeListVO）

| 字段 | 说明 |
|------|------|
| userId / loginAccount / employeeName / mobile / email / status | 同 v1.0 |
| accountType | 固定 `EMPLOYEE` |
| operable | 固定 `true` |

### 3.2 配额 VO（MerchantEmployeeQuotaVO）

| 字段 | 说明 |
|------|------|
| merchantId / merchantName | 当前商户 |
| shopId / shopName | **当前店铺**（只读展示） |
| usedCount | 商户级员工绑定数 |
| maxCount | 全局上限 |

### 3.3 创建（MerchantEmployeeCreateDTO）

| 字段 | 必填 | 说明 |
|------|:----:|------|
| employeeName / mobile / email / avatar / status | — | 同 v1.0 |
| roleIds | 是 | 至少 1 个；须 ∈ **当前店铺** `biz_shop_role` |

> **不含 shopId 入参**：创建时 `shop_id` 取自 `SellerShopContext.getShopId()`。  
> **不含 password**（EM10）。

---

## 4. 核心流程

### 4.1 创建员工

```text
requireMerchantAdmin() → merchantId
requireCurrentShopId(merchantId) → shopId   # X-Shop-Id
assert usedCount < maxSubAdmin              # 商户级配额
assert mobile 全平台唯一
assert roleIds ⊆ biz_shop_role(shopId)
    → insert sys_user（password=随机 BCrypt）
    → insert biz_merchant_account（shop_id=shopId）
    → insertUserRole(userId, roleIds)
```

### 4.2 编辑 / 修改账号 / 改密 / 删除

```text
requireCurrentShopId(merchantId) → shopId
requireEmployeeRow(merchantId, shopId, userId)
    → 校验目标属于当前店铺员工
    → 更新/删除逻辑同 v1.0
```

### 4.3 店铺上下文协作

```text
登录 → GET /agri/seller/context
    → 商户管理员：默认 merchant 最早 shopId
    → 员工：biz_merchant_account.shop_id
前端设置 X-Shop-Id → 访问 /agri/seller/employee/**
切换店铺 → PUT /agri/seller/context/shop → 更新 X-Shop-Id
```

---

## 5. 校验与错误码

| 场景 | 处理 |
|------|------|
| 未携带 X-Shop-Id | 「请先选择当前店铺」 |
| 非经营账号访问 | 「仅商户经营账号可管理员工」 |
| 超配额 | 「子管理员人数已达上限（{max}）…」 |
| 手机号重复 | 「该手机号已被使用」 |
| 角色非法 | 「只能选择当前店铺的角色」 |
| 跨店铺员工 | 「员工不存在」 |

---

## 6. 测试要点

- 单测：`MerchantEmployeeSupportTest`（配额、店铺归属、角色校验）
- 单测：`MerchantEmployeeServiceImplTest`（创建/删/改密，`requireCurrentShopId` mock）
- 接口测：`MerchantEmployeeControllerTest`

| 用例 | 预期 |
|------|------|
| 店铺 A 列表 | 仅 shopId=A 的员工；不含店铺 B 员工 |
| 无 X-Shop-Id | 业务失败 |
| 绑定非当前店铺 roleId | 失败 |
| 创建员工 shop_id | 等于 header 中 shopId |

---

## 7. 修订记录

| 版本 | 说明 |
|------|------|
| v1.0 | 首版：商家端 `/agri/seller/employee`；配额读全局配置 |
| **v1.1** | 对齐需求 v1.2：列表/角色按 **当前店铺**；`biz_shop_role`；`X-Shop-Id`；包名 `com.ruoyi.web.modules.account` |

---

*文档版本：v1.1 · RuoYi v3.9.2-springboot2 · MySQL 5.7.39 · 关联《员工管理功能需求.md》v1.2*
