# 用户与组织架构管理 - API 文档

> **版本**: v2.4（新增 Entra ID SSO 登录接口）  
> **最后更新**: 2026-05-19  
> **维护者**: FFOA 后端团队

---

## 📋 文档变更记录

| 版本    | 日期       | 修改人    | 修改内容                                                           |
| ------- | ---------- | --------- | ------------------------------------------------------------------ |
| v2.4    | 2026-05-19 | FFOA Team | 新增 Entra ID SSO 登录接口（`/auth/sso/start` + `/auth/sso/callback`），issue #334 |
| v2.1.36 | 2026-03-13 | FFOA Team | 新增钉钉年假计划参数查询与更新接口，明确字段语义                   |
| v2.1.35 | 2026-03-11 | FFOA Team | 年假释放计划接口补充无计划员工与未生成原因                         |
| v2.1.34 | 2026-03-11 | FFOA Team | 新增年假释放计划手动重算接口，用于仅更新本地中间表                 |
| v2.1.31 | 2026-03-11 | FFOA Team | 年假释放计划与详情接口改为读取本地数据库中间表，不再依赖宜搭中间表 |
| v2.1.30 | 2026-03-11 | FFOA Team | 新增钉钉假期余额详情接口，返回释放记录与使用记录                   |
| v2.1.29 | 2026-03-11 | FFOA Team | 钉钉假期余额总览改为读取本地快照，并新增快照刷新接口               |
| v2.1.28 | 2026-03-11 | FFOA Team | 补充钉钉考勤类任务手动与定时触发的时间窗口规则                     |
| v2.1.27 | 2026-03-10 | FFOA Team | 新增钉钉审批撤销修复接口                                           |
| v2.1.26 | 2026-01-25 | FFOA Team | 预定义角色 code 对齐 Administrator/Employee                        |
| v1.0    | 2024-11-01 | FFOA Team | 初始版本                                                           |
| v2.0    | 2025-12-20 | FFOA Team | 新增组织管理、多部门归属 API                                       |
| v2.1    | 2025-12-26 | FFOA Team | 组织级权限隔离、角色分配支持 organizationId                        |
| v2.1.1  | 2025-12-26 | FFOA Team | 补充身份源、密码修改、登录安全策略的 API 说明                      |
| v2.1.25 | 2026-01-05 | FFOA Team | 同步 PRD v2.1.25：身份源简化、登录响应简化                         |

---

## 📋 API 概述

用户与组织架构管理模块 API 提供统一身份管理、组织架构、角色权限、身份认证和外部同步功能。所有接口遵循 FFOA 统一 API 规范。

### Base URL

```
开发环境: http://localhost:3001/api/v1
测试环境: https://test-api.ffoa.com/api/v1
生产环境: https://api.ffoa.com/api/v1
```

### 认证方式

- **类型**: Bearer Token (JWT)
- **Header**: `Authorization: Bearer <token>`
- **Token 有效期**: Access Token 1h + Refresh Token 7d

### 通用响应格式

**成功响应**:

```json
{
  "success": true,
  "data": {
    /* 实际数据 */
  },
  "message": "操作成功",
  "timestamp": "2025-12-26T10:30:00.000Z",
  "path": "/api/v1/users"
}
```

**错误响应**:

```json
{
  "success": false,
  "error": {
    "code": "IAM_USERNAME_EXISTS",
    "message": "用户名已被使用",
    "details": {
      /* 可选的详细信息 */
    }
  },
  "timestamp": "2025-12-26T10:30:00.000Z",
  "path": "/api/v1/users",
  "statusCode": 409
}
```

### 模块信息

- **模块标识**: `organization`
- **数据库 Schema**: `platform_iam` (认证权限) + `corp_hr` (组织架构)
- **状态**: ✅ 核心功能已实现
- **API 总数**: 92 个端点（核心接口 77 个 + 扩展接口 15 个）

### v2.0 架构前提

| 概念           | 说明                                                                 |
| -------------- | -------------------------------------------------------------------- |
| **独立组织表** | v2.0: Organization 表是独立的一等公民，支持组织特有属性              |
| **多组织**     | 系统支持多个独立组织（如 FF China、FF USA），每个组织有独立的 ID     |
| **部门归属**   | 所有部门通过 `organizationId` 归属组织，顶级部门的 `parentId = null` |
| **多部门归属** | 同一用户可同时属于多个组织/部门，每个归属有独立的岗位和汇报关系      |
| **部门内汇报** | 用户在某部门的直属上级（`managerId`）必须是该部门的成员              |
| **矩阵式组织** | 通过用户同时属于多个部门来实现，而非跨部门汇报                       |

### 接口统计（v2.1）

> v2.4 起认证接口新增 2 个 SSO 端点（`/auth/sso/start` + `/auth/sso/callback`），核心接口由 3 升为 5，总计由 5 升为 7。其它分类统计未变。

| 分类         | 核心接口 | 扩展接口 | 总计  | 说明                                                                                             |
| ------------ | -------- | -------- | ----- | ------------------------------------------------------------------------------------------------ |
| 认证接口     | 5        | 2        | 7     | 登录、登出、Token 刷新、注册、修改密码、SSO 发起、SSO 回调（v2.4）                               |
| 用户管理     | 18       | 5        | 23    | 用户 CRUD(8)、角色分配(3)、权限查询(2)、多部门归属(5)、区域角色管理(3)、恢复用户(1)、重置密码(1) |
| **组织管理** | **8**    | **0**    | **8** | **组织 CRUD、区域关联、统计、用户组织列表（v2.0 新增）**                                         |
| 部门管理     | 9        | 4        | 13    | 部门 CRUD、部门树、负责人管理、批量操作、成员管理                                                |
| 岗位管理     | 5        | 0        | 5     | 岗位 CRUD                                                                                        |
| 区域管理     | 5        | 4        | 9     | 区域 CRUD、活跃区域、按代码查询、统计、默认组织设置                                              |
| 系统角色     | 10       | 0        | 10    | 角色 CRUD、权限分配、用户分配                                                                    |
| 权限管理     | 4        | 0        | 4     | 权限查询、分组                                                                                   |
| 流程角色     | 8        | 0        | 8     | 流程角色 CRUD、规则解析                                                                          |
| 外部同步     | 3        | 0        | 3     | Entra ID 同步、状态查询                                                                          |
| 审计日志     | 0        | 0        | 0     | **注意：审计日志接口在审计系统模块中（`/audit/logs`）**                                          |

**总计**: **94 个 API 端点**（v2.4 起新增 2 个 SSO 端点）

- **核心接口**: 79 个（文档主要描述的接口）
- **扩展接口**: 15 个（额外功能接口）
- **v2.1 更新**: 1 个（角色分配支持组织级隔离，修改现有接口）
- **v2.0 新增**: 15 个（8个组织管理 + 5个多部门归属 + 2个其他）
- **v1.0 基础**: 62 个

### 通用约定

| 约定         | 说明                                                          |
| ------------ | ------------------------------------------------------------- |
| **基础路径** | `/api/v1`                                                     |
| **时间格式** | UTC ISO 8601（如 `2025-12-07T10:30:00.123Z`），前端负责本地化 |
| **认证方式** | Bearer Token（`Authorization: Bearer <token>`）               |
| **请求头**   | `Content-Type: application/json`                              |
| **响应格式** | 统一 `{ success, data, message?, timestamp, path }` 结构      |

### v2.0 多区域支持

**区域模型**：v2.0 架构中，区域与组织关联（`organization_regions`），部门通过组织继承区域信息

| 字段                                 | 用途     | 说明                           |
| ------------------------------------ | -------- | ------------------------------ |
| `Organization.primaryRegionId`       | 主要区域 | 单值，组织的主要运营区域       |
| `Organization.organizationRegions[]` | 运营覆盖 | 多值，组织的所有运营区域       |
| `Department.organizationId`          | 组织归属 | v2.0: 部门通过组织间接关联区域 |

**区域管理规则（v2.0）**：

- 每个组织必须归属一个主要区域（`primaryRegionId`）
- 组织可以关联多个运营区域（`organization_regions` 表）
- 部门的区域信息从其所属组织继承
- 用户的区域由其主部门所属的组织决定

### 相关文档

- [PRD 产品需求文档](./01-prd.md) - 完整的功能规格和业务规则
- [ARCHITECTURE 架构设计](./03-architecture.md) - 系统架构和技术设计
- [后端规范入口](../../../.agents/skills/backend-main/references/backend-standards.md) - 通用 API 规范
- [Entra 同步配置](../../../../docs/setup/deployment-complete-guide.md#microsoft-entra-id-配置-用户同步) - 外部同步配置指南

---

## 钉钉模块 API

钉钉模块接口已迁移至独立模块文档维护，见 [docs/modules/dingtalk/07-api.md](../dingtalk/07-api.md)。

---

## 认证与权限

### 认证方式

所有接口（除公开接口外）需要在请求头中携带 JWT Token：

```http
Authorization: Bearer <access_token>
```

### 权限矩阵（v2.1 - 已实现 Scope 控制）

> **更新时间**: 2025-12-26  
> **状态**: ✅ 已完成 Scope 控制实现

| 权限代码                  | 说明         | Scope          | 适用接口                                                              | 装饰器                                                 |
| ------------------------- | ------------ | -------------- | --------------------------------------------------------------------- | ------------------------------------------------------ |
| `user:read`               | 查看用户     | `all`          | GET /users, GET /users/:id                                            | `@RequirePermissions('user:read')`               |
| `user:create`             | 创建用户     | `all`          | POST /users                                                           | `@RequirePermissions('user:create')`             |
| `user:update`             | 更新用户     | `all`          | PATCH /users/:id, POST /users/:id/terminate, POST /users/:id/activate | `@RequirePermissions('user:update')`             |
| `user:delete`             | 删除用户     | `all`          | DELETE /users/:id                                                     | `@RequirePermissions('user:delete')`             |
| `user:export`             | 导出用户     | `all`          | GET /users/export                                                     | `@RequirePermissions('user:export')`             |
| **`organization:read`**   | **查看组织** | **`all`**      | **GET /organizations, GET /organizations/:id**                        | **`@RequirePermissions('organization:read')`**   |
| **`organization:create`** | **创建组织** | **`all`**      | **POST /organizations**                                               | **`@RequirePermissions('organization:create')`** |
| **`organization:update`** | **更新组织** | **`all`**      | **PATCH /organizations/:id**                                          | **`@RequirePermissions('organization:update')`** |
| **`organization:delete`** | **删除组织** | **`all`**      | **DELETE /organizations/:id**                                         | **`@RequirePermissions('organization:delete')`** |
| `department:read`         | 查看部门     | `all`          | GET /departments/\*                                                   | `@RequirePermissions('department:read')`         |
| `department:create`       | 创建部门     | `all`          | POST /departments                                                     | `@RequirePermissions('department:create')`       |
| `department:update`       | 更新部门     | `all`          | PUT /departments/:id, PUT /departments/:id/head                       | `@RequirePermissions('department:update')`       |
| `department:delete`       | 删除部门     | `all`          | DELETE /departments/:id                                               | `@RequirePermissions('department:delete')`       |
| `position:read`           | 查看岗位     | `all`          | GET /positions, GET /positions/:id                                    | `@RequirePermissions('position:read')`           |
| `position:create`         | 创建岗位     | `all`          | POST /positions                                                       | `@RequirePermissions('position:create')`         |
| `position:update`         | 更新岗位     | `all`          | PUT /positions/:id                                                    | `@RequirePermissions('position:update')`         |
| `position:delete`         | 删除岗位     | `all`          | DELETE /positions/:id                                                 | `@RequirePermissions('position:delete')`         |
| `role:read`               | 查看角色     | `all`          | GET /roles/_, GET /permissions/_, GET /workflow-roles/\*              | `@RequirePermissions('role:read')`               |
| `role:create`             | 创建角色     | `all`          | POST /roles, POST /workflow-roles                                     | `@RequirePermissions('role:create')`             |
| `role:update`             | 更新角色     | `all`          | PUT /roles/:id, PUT /workflow-roles/:id                               | `@RequirePermissions('role:update')`             |
| `role:delete`             | 删除角色     | `all`          | DELETE /roles/:id, DELETE /workflow-roles/:id                         | `@RequirePermissions('role:delete')`             |
| `role:manage`             | 管理角色     | `all`          | 角色-权限、角色-用户关联操作                                          | `@RequirePermissions('role:manage')`             |
| **`region:create`**       | **创建区域** | **`all`**      | **POST /regions**                                                     | **`@RequirePermissions('region:create')`**       |
| **`region:read`**         | **查看区域** | **`all`**      | **GET /regions, GET /regions/:id**                                    | **`@RequirePermissions('region:read')`**         |
| **`region:update`**       | **更新区域** | **`all`**      | **PUT /regions/:id**                                                  | **`@RequirePermissions('region:update')`**       |
| **`region:delete`**       | **删除区域** | **`all`**      | **DELETE /regions/:id**                                               | **`@RequirePermissions('region:delete')`**       |
| **`organization:sync`**   | **同步组织** | **`all`**      | **POST /organization/sync, GET /organization/sync/status**            | **`@RequirePermissions('organization:sync')`**   |
| `audit:read`              | 查看审计     | `organization` | GET /audit/logs                                                       | **注意：审计日志接口在审计系统模块中**                 |
| `audit:export`            | 导出审计     | `organization` | GET /audit/export                                                     | **注意：审计日志接口在审计系统模块中**                 |

> **说明（v2.1 - Scope 控制已实现）**：
>
> - **Scope 说明**：
>   - `all`：全局/跨组织（✅ 已实现）
> - **组织隔离**：当前以全局 scope 方式控制访问，组织隔离逻辑以代码实现为准
> - **权限格式**：`{resource}:{action}`，例如 `user:read` 表示可以查看用户
> - **实现状态**：
>   - ✅ 所有 Controller 已迁移到新的 Scope 装饰器
>   - ✅ PermissionsGuard 支持完整的 Scope 检查
>   - ✅ Organizations Service 支持组织级数据过滤

### 权限装饰器（v2.0）

```typescript
@RequirePermissions('user:read')           // 用户查看
@RequirePermissions('user:create')         // 用户创建
@RequirePermissions('organization:read')   // 组织查看（v2.0）
@RequirePermissions('organization:create') // 组织创建（v2.0）
@RequirePermissions('role:manage')         // 角色管理
@RequirePermissions('organization:sync')   // 组织同步
```

---

## 公共类型定义

### 统一响应格式

#### 成功响应

```typescript
interface ApiResponse<T> {
  success: true;
  data: T;
  message?: string;
  timestamp: string; // ISO 8601 格式
  path: string; // 请求路径
}
```

#### 分页响应

```typescript
interface PaginatedResponse<T> {
  success: true;
  data: {
    items: T[];
    total: number;
    page: number;
    limit: number; // 等同于 pageSize
    totalPages: number;
    hasNext: boolean;
    hasPrev: boolean;
  };
  message?: string;
  timestamp: string;
  path: string;
}
```

#### 错误响应

```typescript
interface ApiError {
  success: false;
  error: {
    code: string; // 机器可读错误码（以 IAM_ 前缀为准）
    message: string; // 人类可读消息
    details?: any; // 详细信息
    field?: string; // 字段级错误
    errors?: FieldError[]; // 多字段验证错误
  };
  timestamp: string;
  path: string;
  method: string;
  statusCode: number;
}

interface FieldError {
  field: string;
  message: string;
  value?: any;
  constraint?: string;
}
```

### 错误码约定（以实现为准）

- 本模块错误码统一以 `IAM_` 前缀（实现定义见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`）。
- 自动化测试断言以实现返回的 `IAM_` 错误码为准。

### 通用查询参数

所有列表类接口统一支持以下参数：

```typescript
interface CommonQueryParams {
  // 分页
  page?: number; // 页码，从 1 开始，默认 1
  pageSize?: number; // 每页数量，默认 20，最大 100

  // 排序
  sortBy?: string; // 排序字段，默认 'createdAt'
  sortOrder?: "asc" | "desc"; // 排序方向，默认 'desc'

  // 通用筛选
  keyword?: string; // 模糊搜索
  region?: string; // 按区域筛选
}
```

#### keyword 匹配字段

| 接口               | 匹配字段                                         |
| ------------------ | ------------------------------------------------ |
| `GET /users`       | `displayName`、`email`、`employeeId`、`username` |
| `GET /departments` | `name`、`code`                                   |
| `GET /roles`       | `name`、`code`                                   |
| `GET /positions`   | `name`、`code`                                   |

### 枚举类型

```typescript
// 用户状态
enum UserStatus {
  ACTIVE = "ACTIVE", // 正常
  INACTIVE = "INACTIVE", // 未激活
  SUSPENDED = "SUSPENDED", // 已停用
  TERMINATED = "TERMINATED", // 已离职
}

// 用户来源
enum UserSource {
  LOCAL = "LOCAL", // 本地创建
  LDAP = "LDAP", // LDAP 登录同步
}

// 流程角色规则类型
enum WorkflowRuleType {
  ORGANIZATION_RELATION = "ORGANIZATION_RELATION", // 组织关系
  SYSTEM_ROLE_MAPPING = "SYSTEM_ROLE_MAPPING", // 系统角色映射
  FIXED_USERS = "FIXED_USERS", // 固定用户
  DYNAMIC_SCRIPT = "DYNAMIC_SCRIPT", // 动态脚本
}
```

---

## API 列表

### 1️⃣ 认证接口 (7 个)

| 方法 | 路径                    | 说明                            | 权限   |
| ---- | ----------------------- | ------------------------------- | ------ |
| POST | `/auth/login`           | 用户登录                        | 公开   |
| POST | `/auth/logout`          | 用户登出                        | 已登录 |
| POST | `/auth/refresh`         | 刷新 Token                      | 已登录 |
| POST | `/auth/register`        | 用户注册                        | 公开   |
| PUT  | `/auth/change-password` | 修改密码                        | 已登录 |
| POST | `/auth/dev-email-login` | 开发邮箱免密登录（**仅 dev 环境**，不算生产契约面）| 公开 |
| GET  | `/auth/sso/start`       | 发起 Entra ID SSO 登录（v2.4）  | 公开   |
| GET  | `/auth/sso/callback`    | Entra ID SSO 回调（v2.4）       | 公开   |

### 2️⃣ 用户管理接口 (23 个)

#### 核心接口 (18 个)

| 方法   | 路径                                               | 说明               | 权限          |
| ------ | -------------------------------------------------- | ------------------ | ------------- |
| GET    | `/users`                                           | 获取用户列表       | `user:read`   |
| GET    | `/users/me`                                        | 获取当前用户信息   | 已登录        |
| GET    | `/users/:id`                                       | 获取用户详情       | `user:read`   |
| POST   | `/users`                                           | 创建用户           | `user:create` |
| PATCH  | `/users/:id`                                       | 更新用户           | `user:update` |
| DELETE | `/users/:id`                                       | 删除用户（软删除） | `user:delete` |
| GET    | `/users/:id/roles`                                 | 获取用户角色       | 已登录        |
| POST   | `/users/:id/roles`                                 | 为用户分配角色     | `role:manage` |
| DELETE | `/users/:id/roles/:roleId`                         | 移除用户角色       | `role:manage` |
| GET    | `/users/:id/permissions`                           | 获取用户聚合权限   | 已登录        |
| GET    | `/users/:id/subordinates`                          | 获取下属列表       | 已登录        |
| POST   | `/users/:id/terminate`                             | 离职操作           | `user:update` |
| POST   | `/users/:id/activate`                              | 激活用户           | `user:update` |
| GET    | `/users/:id/departments`                           | 获取用户部门归属   | 已登录        |
| POST   | `/users/:id/departments`                           | 添加部门归属       | `user:update` |
| PATCH  | `/users/:userId/departments/:departmentId`         | 更新部门归属       | `user:update` |
| DELETE | `/users/:userId/departments/:departmentId`         | 移除部门归属       | `user:update` |
| PUT    | `/users/:userId/departments/:departmentId/primary` | 设置主部门         | `user:update` |

#### 扩展接口 (5 个)

| 方法 | 路径                             | 说明                     | 权限          |
| ---- | -------------------------------- | ------------------------ | ------------- |
| POST | `/users/:id/region-roles`        | 分配区域角色（完全替换） | `role:manage` |
| POST | `/users/:id/region-roles/add`    | 添加单个区域角色         | `role:manage` |
| POST | `/users/:id/region-roles/remove` | 删除单个区域角色         | `role:manage` |
| POST | `/users/:id/restore`             | 恢复已删除用户           | `user:update` |
| POST | `/users/:id/reset-password`      | 重置密码（管理员）       | `user:update` |

### 3️⃣ 部门管理接口 (13 个)

#### 核心接口 (9 个)

| 方法   | 路径                       | 说明         | 权限                |
| ------ | -------------------------- | ------------ | ------------------- |
| GET    | `/departments`             | 获取部门列表 | 已登录              |
| GET    | `/departments/tree`        | 获取部门树   | 已登录              |
| GET    | `/departments/:id`         | 获取部门详情 | 已登录              |
| GET    | `/departments/:id/members` | 获取部门成员 | 已登录              |
| GET    | `/departments/:id/stats`   | 获取部门统计 | 已登录              |
| POST   | `/departments`             | 创建部门     | `department:create` |
| PUT    | `/departments/:id`         | 更新部门     | `department:update` |
| PUT    | `/departments/:id/head`    | 设置部门主管 | `department:update` |
| DELETE | `/departments/:id`         | 删除部门     | `department:delete` |

#### 扩展接口 (4 个)

| 方法 | 路径                             | 说明                         | 权限                |
| ---- | -------------------------------- | ---------------------------- | ------------------- |
| GET  | `/departments/organizations`     | 获取顶级组织（用于视角选择） | 已登录              |
| POST | `/departments/batch`             | 批量创建部门                 | `department:create` |
| POST | `/departments/:id/members`       | 添加单个成员到部门           | `department:update` |
| POST | `/departments/:id/members/batch` | 批量添加成员到部门           | `department:update` |

### 4️⃣ 组织管理接口 (8 个)

| 方法   | 路径                                         | 说明               | 权限                  |
| ------ | -------------------------------------------- | ------------------ | --------------------- |
| POST   | `/organizations`                             | 创建组织           | `organization:create` |
| GET    | `/organizations`                             | 获取组织列表       | `organization:read`   |
| GET    | `/organizations/:id`                         | 获取组织详情       | `organization:read`   |
| PUT    | `/organizations/:id`                         | 更新组织           | `organization:update` |
| DELETE | `/organizations/:id`                         | 删除组织           | `organization:delete` |
| PUT    | `/organizations/:id/regions`                 | 设置组织的运营区域 | `organization:update` |
| GET    | `/organizations/:id/stats`                   | 获取组织统计       | `organization:read`   |
| GET    | `/organizations/users/:userId/organizations` | 获取用户的组织列表 | 已登录                |

### 6️⃣ 岗位管理接口 (5 个)

| 方法   | 路径             | 说明         | 权限              |
| ------ | ---------------- | ------------ | ----------------- |
| GET    | `/positions`     | 获取岗位列表 | 已登录            |
| GET    | `/positions/:id` | 获取岗位详情 | 已登录            |
| POST   | `/positions`     | 创建岗位     | `position:create` |
| PUT    | `/positions/:id` | 更新岗位     | `position:update` |
| DELETE | `/positions/:id` | 删除岗位     | `position:delete` |

### 7️⃣ 区域管理接口 (9 个)

#### 核心接口 (5 个)

| 方法   | 路径           | 说明         | 权限            |
| ------ | -------------- | ------------ | --------------- |
| GET    | `/regions`     | 获取区域列表 | `region:read`   |
| GET    | `/regions/:id` | 获取区域详情 | `region:read`   |
| POST   | `/regions`     | 创建区域     | `region:create` |
| PUT    | `/regions/:id` | 更新区域     | `region:update` |
| DELETE | `/regions/:id` | 删除区域     | `region:delete` |

#### 扩展接口 (4 个)

| 方法 | 路径                                | 说明                         | 权限            |
| ---- | ----------------------------------- | ---------------------------- | --------------- |
| GET  | `/regions/active`                   | 获取活跃区域（用于下拉选择） | 已登录          |
| GET  | `/regions/code/:code`               | 按代码获取区域               | `region:read`   |
| GET  | `/regions/:id/stats`                | 获取区域统计                 | `region:read`   |
| PUT  | `/regions/:id/default-organization` | 设置区域的默认组织           | `region:update` |

### 8️⃣ 系统角色管理接口 (10 个)

| 方法   | 路径                       | 说明               | 权限          |
| ------ | -------------------------- | ------------------ | ------------- |
| GET    | `/roles`                   | 获取角色列表       | `role:read`   |
| GET    | `/roles/:id`               | 获取角色详情       | `role:read`   |
| POST   | `/roles`                   | 创建角色           | `role:create` |
| PUT    | `/roles/:id`               | 更新角色           | `role:update` |
| DELETE | `/roles/:id`               | 删除角色           | `role:delete` |
| GET    | `/roles/:id/permissions`   | 获取角色权限       | `role:read`   |
| PUT    | `/roles/:id/permissions`   | 分配权限（覆盖式） | `role:manage` |
| GET    | `/roles/:id/users`         | 获取角色用户       | `role:read`   |
| POST   | `/roles/:id/users`         | 添加用户到角色     | `role:manage` |
| DELETE | `/roles/:id/users/:userId` | 从角色移除用户     | `role:manage` |

### 9️⃣ 权限管理接口 (4 个)

| 方法 | 路径                   | 说明         | 权限        |
| ---- | ---------------------- | ------------ | ----------- |
| GET  | `/permissions`         | 获取权限列表 | `role:read` |
| GET  | `/permissions/grouped` | 获取分组权限 | `role:read` |
| GET  | `/permissions/search`  | 搜索权限     | `role:read` |
| GET  | `/permissions/:id`     | 获取权限详情 | `role:read` |

### 🔟 流程角色管理接口 (8 个)

| 方法   | 路径                        | 说明                     | 权限          |
| ------ | --------------------------- | ------------------------ | ------------- |
| GET    | `/workflow-roles`           | 获取流程角色列表         | `role:read`   |
| GET    | `/workflow-roles/:id`       | 获取流程角色详情         | `role:read`   |
| POST   | `/workflow-roles`           | 创建流程角色             | `role:create` |
| PUT    | `/workflow-roles/:id`       | 更新流程角色             | `role:update` |
| DELETE | `/workflow-roles/:id`       | 删除流程角色             | `role:delete` |
| GET    | `/workflow-roles/:id/users` | 获取流程角色用户         | `role:read`   |
| POST   | `/workflow-roles/:id/users` | 分配用户                 | `role:manage` |
| POST   | `/workflow-roles/resolve`   | 解析流程角色（内部调用） | 服务间鉴权    |

### 1️⃣1️⃣ 外部同步接口 (3 个)

| 方法 | 路径                        | 说明               | 权限                |
| ---- | --------------------------- | ------------------ | ------------------- |
| POST | `/organization/sync`        | 触发 Entra ID 同步 | `organization:sync` |
| GET  | `/organization/sync/status` | 获取同步状态       | `organization:read` |
| GET  | `/organization/stats`       | 获取组织统计       | 已登录              |

### 1️⃣2️⃣ 审计日志接口

> **注意**: 审计日志接口位于**审计系统模块** (`backend/src/core/observability/audit/`)，不在本模块中。
>
> **实际路径**: `/api/v1/audit/logs`（而非 `/audit-logs`）
>
> 详细文档请参考：[审计系统 API 文档](../../audit-system/07-api.md)

| 方法 | 路径              | 说明             | 权限           |
| ---- | ----------------- | ---------------- | -------------- |
| GET  | `/audit/logs`     | 查询审计日志     | `audit:read`   |
| GET  | `/audit/logs/:id` | 获取审计日志详情 | `audit:read`   |
| GET  | `/audit/export`   | 导出审计日志     | `audit:export` |

**说明**:

- 审计系统是独立的基础设施模块，提供跨模块的审计日志功能
- Organization 模块的所有关键操作都会自动记录到审计日志中
- 如需查询 Organization 模块相关的审计日志，请使用审计系统的查询接口，并通过 `module` 参数过滤

---

## 接口详情

### 1. 认证接口

#### POST /auth/login

用户登录，获取访问令牌。

**权限**: 公开接口

**请求体**:

```typescript
interface LoginDto {
  username: string; // 用户名或邮箱
  password: string; // 密码
}
```

**示例请求**:

```json
{
  "username": "john.doe",
  "password": "SecurePass123!"
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 86400,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "username": "john.doe",
      "displayName": "John Doe",
      "email": "john@example.com",
      "status": "ACTIVE",
      "source": "LOCAL",
      "roles": ["Employee"],
      "permissions": ["user:read", "department:read"]
    }
  },
  "timestamp": "2025-12-07T10:30:00.000Z",
  "path": "/api/v1/auth/login"
}
```

**错误响应**:

| 错误码                    | HTTP | 说明             |
| ------------------------- | ---- | ---------------- |
| `IAM_INVALID_CREDENTIALS` | 401  | 用户名或密码错误 |
| `IAM_USER_SUSPENDED`      | 403  | 账号已被停用     |
| `IAM_USER_TERMINATED`     | 403  | 账号已注销       |

**业务规则（v2.1.1 更新）**:

- **身份源支持**:
  - LOCAL 用户：使用本地密码验证
  - LDAP 用户：通过 LDAP/AD 服务器验证
  - LDAP（含 Entra 同步）用户：通过 LDAP/AD 认证（与企业目录同步）
- **登录安全策略**（失败锁定机制）: 当前实现未启用，文档预留
- **状态检查**：`TERMINATED` 和 `SUSPENDED` 状态用户无法登录

**登录失败响应示例**（v2.1.1 新增）:

```json
{
  "success": false,
  "error": {
    "code": "IAM_INVALID_CREDENTIALS",
    "message": "用户名或密码错误"
  },
  "statusCode": 401
}
```

#### POST /auth/logout

用户登出，使当前 Token 失效。

**权限**: 已登录

**请求头**:

```http
Authorization: Bearer <access_token>
```

**成功响应** (200):

```json
{
  "success": true,
  "data": null,
  "message": "登出成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 仅使当前会话的 Token 失效（加入黑名单）
- 其他设备上的会话不受影响
- 如需退出所有设备，未来可提供 `POST /auth/logout-all` 接口（规划中）

---

#### POST /auth/refresh

刷新访问令牌。

**权限**: 已登录

**请求头**:

```http
Authorization: Bearer <access_token>
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 86400
  },
  "message": "Token 刷新成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码              | HTTP | 说明                     |
| ------------------- | ---- | ------------------------ |
| `IAM_TOKEN_EXPIRED` | 401  | Token 已过期，需重新登录 |
| `IAM_TOKEN_REVOKED` | 401  | Token 无效或已撤销       |

**业务规则**:

- 使用当前有效的 Token 换取新 Token
- 原 Token 在新 Token 生成后仍有效（短时间内）
- 如 Token 已过期超过宽限期（如 7 天），需重新登录

---

#### POST /auth/register

用户注册（公开接口）。

**权限**: 公开接口

**请求体**:

```typescript
interface RegisterDto {
  username: string; // 用户名（唯一）
  email: string; // 邮箱（唯一）
  password: string; // 密码（8位以上，至少2种字符类型）
  displayName: string; // 显示名称
}
```

**成功响应** (201):

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "john.doe",
    "email": "john.doe@ff.com",
    "displayName": "John Doe",
    "status": "ACTIVE",
    "source": "LOCAL",
    "createdAt": "2025-12-07T10:30:00.000Z"
  },
  "token": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 86400
  },
  "message": "注册成功，请等待管理员激活",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码                  | HTTP | 说明           |
| ----------------------- | ---- | -------------- |
| `IAM_USERNAME_EXISTS`   | 409  | 用户名已被使用 |
| `IAM_USER_EMAIL_EXISTS` | 409  | 邮箱已被使用   |

> 密码复杂度校验相关错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

**业务规则**:

- 注册后用户状态为 `ACTIVE`
- 密码必须符合复杂度要求（8位以上，至少2种字符类型）
- 用户名和邮箱必须全局唯一

---

#### PUT /auth/change-password

修改当前登录用户的密码（仅 LOCAL 用户）。

**权限**: 已登录

**请求头**:

```http
Authorization: Bearer <access_token>
```

**请求体**:

```typescript
interface ChangePasswordDto {
  oldPassword: string; // 旧密码
  newPassword: string; // 新密码（8位以上，至少2种字符类型）
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": null,
  "message": "密码修改成功，请重新登录",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码                   | HTTP | 说明                                 |
| ------------------------ | ---- | ------------------------------------ |
| `CANNOT_CHANGE_PASSWORD` | 400  | 非 LOCAL 用户不允许修改密码          |
| `UNAUTHORIZED`           | 401  | 旧密码错误                           |
| `BAD_REQUEST`            | 400  | 新密码不符合复杂度要求或与旧密码相同 |

**业务规则（v2.1.1 更新）**:

- **身份源限制**：
  - ✅ **LOCAL 用户**：可以修改密码
  - ❌ **LDAP 用户**：不能修改密码，需通过 LDAP/AD 系统修改
  - ❌ **LDAP（含 Entra 同步）用户**：不能修改密码，需通过 LDAP/AD 系统修改
- **密码策略**：
  - 最小长度：8位
  - 字符类型：至少包含大小写字母、数字、特殊字符中的2种
- **验证要求**：
  - 必须提供正确的旧密码
  - 新密码必须符合复杂度要求
  - 新密码不能与旧密码相同
- **后置操作**：
  - 修改成功后需要重新登录
  - 旧 Token 立即失效
  - 记录审计日志

**非本地用户错误响应**（v2.1.1）:

```json
{
  "success": false,
  "error": {
    "code": "CANNOT_CHANGE_PASSWORD",
    "message": "LDAP 用户不能通过本系统修改密码，请通过 LDAP/AD 系统修改",
    "userSource": "LDAP"
  },
  "statusCode": 400
}
```

---

#### GET /auth/sso/start

发起 Entra ID（Microsoft）OIDC SSO 登录，重定向到 Microsoft 授权页（v2.4）。

**权限**: 公开接口（`@Public()`，无 `@Auditable` —— 仅由 callback 记录登录成功/失败审计，避免每次刷新都写一条 audit）

**Query 参数**:

| 参数       | 类型   | 必需 | 说明                                                                              |
| ---------- | ------ | ---- | --------------------------------------------------------------------------------- |
| `redirect` | string | 否   | 登录成功后前端跳转目标 URL，默认 `/`；必须命中后端白名单校验以防 open redirect 攻击 |

**成功响应** (302):

```http
HTTP/1.1 302 Found
Location: https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2/v2.0/authorize?client_id=...&response_type=code&redirect_uri={AZURE_REDIRECT_URI}&scope=openid+profile+email&state={random}&nonce={random}&code_challenge={base64url}&code_challenge_method=S256
Set-Cookie: sso_state={random_base64url_32B}; HttpOnly; Secure; SameSite=Lax; Max-Age=900; Path=/
Set-Cookie: sso_nonce={random_base64url_32B}; HttpOnly; Secure; SameSite=Lax; Max-Age=900; Path=/
Set-Cookie: sso_redirect={safeRelativePath}; HttpOnly; Secure; SameSite=Lax; Max-Age=900; Path=/
Set-Cookie: sso_code_verifier={random_base64url_43_128_chars}; HttpOnly; Secure; SameSite=Lax; Max-Age=900; Path=/
```

**Cookie 约定（v2.4 共 4 个 cookie）**:

- 4 个临时 cookie 全部 `HttpOnly + Secure + SameSite=Lax`，**TTL 15 分钟（900s）**（容纳 Entra 条件访问 + MFA 慢操作）
- `sso_state`：CSRF 防护（query.state 必须等于 cookie 值）；`crypto.randomBytes(32)` → base64url
- `sso_nonce`：写入 OIDC authorize 请求，callback 阶段比对 ID token `nonce` claim；entropy 同 state
- `sso_redirect`：保存前端 redirect 目标（仅同源相对路径），callback 流程末尾 302 回该地址
- `sso_code_verifier`（v2.4 新增第 4 个）：PKCE 必须；43-128 char 随机串；callback 阶段携带到 token endpoint

**错误响应**:

| 错误码                     | HTTP | 说明                                        |
| -------------------------- | ---- | ------------------------------------------- |
| `SSO_PROVIDER_UNAVAILABLE` | 503  | OIDC discovery 拉不到（Entra 5xx 或超时）   |

**业务规则**:

- 不携带任何登录态即可访问；已登录用户也可重新发起 SSO
- 必须使用 `AZURE_TENANT_ID`（必须 GUID，启动期 regex 校验） / `AZURE_CLIENT_ID` / `AZURE_REDIRECT_URI`（env 详见 [06-data-model.md - env 变量约定](./06-data-model.md#env-变量约定v24-起)）
- `redirect` 参数白名单复用 frontend `frontend/src/lib/auth-redirect.ts` `isSafeRelativePath` 规则（**以 `/` 开头且不以 `//` 开头**，且不以 `/login` 开头），非法则回落到 `/overview`（`DEFAULT_POST_LOGIN_PATH`），不向 Entra 透传任意外部 URL
- **PKCE 必须**（confidential client 也强制）：`code_verifier` 43-128 字符 base64url 随机串；`code_challenge = base64url(SHA256(code_verifier))`；`code_challenge_method=S256`
- OIDC 库强制要求 `openid-client`（详见 [03-architecture.md](./03-architecture.md#oidc-实现库)）

---

#### GET /auth/sso/callback

Entra ID OIDC 授权回调，完成 token 交换、绑定/JIT 建账号、签发本系统 JWT（v2.4）。

**权限**: 公开接口（`@Public()` + `@Auditable()`，所有成功/失败分支都写一条 audit）

**Query 参数**:

| 参数                | 类型   | 必需 | 说明                                                  |
| ------------------- | ------ | ---- | ----------------------------------------------------- |
| `code`              | string | 是   | Entra 返回的 authorization code，用于换 token         |
| `state`             | string | 是   | 必须与 cookie 中 `sso_state` 一致，否则 401           |
| `error`             | string | 否   | Entra 报错时返回，通常与 `error_description` 同时存在 |
| `error_description` | string | 否   | Entra 错误描述                                        |

**处理流程**（按顺序；**注释**：步骤 5-7 有写副作用，重入由 Entra `code` 一次性 + audit `entraTid + oid + when` 区分；同 code 重放 → token endpoint 返 `invalid_grant` → 401 `SSO_TOKEN_INVALID`，audit 单独留痕）:

**0. Entra error query 早返**（步骤 1 之前）

callback 命中 `query.error` 非空时按映射立即 302（不读 cookie / 不查 DB）：

| `query.error` | 错误码 | HTTP | 302 Location |
|---|---|---|---|
| `access_denied` | `SSO_USER_CANCELLED` | 403 | `/login?ssoError=SSO_USER_CANCELLED` |
| `consent_required` / `interaction_required` | `SSO_CONSENT_REQUIRED` | 403 | `/login?ssoError=SSO_CONSENT_REQUIRED` |
| 其它 | `SSO_PROVIDER_REJECTED` | 502 | `/login?ssoError=SSO_PROVIDER_REJECTED` |

每条都写一条 audit（`action='LOGIN_FAILED'`，`why=<错误码>`，actor=匿名 / IP）。

**1. state 校验**

校验 `query.state` == cookie `sso_state`，不一致 / 缺失 → 401 `SSO_TOKEN_INVALID`

**2. token 换取**

用 `code` + cookie `sso_code_verifier` + `client_secret` 向 Entra token endpoint POST 换 ID token + access token（5s 超时）。Entra 返 `invalid_grant`（code 已用 / 过期） → 401 `SSO_TOKEN_INVALID`；返 5xx / 超时 → 503 `SSO_PROVIDER_UNAVAILABLE`。**openid-client 自动携带 `code_verifier`**。

**3. ID token 校验**（多子项）

- **3a. 签名**：JWKS 5min 缓存（openid-client 默认）；`kid` 未命中 → 立即清缓存重拉一次（同 kid 多次 miss 加 30s cooldown 防 thundering herd）
- **3b. issuer**：从 ID token 取 `tid` claim，用 discovery `metadata.issuer` 模板里 `{tenantid}` 替换后**严格字符串等于**比对（不硬编码 `https://login.microsoftonline.com/{tenant}/v2.0`）
- **3c. audience**：`aud === AZURE_CLIENT_ID`
- **3d. clock skew**：`exp` / `nbf` / `iat` 容忍 **±5min**（openid-client 默认）
- **3e. nonce**：ID token `nonce` claim === cookie `sso_nonce`

任一失败 → 401 `SSO_TOKEN_INVALID`。

**4. email 取值**

取 `email` claim → 应用层 lower-case；缺失 → 400 `SSO_EMAIL_MISSING`（**不**用 `preferred_username` / `upn` fallback）

**5. 用户匹配 / JIT / 绑定回填**（事务内，`prisma.$transaction()`）

用 lower-case email 查 `User WHERE deletedAt IS NULL`：

- **5a. 命中 + `externalId IS NULL`** → CAS UPDATE `WHERE id=$1 AND externalId IS NULL`，回填 `externalId = oid` + `externalSource = 'entra'`；受影响行 = 0（并发场景）→ 重查走 5b 或 5c
- **5b. 命中 + `externalSource='entra'` + `externalId === oid`** → 直接登录（已绑定）
- **5c. 命中 + `externalSource='entra'` + `externalId !== oid`** → 409 `SSO_BINDING_CONFLICT`，audit `SSO_BINDING_CONFLICT`
- **5d. 命中 + `externalSource='ldap'` + `externalId !== oid`** → **覆盖**为 `externalId = oid` + `externalSource = 'entra'`，audit `SSO_BINDING_UPGRADED_FROM_LDAP`
- **5e. 未命中** → 域名校验：
  - 不在 `SSO_ALLOWED_DOMAINS` → 403 `SSO_DOMAIN_NOT_ALLOWED`
  - 在白名单 → **运行时再查默认 org**（`prisma.organization.findUnique({where:{id:env.SSO_JIT_DEFAULT_ORG_ID, deletedAt:null}})`），缺失 → 503 `SSO_PROVIDER_UNAVAILABLE`
  - JIT 创建：`prisma.user.upsert({where:{email}, create:{...}, update:{}})` 并捕获 `P2002`（unique violation） → 重查走已存在分支
    - 字段：`username = lower(email)` / `email = lower(email)` / `passwordHash = null` / `source = ENTRA` / `externalId = oid` / `externalSource = 'entra'` / `status = ACTIVE` / `region = ${默认 org 的 region 或 'CN'}`
    - 关系：建 1 行 `UserRole { userId, roleId = Employee.id, organizationId = SSO_JIT_DEFAULT_ORG_ID }`
    - **不建** `UserDepartment` 行（部门由 admin 后续手工分配）
    - audit `SSO_JIT_CREATED`

**6. 状态检查**

`user.status === 'ACTIVE'` 否则 403 `IAM_USER_SUSPENDED`（**复用现有错误码**，不自创 `AUTH_USER_DISABLED`）

**7. audit `SSO_LOGIN_SUCCESS`**（事务内，metadata 含 `userId` / `email` / `externalId` / `path: existing|jit|binding_filled|ldap_upgraded` / `entraTid`）

**8. 事务提交 → JWT 签发**

事务提交后用现有 `JwtService` 签发 Access 24h / Refresh 7d。事务任何 step 失败（如 P2002 + 重查仍冲突，或外键约束失败） → 503 `SSO_PROVIDER_UNAVAILABLE`。

**9. callback 响应协议（推翻早期 JSON 响应体方案）**

| 路径 | 响应 |
|---|---|
| **成功** | `HTTP 302` + `Location: ${sso_redirect}#accessToken=<jwt>&refreshToken=<jwt>`（URL fragment 注入；默认 `sso_redirect='/overview'`） |
| **失败** | `HTTP 302` + `Location: /login?ssoError=<CODE>` |

**强约定**：

- **不**使用 JSON 响应体（302 + body 浏览器不暴露给 JS，浏览器只读 Location 头）
- **不**使用 HttpOnly Cookie 存 JWT（不兼容现有 localStorage 存 token 的 password 登录路径）
- fragment 注入后清 4 个 `sso_*` cookie（`Max-Age=0`）
- 前端 `/sso/landing` 路由读 `location.hash` → 写 localStorage → `history.replaceState` 清 hash → 跳业务页；详见 [05-ui-interaction-spec.md - SSO 登录](./05-ui-interaction-spec.md#2-sso-登录v24-新增--主路径)

**成功响应** (302):

```http
HTTP/1.1 302 Found
Location: /overview#accessToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...&refreshToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Set-Cookie: sso_state=; Max-Age=0; Path=/
Set-Cookie: sso_nonce=; Max-Age=0; Path=/
Set-Cookie: sso_redirect=; Max-Age=0; Path=/
Set-Cookie: sso_code_verifier=; Max-Age=0; Path=/
```

**失败响应** (302):

```http
HTTP/1.1 302 Found
Location: /login?ssoError=SSO_BINDING_CONFLICT
Set-Cookie: sso_state=; Max-Age=0; Path=/
Set-Cookie: sso_nonce=; Max-Age=0; Path=/
Set-Cookie: sso_redirect=; Max-Age=0; Path=/
Set-Cookie: sso_code_verifier=; Max-Age=0; Path=/
```

**错误响应**:

| 错误码                     | HTTP | 说明                                                                  |
| -------------------------- | ---- | --------------------------------------------------------------------- |
| `SSO_TOKEN_INVALID`        | 401  | ID token signature/iss/aud/nonce/exp 任一不通过；state 不匹配；Entra `invalid_grant`（code 已用 / 过期） |
| `SSO_EMAIL_MISSING`        | 400  | OIDC token 缺 `email` claim（Entra app 未授予 email scope / B2B guest） |
| `SSO_BINDING_CONFLICT`     | 409  | `externalSource='entra'` 且 `externalId` ≠ token `oid`（LDAP 不触发，走 9e 升级路径） |
| `SSO_DOMAIN_NOT_ALLOWED`   | 403  | JIT 建账号时 email 域名不在 `SSO_ALLOWED_DOMAINS` 白名单               |
| `SSO_PROVIDER_UNAVAILABLE` | 503  | token endpoint 5xx / 超时（5s）；DB 事务失败；运行时 `SSO_JIT_DEFAULT_ORG_ID` 缺失 |
| `SSO_USER_CANCELLED`       | 403  | Entra callback `query.error=access_denied`                            |
| `SSO_CONSENT_REQUIRED`     | 403  | Entra callback `query.error=consent_required` / `interaction_required` |
| `SSO_PROVIDER_REJECTED`    | 502  | Entra callback `query.error` 为其它值                                 |
| `IAM_USER_SUSPENDED`       | 403  | User 命中但 `status !== ACTIVE`（复用现有错误码）                     |

> 完整错误码定义、触发场景、修复建议见 [08-error-codes.md - 认证 / SSO 错误码](./08-error-codes.md#认证--sso-错误码-auth-sso---110)。

**业务规则**:

- **本系统 JWT 自治**：本系统 JWT 完全由现有 JWT secret 自签，**不存储也不转发** Entra access/refresh token；Entra 仅承担"证明你是谁"的职责；JWT secret rotation 对 SSO 流程透明
- **`source` 字段语义不变**：SSO 登录成功不修改既有用户的 `source`（语义是"用户记录的创建/同步来源"，不是"当前登录方式"）；JIT 创建的用户 `source = ENTRA`
- **`externalId` / `externalSource` 回填 / LDAP 升级**：见 [06-data-model.md - 字段语义扩展](./06-data-model.md#sso-相关字段语义扩展v24-起)
- **本期 schema 增量**：1 个 prisma migration（`AuditAction` enum +5 / email lower-case backfill），见 [06-data-model.md - 本期 schema 增量声明](./06-data-model.md#本期-schema-增量声明v24)
- **5 个 audit 事件 metadata 字段**：
  - `SSO_LOGIN_SUCCESS`：`userId` / `email` / `externalId` / `path: existing|jit|binding_filled|ldap_upgraded` / `entraTid`
  - `SSO_JIT_CREATED`：`userId` / `email` / `externalId` / `defaultOrgId` / `entraTid`
  - `SSO_BINDING_FILLED`：`userId` / `email` / `externalId` / `previousExternalId: null` / `entraTid`
  - `SSO_BINDING_UPGRADED_FROM_LDAP`：`userId` / `email` / `previousExternalId: <LDAP_DN>` / `newExternalId: <oid>` / `entraTid`
  - `SSO_BINDING_CONFLICT`：`email` / `existingExternalId` / `attemptedExternalId` / `entraTid`

---

### 2. 用户管理接口

#### GET /users

获取用户列表，支持分页、搜索和筛选。

**权限**: `user:read`

**Query 参数**:

| 参数           | 类型   | 必填 | 说明                                                     |
| -------------- | ------ | ---- | -------------------------------------------------------- |
| `page`         | number | 否   | 页码，默认 1                                             |
| `pageSize`     | number | 否   | 每页数量，默认 20，最大 100                              |
| `keyword`      | string | 否   | 搜索关键词（匹配 displayName/email/employeeId/username） |
| `status`       | string | 否   | 状态筛选：ACTIVE/INACTIVE/SUSPENDED/TERMINATED           |
| `departmentId` | string | 否   | 部门 ID 筛选                                             |
| `region`       | string | 否   | 区域筛选：CN/US/UAE                                      |
| `sortBy`       | string | 否   | 排序字段，默认 createdAt                                 |
| `sortOrder`    | string | 否   | 排序方向：asc/desc，默认 desc                            |

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440000",
        "username": "john.doe",
        "email": "john.doe@ff.com",
        "displayName": "John Doe",
        "status": "ACTIVE",
        "source": "LDAP",
        "region": "CN",
        "department": {
          "id": "uuid",
          "name": "Engineering",
          "code": "ENG"
        },
        "position": {
          "id": "uuid",
          "name": "Senior Engineer",
          "level": 5
        },
        "manager": {
          "id": "uuid",
          "displayName": "Jane Smith"
        },
        "ldapSyncedAt": "2025-12-01T00:00:00.000Z",
        "createdAt": "2025-01-15T08:00:00.000Z"
      }
    ],
    "total": 156,
    "page": 1,
    "limit": 20,
    "totalPages": 8,
    "hasNext": true,
    "hasPrev": false
  },
  "message": "Users fetched successfully",
  "timestamp": "2025-12-07T10:30:00.000Z",
  "path": "/api/v1/users"
}
```

> **说明**：`department`、`position`、`manager` 字段返回用户的主部门（`UserDepartment.isPrimary = true`）信息。如需获取用户所有部门归属，请使用多部门管理 API。

---

#### GET /users/me

获取当前登录用户的详细信息。

**权限**: 已登录

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "john.doe",
    "email": "john.doe@ff.com",
    "displayName": "John Doe",
    "phone": "+86 138****1234",
    "avatar": "https://example.com/avatars/john.jpg",
    "status": "ACTIVE",
    "source": "LDAP",
    "region": "CN",
    "employeeId": "EMP001",
    "department": {
      "id": "uuid",
      "name": "Engineering",
      "code": "ENG"
    },
    "position": {
      "id": "uuid",
      "name": "Senior Engineer",
      "level": 5
    },
    "manager": {
      "id": "uuid",
      "displayName": "Jane Smith",
      "email": "jane.smith@ff.com"
    },
    "roles": [{ "id": "uuid", "name": "开发人员", "code": "DEVELOPER" }],
    "permissions": ["user:read", "department:read", "part:read", "part:create"],
    "hiredAt": "2024-01-15T00:00:00.000Z",
    "createdAt": "2024-01-15T08:00:00.000Z"
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

---

#### GET /users/:id

获取指定用户的详细信息。

**权限**: `user:read`

> 💡 **说明**：普通用户查看自己的信息请使用 `GET /users/me`；查看他人信息需要 `user:read` 权限（通常是 HR/管理员）。

**路径参数**:

| 参数 | 类型   | 说明           |
| ---- | ------ | -------------- |
| `id` | string | 用户 ID (UUID) |

**成功响应** (200):

与 `GET /users/me` 响应结构相同。

**错误响应**:

| 错误码      | HTTP | 说明       |
| ----------- | ---- | ---------- |
| `NOT_FOUND` | 404  | 用户不存在 |

---

#### POST /users

创建本地用户。

**权限**: `user:create`

**请求体**:

```typescript
interface CreateUserDto {
  username: string; // 用户名（唯一）
  email: string; // 邮箱（唯一）
  source?: "LOCAL" | "LDAP"; // v2.1.1: 身份源（默认 LOCAL）
  password?: string; // 密码（LOCAL用户必填，8位以上，至少2种字符类型）
  ldapUsername?: string; // v2.1.1: LDAP用户名（LDAP用户必填，对应 sAMAccountName）
  displayName: string; // 显示名称
  phone?: string; // 手机号
  employeeId?: string; // 员工编号（唯一，允许为空）
  departmentId: string; // v2.0: 主部门 ID（必需，通过 UserDepartment 管理）
  organizationId?: string; // v2.0: 组织 ID（可选，可从 departmentId 推导）
  positionId?: string; // 岗位 ID（通过 UserDepartment 管理）
  managerId?: string; // 直属上级 ID（通过 UserDepartment 管理，必须是同部门成员）
  region?: string; // 区域，不填则从组织继承或使用默认值
  hiredAt?: string; // 入职日期
}
```

> **说明（v2.0）**：
>
> - `departmentId` 必需，创建用户时会自动创建主部门关联（UserDepartment）
> - `organizationId` 可选，如果未提供，系统会从 `departmentId` 对应的部门中推导出 `organizationId`
>   - **如果提供了 `organizationId`，必须与 `departmentId` 所属的组织一致，否则返回验证错误**
>   - **如果不提供 `organizationId`，系统自动从 `departmentId` 推导**
> - `positionId`、`managerId` 通过 `UserDepartment` 表管理
> - `region` 不填时从部门所属组织继承（通过 OrganizationRegion 关联），如果用户没有部门归属则使用默认值 'CN'（region 为冗余字段，用于性能优化）

> **v2.1.1 身份源说明**：
>
> - `source` 默认为 `LOCAL`，可选 `LDAP`（Entra 同步用户只能通过同步创建）
> - **LOCAL 用户**：
>   - `password` 字段必填
>   - `ldapUsername` 字段不需要
> - **LDAP 用户**：
>   - `password` 字段不需要（留空）
>   - `ldapUsername` 字段必填（对应 AD 的 sAMAccountName）
>   - 认证由 LDAP/AD 服务器处理
> - **LDAP（Entra 同步）用户**：
>   - 不能通过此 API 创建，只能通过同步功能创建
>   - 用于信息同步，认证走 LDAP/AD

**示例请求（LOCAL 用户）**:

```json
{
  "username": "john.doe",
  "email": "john.doe@ff.com",
  "source": "LOCAL",
  "password": "SecurePass123!",
  "displayName": "John Doe",
  "phone": "+86 13812345678",
  "employeeId": "EMP001",
  "departmentId": "550e8400-e29b-41d4-a716-446655440001",
  "positionId": "550e8400-e29b-41d4-a716-446655440002",
  "managerId": "550e8400-e29b-41d4-a716-446655440003",
  "region": "CN",
  "hiredAt": "2025-01-15T00:00:00.000Z"
}
```

**示例请求（LDAP 用户，v2.1.1 新增）**:

````json
{
  "username": "zhang.san",
  "email": "zhang.san@ff.com",
  "source": "LDAP",
  "ldapUsername": "zhangsan",
  "displayName": "张三",
  "phone": "+86 13898765432",
  "employeeId": "EMP002",
  "departmentId": "550e8400-e29b-41d4-a716-446655440001",
  "positionId": "550e8400-e29b-41d4-a716-446655440002",
  "region": "CN"
}

**成功响应** (201):

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "john.doe",
    "email": "john.doe@ff.com",
    "displayName": "John Doe",
    "status": "ACTIVE",
    "source": "LOCAL",
    "createdAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "用户创建成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
````

**错误响应**:

| 错误码                          | HTTP | 说明               |
| ------------------------------- | ---- | ------------------ |
| `IAM_USER_EMAIL_EXISTS`         | 409  | 邮箱已被使用       |
| `IAM_USERNAME_EXISTS`           | 409  | 用户名已被使用     |
| `IAM_EMPLOYEE_ID_EXISTS`        | 409  | 员工编号已被使用   |
| `IAM_MANAGER_NOT_IN_DEPARTMENT` | 400  | 上级不是该部门成员 |
| `VALIDATION_ERROR`              | 400  | 参数验证失败       |

> 密码复杂度与汇报关系校验的错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

**业务规则**:

- 用户创建后默认状态为 `ACTIVE`
- 用户创建后来源标记为 `LOCAL`
- 新用户不会自动分配任何角色，需管理员手动分配
- `managerId` 会进行环路检测，防止循环汇报关系
- `employeeId` 允许为空，但有值时必须唯一

---

#### PATCH /users/:id

更新用户信息。

**权限**: `user:update`

**路径参数**:

| 参数 | 类型   | 说明           |
| ---- | ------ | -------------- |
| `id` | string | 用户 ID (UUID) |

**请求体**（所有字段可选）:

```typescript
interface UpdateUserDto {
  displayName?: string;
  email?: string;
  phone?: string;
  employeeId?: string;
  departmentId?: string | null; // 通过 UserDepartment 管理
  positionId?: string | null; // 通过 UserDepartment 管理
  managerId?: string | null; // 通过 UserDepartment 管理
  region?: string;
  status?: UserStatus;
}
```

> **说明**：更新 `departmentId`、`positionId`、`managerId` 会同步更新用户的主部门（`UserDepartment.isPrimary = true`）记录。

**示例请求**:

```json
{
  "displayName": "John Doe Updated",
  "departmentId": "550e8400-e29b-41d4-a716-446655440001",
  "status": "ACTIVE"
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "displayName": "John Doe Updated",
    "updatedAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "用户更新成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 更新 `managerId` 时会进行环路检测
- 更新 `status` 为 `TERMINATED` 时会触发会话终止
- Entra 同步来源的用户，部分字段（displayName、email、职位）会在下次同步时被覆盖
- 部门归属和汇报关系由本地管理员维护，不会被 Entra 同步覆盖

---

#### DELETE /users/:id

软删除用户。

**权限**: `user:delete`

**路径参数**:

| 参数 | 类型   | 说明           |
| ---- | ------ | -------------- |
| `id` | string | 用户 ID (UUID) |

**成功响应** (200):

```json
{
  "success": true,
  "data": null,
  "message": "用户删除成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 软删除：设置 `deletedAt` 字段，不物理删除
- 删除的用户无法登录
- 历史数据（审批记录等）中保留用户身份快照

---

#### GET /users/:id/permissions

获取用户的聚合权限列表。

**权限**: 已登录

**Query 参数（v2.1）**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `organizationId` | string | 否 | 组织上下文，不填则返回所有权限（全局+所有组织） |

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "userId": "550e8400-e29b-41d4-a716-446655440000",
    "organizationId": null,
    "permissions": [
      "user:read",
      "department:read",
      "part:read",
      "part:create",
      "part:update"
    ],
    "roles": [
      { "id": "uuid", "code": "DEVELOPER", "name": "开发人员" },
      { "id": "uuid", "code": "PART_MANAGER", "name": "零件管理员" }
    ]
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 权限是用户所有**启用**角色权限的并集
- 禁用状态的角色不参与权限聚合
- 结果可能被缓存，角色变更时自动刷新

**v2.1 组织上下文（可选）**:

- 如果提供 `organizationId` 查询参数，返回：全局权限（organizationId=null）+ 该组织权限的并集，相同权限点自动去重
- 如果不提供 `organizationId`，返回：所有全局权限 + 所有组织权限的并集，相同权限点自动去重

---

#### GET /users/:id/subordinates

获取用户的下属列表（直接汇报关系）。

**权限**: 已登录（可查看自己的下属）或 `user:read`

**路径参数**:

| 参数 | 类型   | 说明           |
| ---- | ------ | -------------- |
| `id` | string | 用户 ID (UUID) |

**Query 参数**:

| 参数              | 类型    | 必填 | 说明                                           |
| ----------------- | ------- | ---- | ---------------------------------------------- |
| `departmentId`    | string  | 否   | 部门 ID，如果提供则只返回该部门内的下属        |
| `includeIndirect` | boolean | 否   | 是否包含间接下属（默认 false，只返回直接下属） |
| `page`            | number  | 否   | 页码（默认 1）                                 |
| `pageSize`        | number  | 否   | 每页数量（默认 20，最大 100）                  |

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "managerId": "550e8400-e29b-41d4-a716-446655440000",
    "managerName": "John Doe",
    "total": 5,
    "directSubordinates": 3,
    "indirectSubordinates": 2,
    "items": [
      {
        "id": "550e8400-e29b-41d4-a716-446655440001",
        "displayName": "Alice Smith",
        "username": "alice.smith",
        "email": "alice.smith@ff.com",
        "department": {
          "id": "dept-uuid",
          "name": "技术部",
          "code": "TECH"
        },
        "position": {
          "id": "pos-uuid",
          "name": "高级工程师",
          "code": "SENIOR_ENGINEER"
        },
        "isDirect": true,
        "subordinateCount": 2
      },
      {
        "id": "550e8400-e29b-41d4-a716-446655440002",
        "displayName": "Bob Johnson",
        "username": "bob.johnson",
        "email": "bob.johnson@ff.com",
        "department": {
          "id": "dept-uuid",
          "name": "技术部",
          "code": "TECH"
        },
        "position": {
          "id": "pos-uuid",
          "name": "工程师",
          "code": "ENGINEER"
        },
        "isDirect": true,
        "subordinateCount": 0
      }
    ],
    "pagination": {
      "page": 1,
      "pageSize": 20,
      "total": 5,
      "totalPages": 1
    }
  },
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

**业务规则**:

- 下属关系基于 `UserDepartment.managerId` 字段
- 如果提供 `departmentId`，只返回该部门内的下属（用户在该部门的汇报关系）
- `includeIndirect=true` 时，会递归查找所有层级的间接下属
- 如果用户没有下属，返回空数组

**错误响应**:

| 错误码               | HTTP | 说明                   |
| -------------------- | ---- | ---------------------- |
| `IAM_USER_NOT_FOUND` | 404  | 用户不存在             |
| `IAM_FORBIDDEN`      | 403  | 无权限查看该用户的下属 |

---

#### POST /users/:id/roles

为用户分配角色（v2.1 支持组织级角色）。

**权限**: `role:manage`

**请求体**:

```typescript
// v2.1 推荐：单个角色分配项
interface RoleAssignmentDto {
  roleId: string; // 角色 ID
  organizationId?: string | null; // v2.1: 组织 ID，null 表示全局角色
}

// v2.1 推荐：批量分配（支持组织级角色）
interface AssignRoleDto {
  assignments: RoleAssignmentDto[];
}

// 向后兼容：批量分配（默认为全局角色）
interface LegacyAssignRolesDto {
  roleIds: string[]; // 角色 ID 数组（将分配为全局角色）
}
```

**示例请求（v2.1 推荐）**:

```json
{
  "assignments": [
    {
      "roleId": "role-hr-uuid",
      "organizationId": "org-ff-china" // 在 FF China 是 HR
    },
    {
      "roleId": "role-employee-uuid",
      "organizationId": "org-ff-usa" // 在 FF USA 是普通员工
    },
    {
      "roleId": "role-auditor-uuid",
      "organizationId": null // 全局审计员（跨所有组织）
    }
  ]
}
```

> **DTO 类型说明**:
>
> - `RoleAssignmentDto`: 单个角色分配项（roleId + organizationId）
> - `AssignRoleDto`: 请求体，包含 `assignments: RoleAssignmentDto[]`

**示例请求（向后兼容）**:

```json
{
  "roleIds": [
    "550e8400-e29b-41d4-a716-446655440001",
    "550e8400-e29b-41d4-a716-446655440002"
  ]
}
// 注意：此方式将角色分配为全局角色（organizationId=null）
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "userId": "550e8400-e29b-41d4-a716-446655440000",
    "assignedRoles": [
      {
        "roleId": "role-hr-uuid",
        "roleName": "HR管理员",
        "organizationId": "org-ff-china",
        "organizationName": "FF China",
        "isGlobal": false
      },
      {
        "roleId": "role-auditor-uuid",
        "roleName": "审计员",
        "organizationId": null,
        "organizationName": null,
        "isGlobal": true
      }
    ]
  },
  "message": "角色分配成功",
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

**业务规则（v2.1 更新）**:

- 分配角色时必须指定组织上下文（`organizationId`），除非是全局角色
- 用户在同一组织只能被分配一次相同角色
- 用户可以在不同组织拥有不同角色
- 分配角色后立即生效
- 系统会自动清理该用户的权限缓存
- 此操作会记录审计日志

---

#### POST /users/:id/terminate

将用户状态设置为离职（TERMINATED）。

**权限**: `user:update`

**请求体**（可选）:

```typescript
interface TerminateUserDto {
  reason?: string; // 离职原因
  terminatedAt?: string; // 离职日期，默认当前时间
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "userId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "TERMINATED",
    "terminatedAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "用户已离职",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 用户状态变更为 `TERMINATED`
- 立即终止用户的所有会话（Token 加入黑名单）
- 清理用户的权限缓存
- 后续 Entra 同步不会自动复活此用户
- 记录状态变更审计日志

---

#### POST /users/:id/activate

将用户状态激活为 `ACTIVE`。

**权限**: `user:update`

**请求体**（可选）:

```typescript
interface ActivateUserDto {
  reason?: string; // 激活原因
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "userId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "ACTIVE",
    "activatedAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "用户已激活",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`（用户状态相关错误）。

**业务规则**:

- 仅支持从 `INACTIVE` 或 `SUSPENDED` 状态激活为 `ACTIVE`
- ⚠️ **`TERMINATED` 用户不能通过此接口恢复**，需通过特权操作（如超级管理员界面或手动数据库操作）
- 激活后用户可以正常登录
- 记录状态变更审计日志

---

### 3. 部门管理接口

#### GET /departments

获取部门列表。

**权限**: 已登录

**Query 参数**:

| 参数       | 类型   | 必填 | 说明                         |
| ---------- | ------ | ---- | ---------------------------- |
| `keyword`  | string | 否   | 搜索关键词（匹配 name/code） |
| `region`   | string | 否   | 按 primaryRegion 筛选        |
| `parentId` | string | 否   | 父部门 ID 筛选               |

**成功响应** (200):

```json
{
  "success": true,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Engineering",
      "code": "ENG",
      "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" },
      "operatingRegions": [
        { "id": "uuid", "code": "CN", "name": "中国" },
        { "id": "uuid", "code": "US", "name": "美国" }
      ],
      "parentId": null,
      "head": {
        "id": "uuid",
        "displayName": "Jane Smith"
      },
      "order": 1,
      "createdAt": "2025-01-01T00:00:00.000Z"
    }
  ],
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

---

#### GET /departments/tree

获取部门树形结构。

**权限**: 已登录

**成功响应** (200):

```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "name": "公司总部",
      "code": "HQ",
      "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" },
      "head": { "id": "uuid", "displayName": "CEO" },
      "employeeCount": 150,
      "children": [
        {
          "id": "uuid",
          "name": "Engineering",
          "code": "ENG",
          "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" },
          "head": { "id": "uuid", "displayName": "Jane Smith" },
          "employeeCount": 45,
          "children": [
            {
              "id": "uuid",
              "name": "Backend Team",
              "code": "ENG_BACKEND",
              "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" },
              "employeeCount": 12,
              "children": []
            },
            {
              "id": "uuid",
              "name": "Frontend Team",
              "code": "ENG_FRONTEND",
              "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" },
              "employeeCount": 10,
              "children": []
            }
          ]
        },
        {
          "id": "uuid",
          "name": "HR",
          "code": "HR",
          "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" },
          "head": { "id": "uuid", "displayName": "HR Manager" },
          "employeeCount": 8,
          "children": []
        }
      ]
    }
  ],
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

---

#### POST /departments

创建部门。

**权限**: `department:create`

**请求体**:

```typescript
interface CreateDepartmentDto {
  name: string; // 部门名称
  code: string; // 部门编码（组织内唯一）
  organizationId: string; // v2.0: 组织 ID（必需）
  parentId?: string; // 父部门 ID（必填，顶级部门由组织创建时自动生成）
  headId?: string; // 部门主管 ID
  description?: string; // 描述
  order?: number; // 排序序号
  // v2.0 已移除：primaryRegionId, operatingRegionIds（部门从组织继承区域）
}
```

**示例请求**:

```json
{
  "organizationId": "org-001",
  "name": "QA Team",
  "code": "ENG_QA",
  "parentId": "550e8400-e29b-41d4-a716-446655440001",
  "headId": "550e8400-e29b-41d4-a716-446655440002",
  "description": "Quality Assurance Team",
  "order": 3
}
```

**成功响应** (201):

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "QA Team",
    "code": "ENG_QA",
    "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" },
    "createdAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "部门创建成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码                       | HTTP | 说明           |
| ---------------------------- | ---- | -------------- |
| `IAM_DEPARTMENT_CODE_EXISTS` | 409  | 部门编码已存在 |

**业务规则（v2.0）**:

- `organizationId` 必需，所有部门必须归属一个组织
- `code` 在组织内唯一（v2.0 架构变更）
- 部门的区域信息从所属组织继承（通过 OrganizationRegion 关联）
- 顶级部门由组织创建时自动生成，不允许手动创建

---

#### DELETE /departments/:id

删除部门（软删除）。

**权限**: `department:delete`

**删除条件**:

- 部门没有子部门（`deletedAt = null` 的子部门）
- 部门没有在职成员（`leftAt = null` 的成员）

**成功响应** (200):

```json
{
  "success": true,
  "data": null,
  "message": "部门删除成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码                        | HTTP | 说明                     |
| ----------------------------- | ---- | ------------------------ |
| `IAM_DEPARTMENT_HAS_CHILDREN` | 400  | 部门下有子部门，无法删除 |
| `IAM_DEPARTMENT_HAS_USERS`    | 400  | 部门下有成员，无法删除   |

---

### 3.1 部门管理扩展接口

#### GET /departments/organizations

获取顶级部门列表（用于视角选择，包含所属组织信息）。

**权限**: 已登录

**Query 参数**:

| 参数       | 类型   | 必填 | 说明       |
| ---------- | ------ | ---- | ---------- |
| `regionId` | string | 否   | 按区域过滤 |

**成功响应** (200):

```json
{
  "success": true,
  "data": [
    {
      "id": "dept-ff-china-root",
      "code": "FF_CHINA",
      "name": "Faraday Future China",
      "parentId": null,
      "organizationId": "org-ff-china",
      "organization": {
        "id": "org-ff-china",
        "code": "FF_CHINA",
        "name": "Faraday Future China",
        "primaryRegionId": "region-cn"
      }
    },
    {
      "id": "dept-ff-usa-root",
      "code": "FF_USA",
      "name": "Faraday Future USA",
      "parentId": null,
      "organizationId": "org-ff-usa",
      "organization": {
        "id": "org-ff-usa",
        "code": "FF_USA",
        "name": "Faraday Future USA",
        "primaryRegionId": "region-us"
      }
    }
  ],
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 返回所有顶级部门（`parentId = null`），并带上所属组织信息
- 前端视角选择器使用 `organizationId` 作为组织上下文
- 如果提供 `regionId`，只返回所属组织主区域匹配的顶级部门

---

#### POST /departments/batch

批量创建部门。

**权限**: `department:create`

**请求体**:

```typescript
interface BatchCreateDepartmentDto {
  departments: Array<{
    name: string; // 部门名称
    code: string; // 部门代码（唯一）
    organizationId: string; // 组织 ID
    parentId?: string | null; // 父部门 ID（null 表示顶级部门）
    headId?: string; // 部门负责人 ID
    description?: string; // 部门描述
  }>;
}
```

**示例请求**:

```json
{
  "departments": [
    {
      "name": "技术部",
      "code": "TECH",
      "organizationId": "org-ff-china",
      "parentId": null
    },
    {
      "name": "前端组",
      "code": "FRONTEND",
      "organizationId": "org-ff-china",
      "parentId": "tech-dept-id"
    }
  ]
}
```

**成功响应** (201):

```json
{
  "success": true,
  "data": {
    "created": 2,
    "failed": 0,
    "departments": [
      {
        "id": "uuid-1",
        "name": "技术部",
        "code": "TECH"
      },
      {
        "id": "uuid-2",
        "name": "前端组",
        "code": "FRONTEND"
      }
    ]
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 批量创建部门，支持创建多个部门及其层级关系
- 如果某个部门创建失败，会继续创建其他部门
- 返回成功和失败的统计信息

---

#### POST /departments/:id/members

添加单个成员到部门。

**权限**: `department:update`

**请求体**:

```typescript
interface AddDepartmentMemberDto {
  userId: string; // 用户 ID
  positionId?: string; // 岗位 ID
  managerId?: string; // 直属上级 ID（必须是该部门成员）
  title?: string; // 职位名称
  isPrimary?: boolean; // 是否为主部门（默认 false）
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "userId": "user-uuid",
    "departmentId": "dept-uuid",
    "isPrimary": false,
    "positionId": "position-uuid",
    "managerId": "manager-uuid"
  },
  "message": "成员添加成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码                           | HTTP | 说明             |
| -------------------------------- | ---- | ---------------- |
| `IAM_USER_ALREADY_IN_DEPARTMENT` | 409  | 用户已属于该部门 |

---

#### POST /departments/:id/members/batch

批量添加成员到部门。

**权限**: `department:update`

**请求体**:

```typescript
interface AddDepartmentMembersDto {
  members: Array<{
    userId: string; // 用户 ID
    positionId?: string; // 岗位 ID
    managerId?: string; // 直属上级 ID
    title?: string; // 职位名称
    isPrimary?: boolean; // 是否为主部门
  }>;
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "added": 5,
    "failed": 1,
    "errors": [
      {
        "userId": "user-uuid",
        "error": "用户已属于该部门"
      }
    ]
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 批量添加成员，支持一次添加多个用户到部门
- 如果某个用户添加失败，会继续添加其他用户
- 返回成功和失败的统计信息

---

#### GET /departments/:id/stats

获取部门的员工统计信息。

**权限**: 已登录

**路径参数**:

| 参数 | 类型   | 说明           |
| ---- | ------ | -------------- |
| `id` | string | 部门 ID (UUID) |

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "departmentId": "550e8400-e29b-41d4-a716-446655440000",
    "departmentName": "技术部",
    "departmentCode": "TECH",
    "organizationId": "org-ff-china",
    "organizationName": "FF China",
    "totalEmployees": 45,
    "activeEmployees": 42,
    "inactiveEmployees": 2,
    "suspendedEmployees": 1,
    "terminatedEmployees": 0,
    "byPosition": [
      {
        "positionId": "pos-engineer-uuid",
        "positionName": "工程师",
        "positionCode": "ENGINEER",
        "count": 30
      },
      {
        "positionId": "pos-manager-uuid",
        "positionName": "经理",
        "positionCode": "MANAGER",
        "count": 5
      },
      {
        "positionId": "pos-senior-uuid",
        "positionName": "高级工程师",
        "positionCode": "SENIOR_ENGINEER",
        "count": 10
      }
    ],
    "byStatus": {
      "ACTIVE": 42,
      "INACTIVE": 2,
      "SUSPENDED": 1,
      "TERMINATED": 0
    },
    "subDepartments": [
      {
        "departmentId": "dept-frontend-uuid",
        "departmentName": "前端组",
        "employeeCount": 15
      },
      {
        "departmentId": "dept-backend-uuid",
        "departmentName": "后端组",
        "employeeCount": 20
      }
    ],
    "subDepartmentCount": 2,
    "lastUpdated": "2025-12-26T10:30:00.000Z"
  },
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

**业务规则**:

- 统计包括该部门及其所有子部门的员工
- 统计基于 `UserDepartment` 表，只统计有效归属（`leftAt = null`）
- 如果用户同时属于多个部门，在各部门统计中都会计算
- `byPosition` 统计该部门内各岗位的员工数量
- `byStatus` 统计该部门内各状态的员工数量
- `subDepartments` 列出所有子部门及其员工数

**错误响应**:

| 错误码      | HTTP | 说明       |
| ----------- | ---- | ---------- |
| `NOT_FOUND` | 404  | 部门不存在 |

---

### 4. 区域管理接口

#### GET /regions

获取区域列表。

**权限**: 已登录

**成功响应** (200):

```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "code": "CN",
      "name": "中国",
      "nameEn": "China",
      "nameZh": "中国",
      "timezone": "Asia/Shanghai",
      "currency": "CNY",
      "locale": "zh-CN",
      "isActive": true,
      "order": 1,
      "createdAt": "2025-01-01T00:00:00.000Z"
    },
    {
      "id": "uuid",
      "code": "US",
      "name": "美国",
      "nameEn": "United States",
      "nameZh": "美国",
      "timezone": "America/Los_Angeles",
      "currency": "USD",
      "locale": "en-US",
      "isActive": true,
      "order": 2,
      "createdAt": "2025-01-01T00:00:00.000Z"
    }
  ],
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

---

#### POST /regions

创建区域。

**权限**: `region:create`

**请求体**:

```typescript
interface CreateRegionDto {
  code: string; // 区域编码（唯一，如 CN、US、UAE）
  name: string; // 区域名称
  nameEn?: string; // 英文名称
  nameZh?: string; // 中文名称
  timezone?: string; // 时区
  currency?: string; // 货币
  locale?: string; // 语言区域
  order?: number; // 排序序号
}
```

**示例请求**:

```json
{
  "code": "UAE",
  "name": "阿联酋",
  "nameEn": "United Arab Emirates",
  "nameZh": "阿联酋",
  "timezone": "Asia/Dubai",
  "currency": "AED",
  "locale": "ar-AE",
  "order": 3
}
```

**成功响应** (201):

```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "code": "UAE",
    "name": "阿联酋",
    "isActive": true,
    "createdAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "区域创建成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

---

#### DELETE /regions/:id

删除区域。

**权限**: `region:delete`

**错误响应**:

错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

---

### 5.1 区域管理扩展接口

#### GET /regions/active

获取活跃区域列表（用于下拉选择）。

**权限**: 已登录

**成功响应** (200):

```json
{
  "success": true,
  "data": [
    {
      "id": "region-cn",
      "code": "CN",
      "name": "中国",
      "nameEn": "China",
      "timezone": "Asia/Shanghai",
      "enabled": true
    },
    {
      "id": "region-us",
      "code": "US",
      "name": "美国",
      "nameEn": "United States",
      "timezone": "America/New_York",
      "enabled": true
    }
  ],
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 只返回 `enabled = true` 的区域
- 用于前端下拉选择器
- 按 `code` 排序

---

#### GET /regions/code/:code

按区域代码获取区域信息。

**权限**: `region:read`

**路径参数**:

| 参数   | 类型   | 说明                             |
| ------ | ------ | -------------------------------- |
| `code` | string | 区域代码（如 "CN", "US", "UAE"） |

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "id": "region-cn",
    "code": "CN",
    "name": "中国",
    "nameEn": "China",
    "timezone": "Asia/Shanghai",
    "enabled": true
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码      | HTTP | 说明       |
| ----------- | ---- | ---------- |
| `NOT_FOUND` | 404  | 区域不存在 |

---

#### GET /regions/:id/stats

获取区域统计信息。

**权限**: `region:read`

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "region": {
      "id": "region-cn",
      "code": "CN",
      "name": "中国"
    },
    "organizations": {
      "total": 3,
      "active": 2
    },
    "departments": {
      "total": 25,
      "topLevel": 5
    },
    "users": {
      "total": 156,
      "active": 142
    }
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 统计该区域下的组织、部门、用户数量
- 基于组织的主要区域（`primaryRegionId`）统计

---

#### PUT /regions/:id/default-organization

设置区域的默认组织。

**权限**: `region:update`

**请求体**:

```typescript
interface SetDefaultOrganizationDto {
  departmentId: string | null; // 组织对应的顶级部门 ID，null 表示清除默认组织
}
```

**示例请求**:

```json
{
  "departmentId": "org-ff-china-top-dept-id"
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "regionId": "region-cn",
    "defaultOrganizationId": "org-ff-china",
    "defaultDepartmentId": "org-ff-china-top-dept-id"
  },
  "message": "默认组织设置成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 只有顶级部门（`parentId = null`）才能设为区域的默认组织
- 设置默认组织后，该区域的新用户默认归属该组织
- `departmentId = null` 表示清除默认组织设置

**错误响应**:

错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

---

### 6. 系统角色管理接口

#### GET /roles

获取系统角色列表。

**权限**: `role:read`

**成功响应** (200):

```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "name": "系统管理员",
      "code": "Administrator",
      "description": "拥有所有权限",
      "isBuiltIn": true,
      "enabled": true,
      "userCount": 3,
      "permissionCount": 50,
      "createdAt": "2025-01-01T00:00:00.000Z"
    },
    {
      "id": "uuid",
      "name": "普通员工",
      "code": "Employee",
      "description": "基础使用权限",
      "isBuiltIn": true,
      "enabled": true,
      "userCount": 145,
      "permissionCount": 10,
      "createdAt": "2025-01-01T00:00:00.000Z"
    }
  ],
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

> **预定义角色**：系统仅预定义 `Administrator`（系统管理员）和 `Employee`（普通员工）两个内置角色。其他角色（如 HR、部门经理等）由管理员根据业务需要自行创建。

````

---

#### POST /roles

创建系统角色。

**权限**: `role:create`

**请求体**:

```typescript
interface CreateRoleDto {
  name: string;              // 角色名称
  code: string;              // 角色编码（唯一，大写下划线格式）
  description?: string;      // 描述
  permissionIds?: string[];  // 初始权限 ID 列表
}
````

**示例请求**:

```json
{
  "name": "部门经理",
  "code": "DEPT_MANAGER",
  "description": "部门管理角色",
  "permissionIds": [
    "550e8400-e29b-41d4-a716-446655440001",
    "550e8400-e29b-41d4-a716-446655440002"
  ]
}
```

**成功响应** (201):

```json
{
  "success": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "部门经理",
    "code": "DEPT_MANAGER",
    "isBuiltIn": false,
    "enabled": true,
    "createdAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "角色创建成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- `code` 使用大写 + 下划线格式：`DEPT_MANAGER`, `HR_STAFF`
- 创建后 `code` 不可修改
- 新创建的角色 `isBuiltIn = false`
- v2.3.1 起：创建成功后自动种入 `EMPLOYEE_BASELINE_TOOLS`（24 项 AI 工具）到 `AIToolGrant`；`code='SyncBot'` 跳过；seed 失败仅日志告警，不回滚 role

---

#### DELETE /roles/:id

删除系统角色。

**权限**: `role:delete`

**错误响应**:

| 错误码                      | HTTP | 说明                       |
| --------------------------- | ---- | -------------------------- |
| `IAM_SYSTEM_ROLE_PROTECTED` | 403  | 内置角色不可删除           |
| `IAM_ROLE_HAS_USERS`        | 400  | 角色下有关联用户，无法删除 |

**业务规则**:

- 内置角色（`isBuiltIn = true`）不可删除
- 有用户关联的角色不可删除，需先移除用户

---

#### PUT /roles/:id/permissions

为角色分配权限（覆盖式）。

**权限**: `role:manage`

**请求体**:

```typescript
interface AssignPermissionsDto {
  permissionIds: string[]; // 权限 ID 数组，将覆盖现有权限
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "roleId": "uuid",
    "permissionCount": 15
  },
  "message": "权限分配成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 覆盖式分配：传入的权限列表将完全替换现有权限
- 分配后，拥有该角色的所有用户权限缓存会被清理
- 此操作会记录审计日志

---

### 6. 流程角色管理接口

#### POST /workflow-roles

创建流程角色。

**权限**: `role:create`

**请求体**:

```typescript
interface CreateWorkflowRoleDto {
  name: string; // 流程角色名称
  code: string; // 流程角色编码（WF_ 前缀）
  description?: string; // 描述
  ruleType: WorkflowRuleType; // 规则类型
  ruleConfig: object; // 规则配置
}
```

**示例请求 - 组织关系规则**:

```json
{
  "name": "直属上级",
  "code": "WF_DIRECT_MANAGER",
  "description": "解析发起人的直属上级",
  "ruleType": "ORGANIZATION_RELATION",
  "ruleConfig": {
    "relation": "manager",
    "fallbackType": "UP_CHAIN",
    "fallbackConfig": { "maxLevel": 2 }
  }
}
```

**示例请求 - 固定用户规则**:

```json
{
  "name": "财务审批人",
  "code": "WF_FINANCE_APPROVER",
  "description": "财务部门固定审批人",
  "ruleType": "FIXED_USERS",
  "ruleConfig": {
    "userIds": ["uuid1", "uuid2", "uuid3"]
  }
}
```

**业务规则**:

- `code` 必须使用 `WF_` 前缀
- `ruleConfig` 的结构取决于 `ruleType`

---

#### POST /workflow-roles/resolve

解析流程角色，返回具体审批人。

**权限**: 服务间鉴权（内部调用）

> ⚠️ **安全要求**：此接口仅供审批引擎等内部服务调用，禁止前端直接访问。必须通过服务账号/内部 Token/mTLS 等方式鉴权。

**请求头**（服务间调用）:

```http
Authorization: Bearer <access_token>
X-Internal-Service-Token: <internal_service_secret>
X-Request-Source: approval-engine
```

| Header                     | 必填 | 说明                               |
| -------------------------- | ---- | ---------------------------------- |
| `X-Internal-Service-Token` | ✅   | 服务间调用密钥（从环境变量获取）   |
| `X-Request-Source`         | ✅   | 调用方标识（如 `approval-engine`） |

**请求体**:

```typescript
interface ResolveWorkflowRoleDto {
  workflowRoleCode: string; // 流程角色编码
  context: {
    initiatorUserId: string; // 发起人用户 ID
    formData?: {
      // 表单数据（用于条件解析）
      departmentId?: string;
      projectId?: string;
      amount?: number;
      [key: string]: any;
    };
  };
}
```

**示例请求**:

```json
{
  "workflowRoleCode": "WF_DEPT_MANAGER",
  "context": {
    "initiatorUserId": "550e8400-e29b-41d4-a716-446655440000",
    "formData": {
      "departmentId": "550e8400-e29b-41d4-a716-446655440001",
      "amount": 5000
    }
  }
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "users": [
      {
        "userId": "550e8400-e29b-41d4-a716-446655440002",
        "displayName": "张三",
        "email": "zhang.san@ff.com"
      }
    ],
    "strategy": "ALL",
    "resolvedBy": "ORGANIZATION_RELATION",
    "fallbackUsed": false
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码      | HTTP | 说明           |
| ----------- | ---- | -------------- |
| `NOT_FOUND` | 404  | 流程角色不存在 |

> 规则解析为空或用户不属于部门时的错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

**业务规则**:

- 根据 `ruleType` 和 `ruleConfig` 解析审批人
- 支持基于行政组织关系解析：
  - **解析优先级**：
    1. 若 `context.formData.departmentId` 存在：使用该部门的 `UserDepartment.managerId`
       - 若用户不属于该部门：**报错**（错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`），等待管理员处理
    2. 若 `context.formData.departmentId` 不存在：使用用户主部门（`isPrimary = true`）的 `managerId`
  - 部门主管审批：使用 `Department.headId`
- 解析结果为空时，按 `fallbackType` 执行兜底策略
- 接口不返回完整组织结构，只返回解析出的审批人
- 所有调用都会记录日志，便于审计

> **说明**：汇报关系通过 `UserDepartment.managerId` 管理，支持用户在不同部门有不同的直属上级。`managerId` 必须是同部门成员，矩阵式组织通过用户同时属于多个部门来实现。

---

### 7. 外部同步接口

#### POST /organization/sync

触发 Entra ID 同步。

**权限**: `organization:sync`

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "success": true,
    "totalUsers": 125,
    "syncedUsers": 125,
    "createdUsers": 5,
    "updatedUsers": 30,
    "skippedUsers": 90,
    "conflictUsers": 0,
    "errors": [],
    "duration": 18456,
    "timestamp": "2025-12-07T10:30:00.000Z"
  },
  "message": "同步完成",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

**业务规则**:

- 同一时间只允许一个同步任务运行
- 同步不会自动分配角色，需管理员手动分配
- 本地创建的用户（`source = 'LOCAL'`）不会被 Entra 同步覆盖
- 本地已 `TERMINATED` 的用户不会被同步自动复活
- 用户匹配优先使用 `externalId`，其次使用 `email`

**同步范围**:

- ✅ 用户基本信息（姓名、邮箱、员工ID）
- ✅ 职位信息
- ❌ 部门结构（本地管理）
- ❌ 汇报关系（本地管理）
- ❌ 密码、权限和角色

> **设计决策**：部门归属和汇报关系由本地管理员维护，不从 Entra 同步。原因是 Entra 只能提供单一部门信息，无法支持多部门归属场景。

---

#### GET /organization/sync/status

获取同步状态。

**权限**: `organization:read`

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "isSyncing": false,
    "lastSyncTime": "2025-12-07T08:00:00.000Z",
    "lastSyncResult": {
      "success": true,
      "totalUsers": 125,
      "createdUsers": 5,
      "updatedUsers": 30,
      "skippedUsers": 90,
      "errors": [],
      "duration": 18456
    }
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

---

#### GET /organization/stats

获取组织统计信息。

**权限**: 已登录

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "users": {
      "total": 156,
      "active": 142,
      "inactive": 5,
      "suspended": 3,
      "terminated": 6
    },
    "departments": {
      "total": 25,
      "topLevel": 5
    },
    "positions": {
      "total": 18
    },
    "roles": {
      "system": 12,
      "workflow": 8
    },
    "lastSyncTime": "2025-12-07T08:00:00.000Z"
  },
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

---

## 错误码汇总

本模块错误码以 `IAM_` 前缀为准（实现定义见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`）。以下为常用错误码示例，自动化测试断言以实现返回为准：

| 错误码                                    | HTTP | 场景          | 说明                         |
| ----------------------------------------- | ---- | ------------- | ---------------------------- |
| `IAM_INVALID_CREDENTIALS`                 | 401  | 登录          | 用户名或密码错误             |
| `IAM_USER_SUSPENDED`                      | 403  | 登录          | 账号被停用                   |
| `IAM_USER_TERMINATED`                     | 403  | 登录          | 账号已离职                   |
| `IAM_USER_LOCKED`                         | 423  | 登录          | 用户被锁定（当前未启用）     |
| `IAM_PASSWORD_TOO_WEAK`                   | 400  | 密码          | 密码不符合复杂度要求         |
| `IAM_USERNAME_EXISTS`                     | 409  | 创建用户      | 用户名已被使用               |
| `IAM_USER_EMAIL_EXISTS`                   | 409  | 创建用户      | 邮箱已被使用                 |
| `IAM_EMPLOYEE_ID_EXISTS`                  | 409  | 创建/更新用户 | 员工编号已被使用             |
| `IAM_MANAGER_LOOP_DETECTED`               | 400  | 设置上级      | 汇报关系形成环路             |
| `IAM_MANAGER_SELF_REFERENCE`              | 400  | 设置上级      | 不能将自己设为上级           |
| `IAM_MANAGER_NOT_IN_DEPARTMENT`           | 400  | 设置上级      | 上级不是该部门成员           |
| `IAM_DEPARTMENT_CODE_EXISTS`              | 409  | 创建部门      | 部门编码已存在               |
| `IAM_DEPARTMENT_HAS_CHILDREN`             | 400  | 删除部门      | 部门下有子部门               |
| `IAM_DEPARTMENT_HAS_USERS`                | 400  | 删除部门      | 部门下有成员                 |
| `IAM_SYSTEM_ROLE_PROTECTED`               | 403  | 删除角色      | 内置角色不可删除             |
| `IAM_ROLE_HAS_USERS`                      | 400  | 删除角色      | 角色下有关联用户             |
| `IAM_WORKFLOW_ROLE_RESOLVE_EMPTY`         | 422  | 流程角色      | 规则解析结果为空且无兜底策略 |
| `IAM_SYNC_IN_PROGRESS`                    | 409  | 同步          | 已有同步任务正在进行         |
| `IAM_EXTERNAL_ID_MISMATCH`                | 409  | 同步          | externalId 与现有记录冲突    |
| `IAM_TOKEN_EXPIRED`                       | 401  | 刷新 Token    | Token 已过期                 |
| `IAM_TOKEN_REVOKED`                       | 401  | 刷新 Token    | Token 已被撤销               |
| `IAM_USER_NOT_IN_DEPARTMENT`              | 404  | 部门归属      | 用户不属于指定部门           |
| `IAM_USER_ALREADY_IN_DEPARTMENT`          | 409  | 部门归属      | 用户已属于该部门             |
| `IAM_CANNOT_REMOVE_PRIMARY_DEPARTMENT`    | 400  | 部门归属      | 不能移除主部门               |
| `IAM_REGION_CODE_EXISTS`                  | 409  | 区域          | 区域编码已存在               |
| `IAM_REGION_HAS_DEPARTMENTS`              | 400  | 区域          | 区域下有关联组织             |
| `IAM_ONLY_ROOT_DEPARTMENT_CAN_BE_DEFAULT` | 400  | 区域          | 只有顶级部门才能设为默认组织 |
| `IAM_ORGANIZATION_CODE_EXISTS`            | 409  | 组织          | 组织代码已被使用             |
| `IAM_ORGANIZATION_NAME_EXISTS`            | 409  | 组织          | 组织名称已被使用             |

### 通用错误码

| 错误码                   | HTTP | 说明                        |
| ------------------------ | ---- | --------------------------- |
| `BAD_REQUEST`            | 400  | 请求参数或业务规则错误      |
| `VALIDATION_ERROR`       | 400  | 参数验证失败                |
| `CANNOT_CHANGE_PASSWORD` | 400  | 非 LOCAL 用户不允许修改密码 |
| `UNAUTHORIZED`           | 401  | 未认证                      |
| `FORBIDDEN`              | 403  | 无权限                      |
| `NOT_FOUND`              | 404  | 资源不存在                  |
| `CONFLICT`               | 409  | 资源冲突                    |
| `INTERNAL_ERROR`         | 500  | 服务器内部错误              |

---

## 8. 组织管理 API（v2.0 新增）

> **v2.0 架构**：Organization 是独立的一等公民，通过专用的组织 API 进行管理。

### 核心概念（v2.0）

| 概念           | 说明                                                                |
| -------------- | ------------------------------------------------------------------- |
| **独立组织表** | Organization 表是独立实体，支持组织特有属性（法人信息、财务配置等） |
| **部门归属**   | Department 通过 `organizationId` 归属组织                           |
| **顶级部门**   | `parentId = null` 的部门是组织的顶级部门                            |
| **组织区域**   | 通过 `OrganizationRegion` 表关联，区域信息从组织继承到部门          |

---

### 8.1 获取组织列表

#### GET /organizations

获取所有组织列表，支持分页和筛选。

**权限**: `organization:read`

**查询参数**:
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `page` | number | 否 | 页码（默认 1） |
| `limit` | number | 否 | 每页数量（默认 20） |
| `search` | string | 否 | 搜索关键字（匹配 name, code） |
| `regionId` | uuid | 否 | 按区域过滤 |
| `status` | string | 否 | 按状态过滤（ACTIVE, INACTIVE, SUSPENDED, DISSOLVED） |

**响应**:

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "org-ff-china",
        "code": "FF_CHINA",
        "name": "Faraday Future China",
        "displayName": "FF中国",
        "nameEn": "Faraday Future China",
        "nameZh": "法拉第未来（中国）",
        "legalName": "法拉第未来（中国）有限公司",
        "legalRepresentative": "张三",
        "registrationNumber": "91110000XXXXXXXX",
        "taxId": "91110000XXXXXXXX",
        "address": "北京市朝阳区...",
        "phone": "+86-10-12345678",
        "email": "contact@ffchina.com",
        "website": "https://ffchina.com",
        "primaryRegionId": "region-cn",
        "primaryRegion": {
          "id": "region-cn",
          "code": "CN",
          "name": "中国",
          "timezone": "Asia/Shanghai"
        },
        "organizationRegions": [
          { "id": "uuid", "regionId": "region-cn", "isDefault": true }
        ],
        "status": "ACTIVE",
        "isActive": true,
        "order": 0,
        "establishedAt": "2015-01-01",
        "departmentCount": 25,
        "employeeCount": 150,
        "createdAt": "2025-01-01T00:00:00.000Z",
        "updatedAt": "2025-12-26T00:00:00.000Z"
      }
    ],
    "total": 3,
    "page": 1,
    "limit": 20,
    "totalPages": 1
  },
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

---

### 8.2 获取组织详情

#### GET /organizations/:id

获取指定组织的详细信息。

**权限**: `organization:read`

**响应**: 同上单个组织结构

---

### 8.3 创建组织

#### POST /organizations

创建新组织。

**权限**: `organization:create`

**请求体**:

```typescript
interface CreateOrganizationDto {
  code: string; // 组织代码（唯一）
  name: string; // 组织名称
  displayName?: string; // 显示名称
  nameEn?: string; // 英文名称
  nameZh?: string; // 中文名称
  legalName?: string; // 法定名称
  legalRepresentative?: string; // 法人代表
  registrationNumber?: string; // 注册号/统一社会信用代码
  taxId?: string; // 税号
  address?: string; // 地址
  phone?: string; // 电话
  email?: string; // 邮箱
  website?: string; // 网站
  primaryRegionId: string; // 主要区域 ID（必需）
  operatingRegionIds?: string[]; // 运营区域 ID 数组
  establishedAt?: string; // 成立日期（ISO 8601 Date）
  settings?: Record<string, any>; // 组织配置
  financialConfig?: Record<string, any>; // 财务配置
  complianceConfig?: Record<string, any>; // 合规配置
  order?: number; // 排序
}
```

**示例请求**:

```json
{
  "code": "FF_USA",
  "name": "Faraday Future USA",
  "displayName": "FF美国",
  "nameEn": "Faraday Future USA",
  "legalName": "Faraday Future Inc.",
  "primaryRegionId": "region-us",
  "operatingRegionIds": ["region-us", "region-cn"],
  "establishedAt": "2014-05-01"
}
```

**响应**:

```json
{
  "success": true,
  "data": {
    "id": "org-ff-usa",
    "code": "FF_USA",
    "name": "Faraday Future USA",
    ...
  },
  "message": "组织创建成功",
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

**业务规则**:

- `code` 必须全局唯一
- `primaryRegionId` 必须是已存在的区域
- 如果提供 `operatingRegionIds`，会自动创建 `OrganizationRegion` 关联

---

### 8.4 更新组织

#### PATCH /organizations/:id

更新组织信息（部分更新）。

**权限**: `organization:update`

**请求体**: 同创建接口，所有字段可选

**示例请求**:

```json
{
  "displayName": "FF中国总部",
  "phone": "+86-10-87654321",
  "status": "ACTIVE"
}
```

**响应**: 同获取组织详情

---

### 8.5 删除组织

#### DELETE /organizations/:id

删除组织（软删除）。

**权限**: `organization:delete`

**业务规则**:

- 组织下有部门时不能删除，需要先删除所有部门
- 组织下有员工时不能删除
- 删除操作是软删除，可以恢复

**响应**:

```json
{
  "success": true,
  "message": "组织删除成功",
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

---

### 8.6 获取组织部门树

#### GET /organizations/:id/departments

获取组织下的完整部门树。

**权限**: `department:read`

**响应**:

```json
{
  "success": true,
  "data": {
    "organizationId": "org-ff-china",
    "organization": {
      "id": "org-ff-china",
      "name": "FF China"
    },
    "departmentTree": [
      {
        "id": "dept-tech",
        "name": "技术部",
        "code": "TECH",
        "organizationId": "org-ff-china",
        "parentId": null,
        "children": [
          {
            "id": "dept-frontend",
            "name": "前端组",
            "code": "FRONTEND",
            "organizationId": "org-ff-china",
            "parentId": "dept-tech",
            "children": []
          }
        ]
      }
    ],
    "totalDepartments": 25
  },
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

---

### 8.7 获取组织员工统计

#### GET /organizations/:id/stats

获取组织的员工统计信息。

**权限**: `organization:read`

**路径参数**:

| 参数 | 类型   | 说明           |
| ---- | ------ | -------------- |
| `id` | string | 组织 ID (UUID) |

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "organizationId": "org-ff-china",
    "organizationName": "FF China",
    "organizationCode": "FF_CHINA",
    "totalEmployees": 150,
    "activeEmployees": 145,
    "inactiveEmployees": 3,
    "suspendedEmployees": 1,
    "terminatedEmployees": 1,
    "totalDepartments": 25,
    "topLevelDepartments": 5,
    "byDepartment": [
      {
        "departmentId": "dept-tech-uuid",
        "departmentName": "技术部",
        "departmentCode": "TECH",
        "count": 50,
        "activeCount": 48,
        "inactiveCount": 2
      },
      {
        "departmentId": "dept-product-uuid",
        "departmentName": "产品部",
        "departmentCode": "PRODUCT",
        "count": 30,
        "activeCount": 30,
        "inactiveCount": 0
      },
      {
        "departmentId": "dept-hr-uuid",
        "departmentName": "人力资源部",
        "departmentCode": "HR",
        "count": 20,
        "activeCount": 19,
        "inactiveCount": 1
      }
    ],
    "byPosition": [
      {
        "positionId": "pos-engineer-uuid",
        "positionName": "工程师",
        "positionCode": "ENGINEER",
        "count": 80
      },
      {
        "positionId": "pos-manager-uuid",
        "positionName": "经理",
        "positionCode": "MANAGER",
        "count": 10
      },
      {
        "positionId": "pos-senior-uuid",
        "positionName": "高级工程师",
        "positionCode": "SENIOR_ENGINEER",
        "count": 30
      }
    ],
    "byStatus": {
      "ACTIVE": 145,
      "INACTIVE": 3,
      "SUSPENDED": 1,
      "TERMINATED": 1
    },
    "byRegion": [
      {
        "regionId": "region-cn-uuid",
        "regionCode": "CN",
        "regionName": "中国",
        "count": 120
      },
      {
        "regionId": "region-us-uuid",
        "regionCode": "US",
        "regionName": "美国",
        "count": 30
      }
    ],
    "lastUpdated": "2025-12-26T10:30:00.000Z"
  },
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

**业务规则**:

- 统计包括该组织下所有部门的员工
- 统计基于 `UserDepartment` 表，只统计有效归属（`leftAt = null`）
- 如果用户同时属于多个组织，在每个组织的统计中都会计算
- `byDepartment` 按部门统计员工数量（包括子部门）
- `byPosition` 统计组织内各岗位的员工数量
- `byStatus` 统计组织内各状态的员工数量
- `byRegion` 按用户的主区域统计（基于 `User.region` 字段）
- `topLevelDepartments` 统计顶级部门数量（`parentId = null`）

**错误响应**:

| 错误码      | HTTP | 说明       |
| ----------- | ---- | ---------- |
| `NOT_FOUND` | 404  | 组织不存在 |

---

### 8.8 管理组织区域关联

#### POST /organizations/:id/regions

为组织添加运营区域。

**权限**: `organization:update`

**请求体**:

```json
{
  "regionId": "region-uae",
  "isDefault": false
}
```

**响应**:

```json
{
  "success": true,
  "data": {
    "id": "org-region-uuid",
    "organizationId": "org-ff-china",
    "regionId": "region-uae",
    "isDefault": false
  },
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

---

#### DELETE /organizations/:id/regions/:regionId

移除组织的运营区域关联。

**权限**: `organization:update`

**业务规则**:

- 不能移除主要区域（`primaryRegionId`）
- 如果是 `isDefault=true` 的区域，需要先设置其他区域为默认

**响应**:

```json
{
  "success": true,
  "message": "区域关联已移除",
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

---

### 8.9 获取用户的组织列表

#### GET /users/:id/organizations

获取用户所属的所有组织（通过用户的部门归属计算）。

**权限**: 已登录（可查看自己）或 `user:read`

**响应**:

```json
{
  "success": true,
  "data": [
    {
      "organizationId": "org-ff-china",
      "organization": {
        "id": "org-ff-china",
        "code": "FF_CHINA",
        "name": "FF China"
      },
      "departments": [
        {
          "id": "dept-tech",
          "name": "技术部",
          "isPrimary": true
        }
      ]
    },
    {
      "organizationId": "org-ff-usa",
      "organization": {
        "id": "org-ff-usa",
        "code": "FF_USA",
        "name": "FF USA"
      },
      "departments": [
        {
          "id": "dept-sales",
          "name": "销售部",
          "isPrimary": false
        }
      ]
    }
  ],
  "timestamp": "2025-12-26T10:30:00.000Z"
}
```

---

## 9. 多部门归属管理 API（v2.0 新增）

> 以下 API 用于管理用户的多部门归属，支持用户同时属于多个组织/部门，每个归属有独立的岗位和汇报关系。
> **v2.0 新增**：这 5 个 API 是 v2.0 架构升级的一部分，支持矩阵式组织管理。

### 核心概念

| 概念               | 说明                                                           |
| ------------------ | -------------------------------------------------------------- |
| **主部门**         | 用户的主要归属部门（`isPrimary = true`），每个用户有且只有一个 |
| **兼职部门**       | 用户的次要归属部门（`isPrimary = false`），可有多个            |
| **部门内汇报**     | `managerId` 必须是同一部门的成员                               |
| **UserDepartment** | 用户-部门关联表，存储岗位、汇报关系等信息                      |

### 9.1 获取用户所有部门归属

#### GET /users/:id/departments

获取用户所属的所有部门列表（包括主部门和兼职部门）

**权限**: 已登录

**响应**:

```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "departmentId": "uuid",
      "department": {
        "id": "uuid",
        "name": "技术部",
        "code": "TECH",
        "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" }
      },
      "isPrimary": true,
      "position": {
        "id": "uuid",
        "name": "高级工程师",
        "level": 5
      },
      "manager": {
        "id": "uuid",
        "displayName": "李四",
        "email": "lisi@ff.com"
      },
      "title": "后端开发负责人",
      "joinedAt": "2024-01-15T00:00:00.000Z"
    },
    {
      "id": "uuid",
      "departmentId": "uuid",
      "department": {
        "id": "uuid",
        "name": "项目A组",
        "code": "PROJ_A",
        "primaryRegion": { "id": "uuid", "code": "CN", "name": "中国" }
      },
      "isPrimary": false,
      "position": {
        "id": "uuid",
        "name": "技术顾问",
        "level": 6
      },
      "manager": {
        "id": "uuid",
        "displayName": "王五",
        "email": "wangwu@ff.com"
      },
      "title": null,
      "joinedAt": "2024-06-01T00:00:00.000Z"
    }
  ],
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

---

### 9.2 添加用户部门归属

#### POST /users/:id/departments

为用户添加新的部门归属（兼职部门）

**权限**: `user:update`

**请求体**:

```typescript
interface AddUserDepartmentDto {
  departmentId: string; // 部门 ID
  positionId?: string; // 岗位 ID（可选）
  managerId?: string; // 直属上级 ID（可选，必须是同部门成员）
  title?: string; // 职位头衔（可选）
  isPrimary?: boolean; // 是否设为主部门（默认 false）
}
```

**示例请求**:

```json
{
  "departmentId": "550e8400-e29b-41d4-a716-446655440001",
  "positionId": "550e8400-e29b-41d4-a716-446655440002",
  "managerId": "550e8400-e29b-41d4-a716-446655440003",
  "title": "技术顾问"
}
```

**成功响应** (201):

```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "userId": "uuid",
    "departmentId": "uuid",
    "isPrimary": false,
    "joinedAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "部门归属添加成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码                           | HTTP | 说明               |
| -------------------------------- | ---- | ------------------ |
| `IAM_USER_ALREADY_IN_DEPARTMENT` | 409  | 用户已属于该部门   |
| `IAM_MANAGER_NOT_IN_DEPARTMENT`  | 400  | 上级不是该部门成员 |

> 汇报关系校验相关错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

**业务规则**:

- 同一用户在同一部门只能有一条有效记录
- `managerId` 必须是同部门成员
- 如果 `isPrimary = true`，会自动将原主部门改为兼职

---

### 9.3 更新用户部门归属

#### PATCH /users/:userId/departments/:departmentId

更新用户在指定部门的归属信息

**权限**: `user:update`

**请求体**（所有字段可选）:

```typescript
interface UpdateUserDepartmentDto {
  positionId?: string | null; // 岗位 ID
  managerId?: string | null; // 直属上级 ID（必须是同部门成员）
  title?: string | null; // 职位头衔
  isPrimary?: boolean; // 是否设为主部门
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "isPrimary": true,
    "updatedAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "部门归属更新成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

| 错误码                          | HTTP | 说明               |
| ------------------------------- | ---- | ------------------ |
| `IAM_MANAGER_NOT_IN_DEPARTMENT` | 400  | 上级不是该部门成员 |

> 用户不属于部门/汇报关系校验相关错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

---

### 9.4 移除用户部门归属

#### DELETE /users/:userId/departments/:departmentId

移除用户在指定部门的归属

**权限**: `user:update`

**业务逻辑**:

- 如果删除的是主部门，系统自动将用户最早加入的其他部门设为主部门
- 如果是用户的最后一个部门，允许删除（用户可以没有部门归属）
- 返回警告信息提示主部门删除和自动调整结果

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "message": "Department membership removed successfully",
    "warning": "Primary department removed. Another department has been automatically set as primary."
  },
  "message": "部门归属移除成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

**业务规则**:

- 允许移除主部门，系统自动调整
- 删除主部门时，自动将用户最早加入的其他部门设为主部门
- 如果是用户的最后一个部门，允许删除

---

### 9.5 设置用户主部门

#### PUT /users/:userId/departments/:departmentId/primary

将指定部门设置为用户的主部门

**权限**: `user:update`

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "userId": "uuid",
    "primaryDepartmentId": "uuid",
    "previousPrimaryDepartmentId": "uuid"
  },
  "message": "主部门设置成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

**业务规则**:

- 每个用户有且只有一个主部门
- 设置新主部门会自动将原主部门改为兼职

### 9.6 批量导入用户-部门关系

#### POST /users/import-memberships

通过 Excel 批量导入用户与部门的关系（岗位、直属主管、头衔、主部门）。

**权限**: `user:update`

**约束**: 用户必须已存在系统中（通过 Entra ID 同步创建），此接口仅导入关系，不创建新用户。支持创建新关系或更新已有关系。

**请求体**:

```typescript
interface ImportUserMembershipDto {
  email: string;           // 用户邮箱（必填，用于查找用户）
  departmentCode: string;  // 部门编码（必填，格式：/^[A-Z0-9_-]+$/）
  positionId?: string;     // 岗位 ID（可选）
  managerEmail?: string;   // 直属主管邮箱（可选）
  title?: string;          // 部门内头衔（可选）
  isPrimary?: boolean | string; // 是否主部门（可选）
}

// 请求
{ users: ImportUserMembershipDto[] }
```

**验证规则**:

| 字段 | 规则 |
|------|------|
| `email` | 必填，邮箱格式 `/^[^\s@]+@[^\s@]+\.[^\s@]+$/` |
| `departmentCode` | 必填，大写字母、数字、下划线或连字符 `/^[A-Z0-9_-]+$/` |
| `positionId` | 可选，UUID |
| `managerEmail` | 可选，邮箱格式，对应用户必须存在 |

**响应** (200):

```json
{
  "success": true,
  "data": {
    "total": 5,
    "successCount": 3,
    "failedCount": 2,
    "results": {
      "success": [
        {
          "email": "zhang@ff.com",
          "userId": "uuid",
          "departmentId": "uuid",
          "action": "created"
        }
      ],
      "failed": [
        {
          "email": "unknown@ff.com",
          "error": "User not found"
        }
      ]
    }
  }
}
```

**错误场景**:

| 场景 | 行为 |
|------|------|
| 用户不存在 | 该行记入 `failed`，不中断其余行 |
| 部门不存在 | 该行记入 `failed` |
| 主管不存在 | 该行记入 `failed` |
| 关系已存在 | 更新现有关系（`action: "updated"`） |

---

### 2.1 用户管理扩展接口

#### POST /users/:id/region-roles

分配区域角色（完全替换用户的所有角色）。

**权限**: `role:manage`

**请求体**:

```typescript
interface AssignRegionRolesDto {
  roles: Array<{
    roleId: string; // 角色 ID
    region?: string | null; // 区域代码（如 "CN", "US"），null 表示全局角色
  }>;
}
```

**示例请求**:

```json
{
  "roles": [
    { "roleId": "role-hr", "region": null }, // 全局角色
    { "roleId": "role-manager", "region": "CN" }, // 中国区角色
    { "roleId": "role-employee", "region": "US" } // 美国区角色
  ]
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "userId": "uuid",
    "roles": [
      { "roleId": "role-hr", "region": null },
      { "roleId": "role-manager", "region": "CN" },
      { "roleId": "role-employee", "region": "US" }
    ]
  },
  "message": "区域角色分配成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 完全替换用户的所有角色（包括全局角色和区域角色）
- `region=null` 表示全局角色，适用于所有区域
- 如果用户在同一区域有多个角色，需要多次调用或使用批量接口

---

#### POST /users/:id/region-roles/add

添加单个区域角色。

**权限**: `role:manage`

**请求体**:

```typescript
interface AddRegionRoleDto {
  roleId: string; // 角色 ID
  region?: string | null; // 区域代码（如 "CN", "US"），null 表示全局角色
}
```

**示例请求**:

```json
{
  "roleId": "role-manager",
  "region": "CN"
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "userId": "uuid",
    "roleId": "role-manager",
    "region": "CN"
  },
  "message": "区域角色添加成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**错误响应**:

错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

---

#### POST /users/:id/region-roles/remove

删除单个区域角色。

**权限**: `role:manage`

**请求体**:

```typescript
interface RemoveRegionRoleDto {
  roleId: string; // 角色 ID
  region?: string | null; // 区域代码（如 "CN", "US"），null 表示全局角色
}
```

**示例请求**:

```json
{
  "roleId": "role-manager",
  "region": "CN"
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": null,
  "message": "区域角色移除成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 只移除指定区域的角色，不影响其他区域的角色
- 如果用户没有该区域角色，操作仍然返回成功（幂等性）

---

#### POST /users/:id/restore

恢复已删除的用户（软删除恢复）。

**权限**: `user:update`

**请求体**:

```typescript
interface RestoreUserDto {
  departmentId?: string; // 可选：恢复后设置的主部门 ID
  positionId?: string; // 可选：恢复后设置的岗位 ID
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "username": "john.doe",
    "status": "INACTIVE",
    "restoredAt": "2025-12-07T10:30:00.000Z"
  },
  "message": "用户恢复成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 只能恢复软删除的用户（`deletedAt != null`）
- 恢复后用户状态为 `INACTIVE`，需要管理员激活
- 恢复后可以重新设置部门和岗位
- 恢复操作会保留用户的历史数据

**错误响应**:

| 错误码                | HTTP | 说明                                 |
| --------------------- | ---- | ------------------------------------ |
| `IAM_USERNAME_EXISTS` | 409  | 用户名已被其他用户使用（恢复时冲突） |

> 用户未删除等状态类错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

---

#### POST /users/:id/reset-password

重置用户密码（管理员操作）。

**权限**: `user:update`

**请求体**:

```typescript
interface ResetPasswordDto {
  newPassword: string; // 新密码（8位以上，至少2种字符类型）
}
```

**成功响应** (200):

```json
{
  "success": true,
  "data": null,
  "message": "密码重置成功",
  "timestamp": "2025-12-07T10:30:00.000Z"
}
```

**业务规则**:

- 管理员可以直接重置用户密码，无需提供旧密码
- 新密码必须符合复杂度要求
- 重置后用户需要重新登录
- 操作会记录审计日志

**错误响应**:

| 错误码               | HTTP | 说明       |
| -------------------- | ---- | ---------- |
| `IAM_USER_NOT_FOUND` | 404  | 用户不存在 |

> 密码复杂度校验相关错误码见 `backend/src/modules/organization/exceptions/iam.exceptions.ts`。

---

## 11. AI 工具授权管理 API（v2.3 全量 per-user 控制）

> **v2.3 升级**: `allow` 白名单语义 + LOCKED_SET 安全兜底 + 全量工具 per-user 控制。
> 同步脚本写 `agents.list[].tools.allow`（白名单过滤），支持加和减。
> 完整方案见 openclaw 仓 `docs/enterprise-plan/solution/governance/permissions-mvp-plan.md`。

### 核心概念

| 概念 | 说明 |
|---|---|
| **角色级授权 (Role Grant)** | 主维度，绑在 Role 上，跨组织共享。一次配置影响该角色下所有用户 |
| **用户级授权 (User Grant)** | 例外维度，绑在 User 上，对角色基线做加/减调整，必填 `reason` |
| **LOCKED_SET** | `[session_status, sessions_history, sessions_list, sessions_send]`。不可编辑的 4 个对话基础工具，前端 disabled，后端保存时强制合入 |
| **生效公式** | `LOCKED_SET ∪ (⋃ 角色授权 ⊕ 用户级调整)`，其中 ⊕ 支持加和减 |
| **可用工具清单** | 全量静态配置 ~20 项，每项含 `category` + `locked` 字段 |
| **权限码** | `ai_tool:read`（查询）、`ai_tool:manage`（写操作） |

### 11.1 列出角色级授权（按角色聚合）

#### GET /api/v1/ai-tools/grants

**权限**：`ai_tool:read`

**查询参数**：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| `roleId` | uuid | 否 | 按角色筛选 |
| `search` | string | 否 | 按角色名/code 模糊匹配 |

> v2.3 变更：返回按角色聚合的结构（一个角色一条记录 + `tools[]`），不再返回展平的 grant 行。

**响应**：

```json
{
  "success": true,
  "data": [
    {
      "roleId": "uuid",
      "roleName": "Employee",
      "roleCode": "Employee",
      "tools": ["browser", "cron", "edit", "exec", "image", "m365_calendar", "m365_files", "m365_mail", "memory_get", "memory_search", "process", "read", "tts", "web_fetch", "web_search", "write"],
      "toolCount": 16,
      "updatedAt": "2026-04-16T...",
      "updatedBy": "itadmin"
    }
  ]
}
```

- SyncBot 角色自动从返回中过滤
- tools 按字母序排列
- v2.3.1 起：以 Role 表为基准（LEFT JOIN 风格），即使没有 AIToolGrant 记录的角色也会返回（`tools=[]`、`toolCount=0`）；行排序按 `createdAt desc`，与「角色与权限」页一致

### 11.2 设置角色级授权（事务 upsert）

#### PUT /api/v1/ai-tools/grants/role/:roleId

**权限**：`ai_tool:manage`（自动审计）

> v2.3 新增。替代旧的 POST / DELETE 逐条操作，一次事务设置整个角色的工具集合。

**请求体**：

```json
{
  "tools": ["browser", "cron", "edit", "exec", "m365_mail"]
}
```

**后端行为**：
1. 校验 `tools[]` 每项必须在 `available-tools` 清单中
2. **强制合入 LOCKED_SET**：无论前端传不传 `session_status` 等 4 项，后端一律并入目标集合
3. 计算与当前 DB 的 diff（added / removed）
4. 事务内 INSERT 新增 + DELETE 移除
5. 写 `platform_audit.audit_log`（`module="ai-tools"`, `entityType="role-grant"`, `entityId=roleId`, `isSensitive=true`, `riskLevel=HIGH`），`changes` 存 `{added: [...], removed: [...]}`

**响应**：200 OK

```json
{
  "success": true,
  "data": {
    "roleId": "uuid",
    "tools": ["browser", "cron", "edit", "exec", "m365_mail", "session_status", "sessions_history", "sessions_list", "sessions_send"],
    "added": ["exec"],
    "removed": ["m365_calendar"],
    "toolCount": 9
  }
}
```

**错误**：
- `IAM_AI_TOOL_UNKNOWN` (400) — 工具不在可用清单
- `IAM_AI_TOOL_GRANT_NOT_FOUND` (404) — roleId 不存在
- `IAM_AI_TOOL_GRANT_CONFLICT` (409) — 并发修改冲突

### 11.3 设置用户级授权（事务 upsert）

#### PUT /api/v1/ai-tools/user-grants/:userId

**权限**：`ai_tool:manage`（自动审计）

> v2.3 新增。用户级调整是相对于角色基线的差集操作。

**请求体**：

```json
{
  "added": ["image"],
  "removed": ["m365_mail", "cron"],
  "reason": "项目 X 临时需要 image；暂不需要邮件和定时"
}
```

**校验**：
- `reason` **强制必填**
- `added` 和 `removed` 中的工具必须在 `available-tools` 清单中
- LOCKED_SET 中的工具不允许出现在 `removed` 中（400 报错）
- `userId` 必须是未软删的有效用户

**后端行为**：
1. `added` 中的工具：INSERT `user_grants`（type=add）
2. `removed` 中的工具：INSERT `user_grants`（type=remove），标记"用户显式取消此基线工具"
3. 写 `platform_audit.audit_log`

**响应**：200 OK

```json
{
  "success": true,
  "data": {
    "userId": "uuid",
    "added": ["image"],
    "removed": ["m365_mail", "cron"],
    "reason": "项目 X 临时需要 image；暂不需要邮件和定时"
  }
}
```

### 11.4 用户授权概览（带过滤）

#### GET /api/v1/ai-tools/user-grants-overview

**权限**：`ai_tool:read`

> v2.3 新增。为"用户授权"tab 提供聚合视图，支持多维过滤。

**查询参数**：

| 参数 | 类型 | 必需 | 说明 |
|---|---|---|---|
| `orgId` | uuid | 否 | 按组织筛选 |
| `deptId` | uuid | 否 | 按部门筛选 |
| `roleId` | uuid | 否 | 按角色筛选（支持逗号分隔多选，OR 语义） |
| `search` | string | 否 | 按 username/displayName/email ilike 模糊匹配 |
| `hasExtra` | boolean | 否 | 只返回有用户级额外添加的用户 |
| `hasRevoked` | boolean | 否 | 只返回有用户级显式取消的用户 |
| `page` | number | 否 | 页码，默认 1 |
| `pageSize` | number | 否 | 每页大小，默认 20 |

**响应**：

```json
{
  "success": true,
  "data": {
    "items": [
      {
        "userId": "uuid",
        "displayName": "Hongwei Zhang",
        "email": "hongwei.zhang@ff.com",
        "avatar": "url",
        "orgName": "Engineering",
        "deptName": "AI Platform",
        "roles": [
          { "id": "uuid", "name": "Administrator", "code": "Administrator" }
        ],
        "inheritedToolCount": 18,
        "addedCount": 2,
        "removedCount": 1,
        "effectiveToolCount": 19
      }
    ],
    "total": 748,
    "page": 1,
    "pageSize": 20
  }
}
```

### 11.5 获取可用工具清单

#### GET /api/v1/ai-tools/available-tools

**权限**：`ai_tool:read`

> v2.3 变更：扩展到全量 ~20 项，增加 `category` 和 `locked` 字段。

**响应**：

```json
{
  "success": true,
  "data": [
    {
      "name": "session_status",
      "label": "会话状态",
      "description": "Agent 运行时自检",
      "category": "core",
      "locked": true
    },
    {
      "name": "m365_mail",
      "label": "邮件（M365）",
      "description": "Microsoft 365 Outlook 邮件只读访问",
      "category": "productivity",
      "locked": false
    }
  ]
}
```

**`category` 取值**：
- `core` — 对话基础工具（locked=true）
- `fs` — 文件工具
- `runtime` — 运行时
- `sessions` — 会话管理（非 locked 部分：sessions_spawn, sessions_yield, subagents）
- `memory` — 记忆
- `web` — 网页
- `media` — 媒体
- `automation` — 自动化
- `browser` — 浏览器
- `productivity` — M365 工具

### 11.6 查询用户最终生效工具（按用户）

#### GET /api/v1/ai-tools/user-effective/:userId

**权限**：`ai_tool:read`

> v2.3 变更：返回**所有**生效工具（含 LOCKED_SET），每项带详细来源。

**响应**：

```json
{
  "success": true,
  "data": [
    {
      "toolName": "session_status",
      "category": "core",
      "locked": true,
      "sources": [
        { "type": "locked", "label": "系统锁定" }
      ]
    },
    {
      "toolName": "m365_mail",
      "category": "productivity",
      "locked": false,
      "sources": [
        { "type": "role", "roleId": "uuid", "roleName": "Administrator", "grantId": "uuid" }
      ]
    },
    {
      "toolName": "image",
      "category": "media",
      "locked": false,
      "sources": [
        { "type": "user", "userId": "uuid", "grantId": "uuid", "reason": "项目 X 临时需要" }
      ]
    }
  ],
  "meta": {
    "totalTools": 19,
    "lockedCount": 4,
    "inheritedCount": 14,
    "userAddedCount": 1,
    "userRemovedCount": 1,
    "userRemovedTools": ["cron"]
  }
}
```

`sources[].type` 枚举：`locked` / `role` / `user`

### 11.7 反查工具下的所有生效用户（按工具）

#### GET /api/v1/ai-tools/tool-subjects/:toolName

**权限**：`ai_tool:read`

反查某个工具被哪些用户在用。用于合规审计「谁在用 m365_mail」。

> v2.3 变更：返回通过角色继承得到的用户（不仅是直接授权的），按角色分组。

**响应**：

```json
{
  "success": true,
  "data": [
    {
      "userId": "uuid",
      "userDisplayName": "Alice",
      "userEmail": "alice@ff.com",
      "orgName": "Engineering",
      "deptName": "AI Platform",
      "sources": [
        { "type": "role", "roleId": "uuid", "roleName": "Employee", "grantId": "uuid" }
      ]
    }
  ]
}
```

软删用户自动过滤，不出现在结果中。

### 11.8 触发同步到 OpenClaw

#### POST /api/v1/ai-tools/sync

**权限**：`ai_tool:manage`（自动审计）

信息性接口。OpenClaw 同步脚本是跑在服务器 host crontab 上的独立进程（每 5 分钟 pull 一次），Workspace 后端不主动推送 / 远程触发。

**响应**：201 Created

```json
{
  "success": true,
  "data": {
    "scheduled": true,
    "intervalMinutes": 5,
    "message": "授权变更将在 5 分钟内由 OpenClaw 同步脚本自动拉取并生效。"
  }
}
```

### 11.9 v2.2 遗留接口（保持向后兼容，内部使用）

以下接口保留但不再由前端直接使用（前端统一走 11.2/11.3 的 PUT 接口）：
- `POST /api/v1/ai-tools/grants` — 单条创建角色级授权
- `POST /api/v1/ai-tools/grants/batch` — 批量创建角色级授权
- `DELETE /api/v1/ai-tools/grants/:id` — 删除角色级授权
- `GET /api/v1/ai-tools/user-grants` — 列出用户级授权
- `POST /api/v1/ai-tools/user-grants` — 创建用户级授权
- `DELETE /api/v1/ai-tools/user-grants/:id` — 删除用户级授权

sync 脚本内部仍通过 `user-effective/:userId` 接口获取数据。

### 11.10 错误码

| 错误码 | HTTP | 场景 | 说明 |
|---|---|---|---|
| `IAM_AI_TOOL_GRANT_ROLE_EXISTS` | 409 | 创建角色级授权 | (roleId, toolName) 已存在 |
| `IAM_AI_TOOL_GRANT_USER_EXISTS` | 409 | 创建用户级授权 | (userId, toolName) 已存在 |
| `IAM_AI_TOOL_GRANT_NOT_FOUND` | 404 | 删除/查询 | 授权 id / role / user 不存在 |
| `IAM_AI_TOOL_UNKNOWN` | 400 | 创建/设置 | 工具不在可用清单 |
| `IAM_AI_TOOL_GRANT_CONFLICT` | 409 | PUT 设置 | 并发修改冲突 |
| `IAM_AI_TOOL_LOCKED_CANNOT_REMOVE` | 400 | PUT 用户级设置 | 尝试 remove LOCKED_SET 工具 |

---

## 幂等性说明

| 操作                      | 幂等性    | 说明                         |
| ------------------------- | --------- | ---------------------------- |
| `GET` 请求                | ✅ 幂等   | 只读操作                     |
| `PUT` 请求                | ✅ 幂等   | 重复调用产生相同结果         |
| `DELETE` 请求             | ✅ 幂等   | 已删除的资源再次删除返回成功 |
| `POST /users`             | ❌ 非幂等 | 重复提交相同 email 返回错误  |
| `POST /organization/sync` | ✅ 幂等   | 重复调用不会创建重复用户     |
| `POST /roles/:id/users`   | ✅ 幂等   | 重复添加相同用户无副作用     |

---

## 🧪 测试示例

### 认证流程

#### 1. 用户登录

```bash
curl -X POST 'http://localhost:3001/api/v1/auth/login' \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "john.doe",
    "password": "SecurePass123!"
  }'
```

**响应**:

```json
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 86400,
    "user": { "id": "uuid", "username": "john.doe", ... }
  }
}
```

#### 2. 使用 Token 访问 API

```bash
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

curl 'http://localhost:3001/api/v1/users/me' \
  -H "Authorization: Bearer $TOKEN"
```

---

### 用户管理

#### 1. 查询用户列表

```bash
curl 'http://localhost:3001/api/v1/users?page=1&pageSize=20&status=ACTIVE&search=john' \
  -H "Authorization: Bearer $TOKEN"
```

#### 2. 创建用户

```bash
curl -X POST 'http://localhost:3001/api/v1/users' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "jane.doe",
    "email": "jane.doe@ff.com",
    "password": "SecurePass123!",
    "displayName": "Jane Doe",
    "departmentId": "dept-uuid",
    "organizationId": "org-uuid",
    "positionId": "pos-uuid",
    "region": "CN"
  }'
```

#### 3. 分配角色（v2.1 新格式）

```bash
curl -X POST 'http://localhost:3001/api/v1/users/{userId}/roles' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "assignments": [
      {
        "roleId": "role-hr-uuid",
        "organizationId": "org-ff-china"
      },
      {
        "roleId": "role-auditor-uuid",
        "organizationId": null
      }
    ]
  }'
```

---

### 组织管理（v2.0）

#### 1. 查询组织列表

```bash
curl 'http://localhost:3001/api/v1/organizations?page=1&limit=20&status=ACTIVE' \
  -H "Authorization: Bearer $TOKEN"
```

#### 2. 创建组织

```bash
curl -X POST 'http://localhost:3001/api/v1/organizations' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "code": "FF_USA",
    "name": "Faraday Future USA",
    "primaryRegionId": "region-us-uuid",
    "operatingRegionIds": ["region-us-uuid", "region-cn-uuid"]
  }'
```

---

### TypeScript 客户端示例

```typescript
import apiClient from "@/lib/api-client";

// 1. 登录
async function login(username: string, password: string) {
  const response = await apiClient.post("/auth/login", { username, password });
  const { accessToken } = response.data;
  localStorage.setItem("token", accessToken);
  return response.data.user;
}

// 2. 获取用户列表
async function fetchUsers(params: {
  page?: number;
  keyword?: string;
  status?: string;
}) {
  const response = await apiClient.get("/users", { params });
  return response; // { items: User[], total, page, limit, totalPages, hasNext, hasPrev }
}

// 3. 创建用户
async function createUser(userData: CreateUserDto) {
  try {
    const user = await apiClient.post("/users", userData);
    return user;
  } catch (error) {
    if (error.code === "IAM_USERNAME_EXISTS") {
      alert("用户名已被使用");
    } else if (error.code === "IAM_USER_EMAIL_EXISTS") {
      alert("邮箱已被使用");
    }
    throw error;
  }
}

// 4. v2.1: 分配组织级角色
async function assignOrganizationRoles(
  userId: string,
  assignments: RoleAssignment[],
) {
  const response = await apiClient.post(`/users/${userId}/roles`, {
    assignments,
  });
  return response.data;
}

// 5. 触发同步
async function triggerSync() {
  try {
    const result = await apiClient.post("/organization/sync");
    console.log(
      `同步完成：创建 ${result.createdUsers} 人，更新 ${result.updatedUsers} 人`,
    );
  } catch (error) {
    if (error.code === "IAM_SYNC_IN_PROGRESS") {
      alert("同步任务进行中，请稍后再试");
    }
    throw error;
  }
}

// 6. 获取组织列表（v2.0）
async function fetchOrganizations(params?: {
  page?: number;
  limit?: number;
  search?: string;
  regionId?: string;
  status?: string;
}) {
  const response = await apiClient.get("/organizations", { params });
  return response.data;
}
```

---

## 📝 版本历史

| 版本     | 日期       | 修改内容                                                                     | 影响                                            |
| -------- | ---------- | ---------------------------------------------------------------------------- | ----------------------------------------------- |
| **v2.1** | 2025-12-26 | 组织级权限隔离                                                               | ✅ 核心变更                                     |
|          |            | - `POST /users/:id/roles` 支持 `organizationId` 参数                         | 角色分配 API 更新                               |
|          |            | - 用户可在不同组织拥有不同角色                                               | 权限模型重大升级                                |
|          |            | - 支持全局角色（`organizationId=null`）                                      | 系统管理员等全局角色                            |
|          |            | - 新增错误码 `IAM_FORBIDDEN`                                                 | 权限不足                                        |
| **v2.0** | 2025-12-20 | 独立 Organization 表                                                         | ✅ 架构升级                                     |
|          |            | - 新增 15 个 API（8个组织管理 + 5个多部门归属 + 2个其他）                    | 组织 CRUD、区域关联、多部门管理                 |
|          |            | - `Department.organizationId` 必填                                           | 部门归属组织                                    |
|          |            | - `UserDepartment.organizationId` 冗余字段                                   | 性能优化                                        |
|          |            | - 新增错误码 `IAM_ORGANIZATION_CODE_EXISTS` / `IAM_ORGANIZATION_NAME_EXISTS` | 组织管理错误                                    |
| **v1.0** | 2024-11-01 | 初始版本                                                                     | -                                               |
|          |            | - 认证接口（3个API）                                                         | 登录、登出、Token 刷新                          |
|          |            | - 用户管理（13个API）                                                        | 用户 CRUD、状态管理、角色分配（不含多部门归属） |
|          |            | - 部门管理（9个API）                                                         | 部门树、成员管理                                |
|          |            | - 岗位管理（5个API）                                                         | 岗位 CRUD                                       |
|          |            | - 区域管理（5个API）                                                         | 区域 CRUD                                       |
|          |            | - 系统角色（10个API）                                                        | 角色 CRUD、权限分配                             |
|          |            | - 权限管理（4个API）                                                         | 权限查询、分组                                  |
|          |            | - 流程角色（8个API）                                                         | 流程角色 CRUD、解析                             |
|          |            | - 外部同步（3个API）                                                         | Entra ID 同步                                   |
|          |            | - 审计日志（2个API）                                                         | 日志查询、导出                                  |
|          |            | - 区域按 `region` 字段隔离                                                   | 已废弃，v2.1 改用组织                           |

---

## 📊 接口统计

### 按分类统计

| 分类         | 核心接口 | 扩展接口 | 总计   | v2.0 新增 | v2.1 更新 | 说明                                                                                             |
| ------------ | -------- | -------- | ------ | --------- | --------- | ------------------------------------------------------------------------------------------------ |
| 认证接口     | 5        | 2        | 7      | -         | -         | 登录、登出、Token 刷新、注册、修改密码、SSO 发起、SSO 回调（v2.4 新增 2 个 SSO）                 |
| 用户管理     | 18       | 5        | 23     | +5        | 1         | 用户 CRUD(8)、角色分配(3)、权限查询(2)、多部门归属(5)、区域角色管理(3)、恢复用户(1)、重置密码(1) |
| **组织管理** | **8**    | **0**    | **8**  | **✅**    | -         | **组织 CRUD、区域关联、统计、用户组织列表（v2.0）**                                              |
| 部门管理     | 9        | 4        | 13     | -         | -         | 部门 CRUD、部门树、批量操作、成员管理                                                            |
| 岗位管理     | 5        | 0        | 5      | -         | -         | 岗位 CRUD                                                                                        |
| 区域管理     | 5        | 4        | 9      | -         | -         | 区域 CRUD、活跃区域、按代码查询、统计、默认组织设置                                              |
| 系统角色     | 10       | 0        | 10     | -         | 1         | 角色 CRUD、权限分配、用户分配（v2.1 支持组织级）                                                 |
| 权限管理     | 4        | 0        | 4      | -         | -         | 权限查询、分组                                                                                   |
| 流程角色     | 8        | 0        | 8      | -         | -         | 流程角色 CRUD、解析                                                                              |
| 外部同步     | 3        | 0        | 3      | -         | -         | Entra ID 同步                                                                                    |
| 审计日志     | 0        | 0        | 0      | -         | -         | **注意：审计日志接口在审计系统模块中（`/audit/logs`）**                                          |
| **总计**     | **79**   | **15**   | **94** | **15**    | **1**     | 核心接口 79 个 + 扩展接口 15 个（v2.4 +2 SSO）                                                   |

### v2.1 核心变更

**角色分配 API 升级**（`POST /users/:id/roles`）:

- ✅ 支持组织级角色分配（`organizationId` 参数）
- ✅ 支持全局角色（`organizationId=null`）
- ✅ 用户可在不同组织拥有不同角色
- ✅ 向后兼容旧格式（`roleIds` 数组）

**示例**（v2.1 新格式）:

```json
{
  "assignments": [
    { "roleId": "role-hr", "organizationId": "org-ff-china" },
    { "roleId": "role-admin", "organizationId": null }
  ]
}
```

> **类型定义**: 参见 [架构文档 - DTO 层](./03-architecture.md#dto-层数据传输对象) 中的 `RoleAssignmentDto` 和 `AssignRoleDto`

---

## 🔗 相关文档

- [产品需求文档](./01-prd.md) - 业务背景和功能规格
- [架构设计](./03-architecture.md) - 系统架构和技术设计
- [数据模型](./06-data-model.md) - 数据库表设计
- [错误码定义](../../../../backend/src/modules/organization/exceptions/iam.exceptions.ts) - 以实现为准
- [状态机](./04-state-machine.md) - 用户状态流转规则
- [UI 交互规范](./05-ui-interaction-spec.md) - 前端交互设计

---

**最后更新**: 2025-12-26  
**维护者**: FFOA 后端团队  
**版本**: v2.1.1（身份源管理 + 密码策略 + 登录安全说明）  
**API 总数**: 92 个端点（v2.0 新增 15 个，v2.1 更新 1 个，v2.1.1 完善 3 个）

**v2.1.1 更新内容**:

- ✅ 完善登录 API 的身份源支持和失败锁定说明
- ✅ 补充修改密码 API 的身份源限制和错误码
- ✅ 更新创建用户 API 支持身份源选择（LOCAL/LDAP）
- ✅ 明确 Entra 同步用户通过 LDAP/AD 认证，不支持本地密码修改
- ✅ 补充登录安全说明（失败锁定未启用）
- ✅ 新增身份源相关错误响应示例
