# 用户与组织架构管理 - 产品需求文档

> **版本**: v2.4  
> **创建日期**: 2024-11-01  
> **最后更新**: 2026-05-19  
> **产品经理**: FFOA 产品团队

---

## 📋 文档变更记录

| 版本 | 日期 | 修改人 | 修改内容 |
|------|------|--------|---------|
| v2.4 | 2026-05-19 | FFOA Team | 新增「功能 13: Entra ID SSO 登录」（OIDC 授权码流程 + JIT 建账号 + 双通道并存），推翻 v2.1.1「ENTRA 不作为身份源」决策；关联 issue #334 |
| v2.1.35 | 2026-03-13 | FFOA Team | 钉钉年假页面支持按员工编辑本地释放计划参数，并明确额度扣减与计划偏移字段语义 |
| v2.1.34 | 2026-03-11 | FFOA Team | 新增年假释放计划手动重算能力，支持页面一键更新本地中间表 |
| v2.1.33 | 2026-03-11 | FFOA Team | 年假释放中间表从宜搭迁移到本地数据库，年假释放与计划展示统一读取本地表 |
| v2.1.32 | 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-10 | FFOA Team | 新增钉钉年假余额总览与释放计划可视化页面 |
| v2.1.28 | 2026-03-10 | FFOA Team | 新增钉钉同步人工撤销修复能力 |
| v2.1.27 | 2026-02-25 | 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 | 独立 Organization 表，性能优化 |
| v2.1 | 2025-12-26 | FFOA Team | 组织级权限隔离，修复区域隔离问题 |
| v2.1.1 | 2025-12-26 | FFOA Team | 补充完整功能细节：身份源管理、密码修改、登录安全、Scope系统 |
| v2.1.2 | 2026-01-05 | FFOA Team | 登录安全策略调整：明确不实现登录失败锁定 |
| v2.1.3 | 2026-01-05 | FFOA Team | Scope 系统推迟到 v2.2，功能范围优化 |
| v2.1.22 | 2026-01-05 | FFOA Team | 身份源说明优化：明确 Entra ID 仅用于同步 |
| v2.1.23 | 2026-01-05 | FFOA Team | AD 用户创建方式明确：仅支持 Entra ID 同步 |
| v2.1.24 | 2026-01-05 | FFOA Team | 定时自动同步功能已实现 |
| v2.1.25 | 2026-01-05 | FFOA Team | 删除员工入职流程图，聚焦实际业务场景 |

---

## 🎯 产品概述

### 背景

FFOA 平台作为企业级办公自动化系统，需要一个强大的身份与组织架构管理系统作为基础设施。

### 目标

构建一个企业级的用户与组织架构管理系统，实现：

1. **统一身份管理**：支持本地用户、LDAP/AD 认证
2. **多组织架构**：支持独立的多组织管理，每个组织有独立的部门树和权限体系
3. **灵活的组织架构**：支持用户多部门归属、矩阵式组织、独立的汇报关系
4. **精细的权限控制**：基于 RBAC + PBAC 的双重权限模型，支持组织级权限隔离
5. **流程角色抽象**：支持审批流程中的动态审批人解析
6. **外部系统集成**：支持从 Microsoft Entra ID 同步用户信息（仅同步，不认证）
7. **完整的审计追踪**：所有关键操作可追溯

### 成功指标

- **性能指标**: 权限查询响应时间 < 100ms，用户搜索 < 200ms
- **可用性指标**: 系统可用性 ≥ 99.9%
- **安全指标**: 所有敏感操作 100% 记录审计日志
- **用户满意度**: HR 管理员操作效率提升 50%
- **集成指标**: Entra ID 同步成功率 ≥ 99%

---

## 👥 目标用户

### 用户角色

| 角色 | 描述 | 使用场景 |
|------|------|---------|
| **系统管理员** | 管理整个系统的配置和权限 | 系统初始化、权限分配、全局配置 |
| **HR 管理员** | 管理组织架构和人员信息 | 员工入职/离职、组织架构调整、权限申请审批 |
| **部门经理** | 管理本部门的人员和日常事务 | 查看部门成员、审批部门内请求 |
| **普通员工** | 使用系统完成日常工作 | 查看自己的信息、更新个人资料、发起审批 |
| **IT 管理员** | 维护外部系统集成 | Entra ID 同步、LDAP 配置、故障排查 |

### 用户画像

**主要用户 - HR 管理员**:
- 年龄：28-45 岁
- 工作经验：3-10 年 HR 从业经验
- 技能水平：熟悉 Office 软件，对 HR 系统有一定使用经验
- 痛点：需要频繁处理员工入职、离职、调动等操作，希望系统操作简便、快速
- 期望：一站式管理组织架构，快速查询员工信息，批量导入导出功能

**次要用户 - 部门经理**:
- 年龄：30-50 岁
- 工作经验：5-15 年管理经验
- 技能水平：基本的计算机操作能力
- 痛点：需要查看团队成员信息、审批部门内请求
- 期望：快速查看团队结构，简单直观的审批流程

---

## 🎨 功能需求

### 范围定义

#### In Scope（本期实现）✅

**v2.1 核心功能**：
- ✅ 用户管理：CRUD、状态管理、多部门归属、身份源管理（LOCAL/LDAP）
- ✅ 组织管理：独立 Organization 表、区域关联、组织配置
- ✅ 部门管理：无限层级树形结构、部门负责人
- ✅ 岗位管理：岗位 CRUD、职级管理、删除保护
- ✅ 区域管理：多区域支持（CN/US/UAE）
- ✅ 权限管理：组织级角色隔离、RBAC + PBAC 双重模型
- ✅ 流程角色：动态审批人解析、三种规则类型、组织关系支持
- ✅ 外部同步：Microsoft Entra ID 用户同步（手动触发 + 定时自动同步）
- ✅ 身份认证：JWT、LDAP/AD 认证、密码修改（仅本地用户）、基础登录安全
- ✅ 审计日志：关键操作自动记录
- ✅ 钉钉相关能力：文档已迁移至 [dingtalk 模块](../dingtalk/README.md)

**v2.1 核心改进**：
- ✅ 权限从区域隔离升级为组织隔离
- ✅ 支持用户在不同组织拥有不同角色
- ✅ 支持全局角色（系统管理员）
- ✅ 性能优化：organizationId 冗余字段，查询性能提升 10 倍
- ✅ 身份源简化：仅两种（LOCAL 本地用户 + LDAP AD用户）
- ✅ AD 用户创建方式：仅支持 Entra ID 同步，不支持手动创建
- ✅ Entra ID 定位明确：仅用于同步用户信息，不是独立的认证方式
- ✅ 基础登录安全：密码强度验证、账号状态检查
- ✅ 简化设计：不实现登录失败锁定（LDAP 认证自带锁定机制）

**v2.2 规划功能**（计划 2026-01-15）：
- 📋 完整的 Scope 系统（own/department/organization/all）
- 📋 细粒度权限控制（基于 Scope 的数据访问范围）
- 📋 流程角色兜底策略完善



---

## 📊 功能详细设计

### 功能0: 钉钉年假计划参数维护

该能力已迁移至独立模块文档维护，见 [docs/modules/dingtalk/01-prd.md](../dingtalk/01-prd.md)。

### 功能1: 用户管理

**优先级**: P0

**功能描述**:
管理系统用户的完整生命周期，包括创建、编辑、状态管理、部门归属、角色分配等。支持两种身份源：本地用户（LOCAL）和 AD 用户（LDAP）。

**两种身份源**:

| 身份源 | 创建方式 | 认证方式 | 密码管理 | 同步更新 | 说明 |
|--------|---------|---------|---------|---------|------|
| **本地用户（LOCAL）** | 手动创建 | 本地密码验证 | ✅ 系统管理 | ❌ N/A | 用于测试或特殊场景 |
| **AD 用户（LDAP）** | ✅ **仅 Entra ID 同步** | LDAP/AD 认证 | ❌ AD 管理 | ✅ 自动同步 | 生产环境推荐 |

> **说明**：
> - 本地用户：支持手动创建，用于测试或特殊场景
> - AD 用户：**只能通过 Entra ID 同步创建**，不支持手动创建
> - Microsoft Entra ID 仅用于**同步用户信息**（显示名、邮箱等），不是独立的身份源
> - AD 用户统一使用 LDAP 协议进行身份认证
> - Entra ID 和 LDAP 都是连接到同一个 Active Directory

**用户故事**:
- 作为 **IT 管理员**，我想要 **从 Entra ID 同步 AD 用户**，以便 **员工使用企业 AD 账号登录**
- 作为 **HR 管理员**，我想要 **为员工设置多个部门归属**，以便 **支持矩阵式组织管理**
- 作为 **普通员工**，我想要 **查看和更新自己的基本信息**，以便 **保持信息准确**

**接受标准**:
- [x] 支持创建、编辑、删除用户
- [x] 支持两种身份源：本地用户（LOCAL）、AD 用户（LDAP）
- [x] 本地用户支持手动创建和密码设置
- [x] AD 用户仅通过 Entra ID 同步创建，不支持手动创建
- [x] AD 用户通过企业 AD 认证，无需在系统设置密码
- [x] 支持用户状态管理（激活、停用、暂停、离职）
- [x] 支持用户多部门归属，每个归属独立配置岗位和汇报关系
- [x] 支持为用户分配角色和权限（组织级隔离）
- [x] 支持按姓名、邮箱、部门、状态、身份源搜索筛选
- [x] TERMINATED 用户无法登录但数据保留

**业务规则**:
1. **身份源管理**:
   - 本地用户（LOCAL）：
     - 支持手动创建，需要设置密码
     - 支持密码修改，用于测试或特殊场景
   - AD 用户（LDAP）：
     - **只能通过 Entra ID 同步创建**，不支持手动创建
     - 使用 LDAP 协议连接 AD 进行认证
     - 密码由 Active Directory 管理，不能在系统中修改
     - Entra ID 仅用于同步用户信息（如 displayName、email），不是独立的认证方式
2. **唯一性约束**: 邮箱、用户名、员工编号必须全局唯一
3. **状态流转**: 
   - INACTIVE → ACTIVE（激活）
   - ACTIVE → SUSPENDED（停用）
   - SUSPENDED → ACTIVE（恢复）
   - ACTIVE/SUSPENDED → TERMINATED（离职，不可逆）
4. **多部门归属**:
   - 用户可以同时属于多个部门
   - 必须有且仅有一个主部门
   - 不同部门可以有不同的岗位和汇报关系
5. **汇报关系约束**:
   - 不允许自己是自己的上级
   - 不允许形成汇报环路（A→B→C→A）
   - 上级必须是同部门成员
6. **删除保护**: TERMINATED 用户不能物理删除，必须保留历史记录

**UI 要求**:
- 详见 [UI 交互规范 - 用户管理页面](./05-ui-interaction-spec.md#1-用户管理页面)

---

### 功能2: 组织管理（v2.0 新增）

**优先级**: P0

**功能描述**:
管理独立的组织实体，每个组织代表一个独立的法人公司（如 FF China、FF USA）。组织支持特有属性配置（法人信息、财务配置等），并通过主要区域和运营区域关联多个地理区域。

**用户故事**:
- 作为 **系统管理员**，我想要 **创建新的组织实体**，以便 **支持公司在新地区的运营**
- 作为 **系统管理员**，我想要 **设置组织的主要区域和运营区域**，以便 **正确的数据隔离和业务路由**

**接受标准**:
- [x] 支持创建、编辑、删除组织
- [x] 组织代码全局唯一
- [x] 支持设置主要区域（primaryRegion）和运营区域（operatingRegions）
- [x] 支持配置组织特有属性（法人名称、统一社会信用代码等）
- [x] 支持设置组织负责人
- [x] 删除前检查是否有关联的部门和用户

**业务规则**:
1. **组织独立性**: 每个组织是独立的一等公民，拥有独立的：
   - 部门树结构
   - 权限体系（v2.1）
   - 组织配置
2. **区域关联**:

---

### 功能12: 钉钉同步人工修复

该能力已迁移至独立模块文档维护，见 [docs/modules/dingtalk/01-prd.md](../dingtalk/01-prd.md)。

---

### 功能3: 部门管理

**优先级**: P0

**功能描述**:
管理企业的部门树形结构，支持无限层级嵌套。每个部门归属一个组织，可以设置部门负责人和部门信息。

> **⭐ 核心设计原则**: 组织与部门分离。创建组织时自动创建根部门（`parentId = null`）作为部门树入口，但根部门不等同于组织实体；组织通过 `organizationId` 与部门关联。

**用户故事**:
- 作为 **HR 管理员**，我想要 **创建部门并设置层级关系**，以便 **反映公司的组织架构**
- 作为 **HR 管理员**，我想要 **调整部门层级和归属**，以便 **适应组织架构变化**
- 作为 **部门经理**，我想要 **查看我的部门树**，以便 **了解团队结构**

**接受标准**:
- [x] 支持创建、编辑、删除部门
- [x] 支持无限层级的部门树
- [x] 支持设置部门负责人
- [x] 支持树形展示和折叠/展开
- [x] 显示每个部门的成员数
- [x] 删除前检查是否有子部门和成员
- [x] 创建组织时自动创建对应的根部门

**业务规则**:

1. **组织与部门分离 + 自动根部门** ⭐ 核心设计：
   - 组织为独立实体，不与部门层级强绑定
   - 创建组织时自动创建根部门（`parentId = null`）
   - 根部门名称/编码与组织保持一致
   - **不允许手动创建顶级部门**（应用层约束）

2. **组织归属**: 
   - 所有部门必须归属一个组织（organizationId）
   - 根部门由系统自动创建
   - 除根部门外，其他部门都必须有父部门

3. **树形结构**:
   - 所有其他部门的 parentId 必须指向同组织内的部门
   - 不允许跨组织的父子关系
   - 层级深度建议不超过 10 层

4. **部门代码**: 
   - 部门代码在同一组织内必须唯一

5. **删除保护**: 
   - 有子部门或成员的部门不能删除
   - **根部门不能删除**（只能通过删除组织来删除）

6. **审批链完整性**:
   - 审批链解析以部门层级为准，不依赖组织实体
   - 根部门确保部门链路有确定入口

**设计优势**:
- ✅ **概念清晰**: 组织与部门职责分离，归属关系清晰
- ✅ **结构稳定**: 自动根部门确保部门树有确定入口
- ✅ **数据一致**: 部门始终有组织归属
- ✅ **逻辑自然**: 部门层级独立演进

**UI 要求**:
- 详见 [UI 交互规范 - 组织架构树页面](./05-ui-interaction-spec.md#3-组织架构树页面)
- 组织架构树的根节点显示为根部门名称
- 创建部门时必须选择父部门（无法创建顶级部门）

---

### 功能4: 多部门归属

**优先级**: P1

**功能描述**:
支持用户同时属于多个部门，每个部门归属有独立的岗位和汇报关系。用户必须有且仅有一个主部门。

**用户故事**:
- 作为 **HR 管理员**，我想要 **为员工添加兼职部门**，以便 **支持矩阵式组织管理**
- 作为 **HR 管理员**，我想要 **为员工在不同部门设置不同的岗位和上级**，以便 **反映实际工作关系**
- 作为 **普通员工**，我想要 **查看我所属的所有部门**，以便 **了解我的工作职责**

**接受标准**:
- [x] 用户可以同时属于多个部门
- [x] 每个用户有且只有一个主部门
- [x] 不同部门可以设置不同的岗位和汇报关系
- [x] 支持添加、移除、更新部门归属
- [x] 支持设置和切换主部门

**业务规则**:
1. **主部门唯一性**: 每个用户必须有且只有一个主部门（isPrimary = true）
2. **独立配置**: 每个部门归属可以独立配置：
   - 岗位（positionId）
   - 直属上级（managerId）
   - 加入时间（joinedAt）
3. **汇报关系**: 用户在某部门的上级必须是该部门的成员
4. **主部门自动调整**: 删除主部门时，系统自动将最早加入的其他部门设为主部门
5. **审批解析**: 审批流程在提交时基于主部门确定审批人并固化

**UI 要求**:
- 详见 [UI 交互规范 - 用户详情页面](./05-ui-interaction-spec.md#2-用户详情页面)

---

### 功能5: 权限管理（v2.1 核心改进）

**优先级**: P0

**功能描述**:
基于 RBAC + PBAC 的双重权限模型，v2.1 引入组织级角色隔离，解决 v2.0 区域隔离导致的权限泄露问题。支持用户在不同组织拥有不同角色，支持全局角色（系统管理员）。

**用户故事**:
- 作为 **系统管理员**，我想要 **为用户在指定组织分配角色**，以便 **精确控制权限范围**
- 作为 **HR 管理员**，我想要 **管理本组织的所有用户**，但 **不能访问其他组织的数据**
- 作为 **跨组织 HR**，我想要 **在不同组织拥有不同的权限**，以便 **灵活处理多组织事务**

**接受标准**:
- [x] 支持创建、编辑、删除角色
- [x] 支持为角色分配权限点
- [x] 支持为用户在指定组织分配角色（v2.1）
- [x] 支持全局角色（organizationId = null）
- [x] 内置角色不可删除
- [x] 权限变更后立即生效
- [ ] 基于 Scope 的细粒度权限控制（计划 v2.2）

**业务规则**:

**v2.1 核心变更**:
1. **组织级角色隔离**:
   - 角色分配时必须指定组织（organizationId）
   - null 表示全局角色（系统管理员）
   - 同一用户可以在不同组织有不同角色
2. **权限控制维度**:
   - **organizationId**: 横向隔离（在哪个组织有权限）
   - **scope**: 纵向控制（在组织内能访问多少数据）⚠️ **v2.2 实现**

**权限范围（Scope）**（⚠️ 计划 v2.2 实现）:
| Scope | 说明 | 数据范围 | 典型角色 | 状态 |
|-------|------|----------|----------|------|
| `own` | 仅自己的数据 | 当前用户 | 普通员工 | 📋 v2.2 |
| `department` | 本部门数据 | 用户所属部门 | 部门经理 | 📋 v2.2 |
| `organization` | 本组织数据 | 用户所属组织 | HR 管理员 | 📋 v2.2 |
| `all` | 全局数据 | 所有组织 | 系统管理员 | 📋 v2.2 |

**权限命名规范**:
- 当前格式：`{resource}:{action}`
- Scope 版格式：`{resource}:{action}[:{scope}]`（📋 v2.2 规划）
- 示例（当前）：`user:read`、`user:update`、`role:assign`

> 📋 **v2.1 实施状态**：
> - ✅ 权限点的 `scope` 字段已在数据模型中预留
> - ✅ 当前权限码使用 `{resource}:{action}`
> - ⚠️ Guards 和业务逻辑中的 Scope 检查**暂未实现**
> - 📋 **v2.2 将实现**：完整的 Scope 检查逻辑、Guards、API 层面的细粒度控制

3. **内置角色**:
   - Administrator（系统管理员）：拥有所有权限，不可删除
   - Employee（普通员工）：基础权限，不可删除

**实际应用场景**（v2.2 实现 Scope 后）:

> ⚠️ **当前状态（v2.1）**：仅实现组织级隔离（organizationId），Scope 检查暂未实现  
> 📋 **v2.2 将实现**：完整的 Scope 系统和以下应用场景

**场景1: 部门经理**（v2.2）
```
角色分配: { userId: "li-si", roleId: "manager", organizationId: "org-ff-china" }
权限: ["user:read:department", "user:update:department"]
效果: 可以管理 FF China > 技术部的员工，不能访问其他部门或组织
```

**场景2: HR 管理员**（v2.2）
```
角色分配: { userId: "wang-wu", roleId: "hr", organizationId: "org-ff-china" }
权限: ["user:read:organization", "user:update:organization"]
效果: 可以管理 FF China 的所有员工，不能访问 FF USA
```

**场景3: 跨组织 HR**（✅ v2.1 已支持）
```
角色分配:
  - { userId: "sun-qi", roleId: "hr", organizationId: "org-ff-china" }
  - { userId: "sun-qi", roleId: "hr", organizationId: "org-ff-usa" }
效果: 在 FF China 和 FF USA 都有 HR 权限，可以分别管理
```

**UI 要求**:
- 详见 [UI 交互规范 - 角色管理页面](./05-ui-interaction-spec.md#5-角色管理页面)

---

### 功能6: 流程角色

**优先级**: P1

**功能描述**:
支持审批流程中的动态审批人解析。基于组织关系（如直属上级、部门经理）或系统角色映射，在流程提交时动态确定审批人。

**用户故事**:
- 作为 **审批流程管理员**，我想要 **配置"直属上级"流程角色**，以便 **自动找到提交人的上级作为审批人**
- 作为 **审批流程管理员**，我想要 **配置兜底策略**，以便 **处理解析失败的情况**

**接受标准**:
- [x] 支持创建、编辑流程角色
- [x] 支持组织关系解析（直属上级、部门经理）
- [x] 支持系统角色映射
- [x] 支持固定用户列表
- [ ] 支持兜底策略（计划 v2.2 完善）

**业务规则**:
1. **规则类型**:
   - **组织关系 (ORGANIZATION_RELATION)**: 基于组织架构动态解析（如直属上级、部门主管）
   - **系统角色映射 (SYSTEM_ROLE_MAPPING)**: 映射到系统角色的所有用户（计划 v2.2）
   - **固定用户 (FIXED_USERS)**: 指定固定的用户列表
2. **解析时机**: 审批流程在提交时解析并固化审批人，后续组织架构变更不影响已提交流程
3. **上下文支持**: 
   - 支持指定部门上下文（`formData.departmentId`）
   - 如指定部门：使用该部门的汇报关系（`UserDepartment.managerId`）
   - 如未指定：使用用户主部门（`isPrimary = true`）的汇报关系
4. **组织关系解析规则**:
   - **直属上级 (manager)**: 
     - 使用 `UserDepartment.managerId`
     - 如用户不在指定部门：抛出 `IAM_USER_NOT_IN_DEPARTMENT` 异常
   - **部门主管 (departmentHead)**: 
     - 使用 `Department.headId`
     - 基于发起人的主部门或指定部门
     - 如当前部门无主管，自动向上追溯到有主管的父部门（最多10层）
   - **连续部门主管链 (departmentHeadChain)**: ⭐ 新增
     - 从用户所在部门开始，逐级向上查找所有部门主管
     - 返回结果为数组，按从下到上的顺序排列
     - 跳过没有主管的部门，继续向上查找
     - **支持指定终止级别** (v2.1.18 新增): ⭐
       - `stopAtLevel`: 可选参数，指定在哪一级部门停止（0=顶级组织部门）
       - 未指定则追溯到顶级部门（parentId = null）
       - 示例：`stopAtLevel: 1` 表示追溯到一级部门即停止
       - 层级计算：从顶级部门开始，level=0（根部门），level=1（一级部门），依次类推
     - 最多追溯20层（防止无限循环）
     - **适用场景**: 
       - 逐级审批流程（如请假需经过部门主管、总监、副总、CEO）
       - 限定审批范围（如只需部门和事业部审批，不需要集团审批）
     - **返回策略**: `strategy: "SEQUENTIAL"` 表示需按顺序审批
5. **兜底策略**（计划 v2.2 完善）:
   - **UP_CHAIN**: 向上查找（如直属上级的上级）
   - **FIXED_FALLBACK**: 固定用户兜底
   - **Administrator**: 系统管理员兜底
6. **解析失败处理**:
   - 解析结果为空且无兜底策略：抛出 `IAM_WORKFLOW_ROLE_RESOLVE_EMPTY` 异常
   - 流程角色不存在：抛出 `NotFoundException`
   - 用户不在指定部门：抛出 `IAM_USER_NOT_IN_DEPARTMENT` 异常
7. **用户分配规则** (仅 FIXED_USERS 类型):
   - 只有 `FIXED_USERS` 类型支持用户分配
   - 其他类型分配用户抛出 `BadRequestException`
   - 重复分配用户：幂等操作，不报错
8. **唯一性约束**:
   - `code` 必须全局唯一
   - `name` 必须全局唯一
   - `code` 必须使用 `WF_` 前缀（推荐规范）
9. **删除保护**:
   - TODO: 计划 v2.2 添加，检查是否有审批流程正在使用

**UI 要求**:
- 待完善（计划 v2.2）

---

### 功能7: 外部同步

**优先级**: P1

**功能描述**:
支持从 Microsoft Entra ID 同步用户信息到本地数据库，实现用户信息的自动更新。同步的用户标记为 AD 用户（LDAP），传统认证路径走 LDAP，**v2.4 起 Entra 同时作为 SSO 身份源**（OIDC 授权码流程，详见「功能 13」）。

> **重要说明**（v2.4 更新）：
> - Entra ID 同时承担**用户信息同步**（本功能）和 **SSO 登录身份源**（功能 13）双重职责
> - 历史 v2.1.1 决策「Entra 仅同步、不作为身份源」已被 v2.4 推翻；同步通道与 SSO 通道**正交**，本功能仅讨论同步
> - LDAP 认证通道保留作为遗留路径，与 OIDC SSO **并存**
> - Entra ID 和 LDAP 都连接到同一个 Active Directory

**用户故事**:
- 作为 **IT 管理员**，我想要 **手动触发 Entra ID 同步**，以便 **及时同步最新的人员变动**
- 作为 **IT 管理员**，我想要 **查看同步结果和错误日志**，以便 **排查同步问题**

**接受标准**:
- [x] 支持手动触发 Entra ID 同步
- [x] 支持定时自动同步
- [x] 显示同步状态（进行中、成功、失败）
- [x] 显示同步结果（新增、更新、跳过、错误数）
- [x] 查看同步日志和错误详情

**业务规则**:
1. **同步范围**:
   - ✅ 用户基本信息（姓名、邮箱、员工ID）
   - ❌ 部门结构（本地管理）
   - ❌ 汇报关系（本地管理）
   - ❌ 权限和角色（本地管理）
2. **冲突策略**:
   - HR 字段（displayName）：以 Entra 为准覆盖
   - 本地字段（工号、手机）：不覆盖
   - 禁用用户：本地状态改为 SUSPENDED
   - TERMINATED 用户：不自动复活，需管理员手动处理
3. **并发控制**: 同一时间只允许一个同步任务运行
4. **定时同步**: 
   - 支持配置自动同步周期（如每天、每周）
   - 支持配置同步时间（如每天凌晨 2 点）
   - 支持启用/禁用定时同步
5. **权限策略**: 同步的用户不会自动分配任何权限

**UI 要求**:
- 详见 [UI 交互规范 - 外部同步页面](./05-ui-interaction-spec.md#8-外部同步页面)

---

### 功能8: 身份认证

**优先级**: P0

**功能描述**:
支持多种身份认证方式，包括本地认证（用户名/密码）、LDAP/AD 企业目录认证，**v2.4 新增 Entra ID SSO 登录**（OIDC 授权码流程，详见「功能 13」）。使用 JWT 令牌进行会话管理。本地用户支持密码修改，AD 用户密码由 Active Directory 管理。

> **说明**（v2.4 更新）：
> - 本地用户（LOCAL）：使用系统数据库验证密码，可修改密码
> - AD 用户（LDAP）：使用 LDAP 协议连接 Active Directory 验证密码
> - Microsoft Entra ID：**v2.4 起既是同步通道（功能 7）也是 SSO 登录身份源（功能 13）**；历史 v2.1.1「Entra 不作为身份源」决策已被推翻
> - 三个通道**并存**，互不影响

**用户故事**:
- 作为 **本地用户**，我想要 **使用邮箱和密码登录**，以便 **访问系统**（测试或特殊场景）
- 作为 **AD 用户**，我想要 **使用 AD 账号登录**，以便 **使用统一的企业账号**
- 作为 **本地用户**，我想要 **修改自己的密码**，以便 **定期更新密码保证安全**
- 作为 **AD 用户**，我想要 **被提示通过 IT 管理员修改密码**，以便 **知道正确的修改方式**

**接受标准**:
- [x] 支持用户名/邮箱 + 密码登录（本地认证）
- [x] 支持 LDAP/AD 认证（企业目录认证）
- [x] 登录成功返回 JWT Token
- [x] 未登录访问受保护页面时，登录成功后自动回到原页面（含查询参数）
- [x] 支持安全登出
- [x] 支持 Token 刷新
- [x] 支持修改密码（仅本地用户）
- [x] AD 用户无法通过系统修改密码
- [ ] 登录失败锁定机制（暂不实现，主要使用 LDAP 认证）

**业务规则**:
1. **身份源认证**:
   - 本地用户（LOCAL）：使用 bcrypt 验证密码
   - AD 用户（LDAP）：转发到 LDAP/AD 服务器认证
   - Entra ID：仅用于同步用户信息，同步的用户使用 LDAP 认证
2. **密码策略**（仅本地用户）:
   - 最小长度 8 位
   - 至少包含 2 种字符类型（大写、小写、数字、特殊字符）
   - 密码加密存储（bcrypt），不可逆
   - 新旧密码不能相同
3. **密码修改**:
   - 仅本地用户可以修改密码
   - AD 用户：提示通过 IT 管理员或 AD 域控制器修改
   - 修改时需要验证当前密码
4. **登录安全**（暂不实现失败锁定）:
   - ⚠️ 登录失败锁定机制暂不实现（原因：主要使用 LDAP 认证，LDAP 服务器自带锁定机制）
   - ✅ 本地用户建议使用强密码策略
   - ✅ 建议生产环境优先使用 LDAP 认证
   - 📋 如未来需要：可在 v2.2 或后续版本添加
5. **Token 有效期**:
   - Access Token: 24 小时
   - Refresh Token: 7 天
6. **状态检查**: INACTIVE、SUSPENDED、TERMINATED 用户无法登录

**UI 要求**:
- 标准登录页面（飞书设计风格）
- 个人设置页面 → 安全设置 → 修改密码
- LDAP/Entra ID用户不显示修改密码选项

---

### 功能9: 区域管理

**优先级**: P1

**功能描述**:
管理公司运营的地理区域，支持区域 CRUD 和配置（时区、货币等）。

**用户故事**:
- 作为 **系统管理员**，我想要 **创建新的区域**，以便 **支持公司在新地区的业务**
- 作为 **系统管理员**，我想要 **配置区域的时区和货币**，以便 **正确处理区域相关的业务**

**接受标准**:
- [x] 支持创建、编辑、删除区域
- [x] 区域代码全局唯一
- [x] 支持配置时区、货币
- [x] 删除前检查是否有关联的组织

**业务规则**:
1. **区域代码**: 使用标准的国家/地区代码（如 CN、US、UAE）
2. **删除保护**: 有关联组织的区域不能删除

**UI 要求**:
- 详见 [UI 交互规范 - 区域管理页面](./05-ui-interaction-spec.md#7-区域管理页面)

---

### 功能10: 岗位管理

**优先级**: P2

**功能描述**:
管理企业的岗位和职级体系，支持岗位 CRUD。

**用户故事**:
- 作为 **HR 管理员**，我想要 **创建岗位和职级**，以便 **标准化员工的岗位信息**

**接受标准**:
- [x] 支持创建、编辑、删除岗位
- [x] 支持岗位级别设置
- [x] 删除前检查是否有用户使用

**业务规则**:
1. **岗位代码**: 岗位代码全局唯一
2. **删除保护**: 有用户使用的岗位不能删除

**UI 要求**:
- 详见 [UI 交互规范 - 岗位管理页面](./05-ui-interaction-spec.md#9-岗位管理页面)

---

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

**优先级**: P0

**版本记录**:
- v2.2：MVP，`alsoAllow` 加法语义，只能开不能收
- **v2.3（当前）**：`allow` 白名单语义 + LOCKED_SET 安全兜底 + 全量工具 per-user 控制 + UI 整体重写

**功能描述**:
管理员在组织管理后台配置「每个角色/用户能用哪些 AI 工具」，配置定时同步到 OpenClaw（企业 AI 助手网关）。v2.3 版本下，授权语义从"加法"升级为"白名单"：除了维护 agent 正常对话的最小工具集（LOCKED_SET）外，所有工具都可以在 Workspace 层被按角色/用户精确控制（既能加也能收）。与 OpenClaw 协同的完整方案见 openclaw 仓 `docs/enterprise-plan/solution/governance/permissions-mvp-plan.md`。

**用户故事**:
- 作为 **系统管理员**，我想要 **批量给所有系统角色默认授权 Employee 基线工具**，以便 **新环境启动时每个角色都有开箱即用的工具集**
- 作为 **系统管理员**，我想要 **在一个 drawer 里一次性勾选某角色的所有工具并保存**，以便 **避免当前按工具逐个弹窗的低效流程**
- 作为 **系统管理员**，我想要 **从某个角色收回 `m365_mail` 工具**，以便 **普通员工不再能让 AI 读他们的邮件**
- 作为 **系统管理员**，我想要 **按组织/部门/角色过滤用户列表**，以便 **在 748 人规模下快速定位目标用户**
- 作为 **系统管理员**，我想要 **查看某个用户最终能用的所有工具及来源（角色链、用户直接授权、LOCKED_SET、全局兜底）**，以便 **回答"为什么 Alice 能用这个工具"**
- 作为 **系统管理员**，我想要 **在系统审计日志里能按 module=ai-tools 过滤看到所有授权变更**，以便 **合规审查谁在什么时候改了什么**

**接受标准**:
- [ ] `getAvailableTools()` 返回 AI 工具全集（~20 项），每个工具带 `category` 和 `locked` 字段
- [ ] LOCKED_SET = `[session_status, sessions_history, sessions_list, sessions_send]`，后端保存 grants 时强制合并，前端渲染 `locked=true` 的 checkbox 为 disabled + 锁图标
- [ ] `GET /ai-tools/grants` 改为按角色聚合返回（一个角色一条记录，`tools[]` 数组）
- [ ] `PUT /ai-tools/grants/role/:roleId` 新增，body `{tools: string[]}` 事务 upsert/delete
- [ ] `PUT /ai-tools/user-grants/:userId` 新增，同上
- [ ] `GET /ai-tools/user-grants-overview` 新增，支持 `org / dept / role / search / hasExtra / hasRevoked / page / pageSize` 过滤
- [ ] `GET /ai-tools/user-effective/:userId` 扩展，返回每个工具的 `sources[]`（角色继承 / 用户直接 / LOCKED_SET / 全局基线）
- [ ] 所有授权变更通过现有 `platform_audit.audit_log` 埋点（`module="ai-tools"`, `isSensitive=true`, `riskLevel=HIGH`）
- [ ] `seed-ai-tool-grants.ts` 脚本：读 Employee 角色的授权作为模板，批量应用到所有系统角色（排除 SyncBot），幂等可重跑
- [ ] `init-itadmin.ts` 初始化钩子集成 seed 流程
- [ ] OpenClaw sync 脚本切换为写 `agent.tools.allow`（白名单语义），硬编码 LOCKED_SET 兜底注入
- [ ] 前端页面一次性重写为 4 tab：角色授权 / 用户授权 / 生效预览 / 右上角"更多"→审计日志快捷跳转

**业务规则**:

1. **两层授权模型保留**：
   - **角色级（AIToolGrant）**：主维度，绑在 Role 上跨组织共享
   - **用户级（AIToolGrantUser）**：例外维度，相对角色基线做加/减（v2.3 新增"减"能力）
   - 最终生效 = LOCKED_SET ∪ (角色授权 ⊕ 用户级调整)，其中 ⊕ 支持加和减

2. **OpenClaw `tools` 配置语义（v2.3 更新）**：
   - 同步脚本改为写 `agents.list[].tools.allow`（**白名单过滤**，支持加和减）
   - 全局 `tools.alsoAllow` 保持为"所有可能授权工具的全集"作为候选池
   - 全局 `tools.sandbox.tools.allow` 保持为"所有可能进 sandbox 的工具全集"作为 sandbox 执行池（运维层配置，不进 Workspace 也不进 sync）
   - 保存时后端强制将 LOCKED_SET 合入用户目标集合，避免意外取消导致 agent 废掉
   - 若 Workspace 为用户返回空集合，sync 脚本写 `allow: [...LOCKED_SET]` 作为最小可用集合（不用 `deny: ["*"]` 因为 LOCKED_SET 必须保留）

3. **LOCKED_SET（不可编辑的 4 个核心对话工具）**：
   - `session_status` — agent 运行时自检
   - `sessions_history` — 读取多轮对话上下文
   - `sessions_list` — 列出会话
   - `sessions_send` — 发送回复给用户
   - 这 4 个是 OpenClaw `messaging` profile 的最小集，**取消任一都会破坏正常 Teams DM 对话**
   - 前端：`available-tools` 返回的 `locked=true` 项渲染为 disabled checkbox + 锁图标 + tooltip"系统基础对话工具，不可取消"
   - 后端：`PUT grants/role` 和 `PUT grants/user` 保存时强制将 LOCKED_SET 合入目标集合，接口不相信前端传入

4. **可用工具清单数据源**：
   - 全集静态配置在 `backend/src/modules/organization/ai-tools/available-tools.config.ts`
   - 每项包含 `{name, label, description, category, locked}`
   - 分类：`core`（locked，对话基础）、`fs`、`runtime`、`memory`、`web`、`media`、`automation`、`browser`、`productivity`（M365）
   - 与 OpenClaw `src/agents/tool-catalog.ts` 保持同步（变更需双边更新）

5. **默认角色基线**：
   - 新环境初始化时，所有系统角色（SyncBot 除外）都默认继承 Employee 的授权集合
   - Employee 默认集合：core 全量（locked 兜底）+ fs 全量 + runtime 全量 + memory 全量 + web 全量 + media 全量 + automation 全量 + browser 全量 + M365 全量
   - 换言之：所有可用工具初始都打开，按需由 admin 收回
   - 通过 `seed-ai-tool-grants.ts` 脚本和 `init-itadmin.ts` 钩子两路保证

6. **权限码**：`ai_tool:read`（查询）和 `ai_tool:manage`（写操作）

7. **审计**：复用现有 `platform_audit.audit_log`，不做独立 tab
   - `module="ai-tools"`, `action=CREATE|UPDATE|DELETE`
   - `entityType="role-grant"|"user-grant"`, `entityId=roleId|userId`
   - `oldValue`/`newValue` 存完整工具集合，`changes` 存 `{added: [...], removed: [...]}`
   - `isSensitive=true`, `riskLevel=HIGH`（权限变更敏感）
   - AI 工具页右上角"更多"下拉有快捷入口跳到 `/audit-logs?module=ai-tools`

**关联文档**：
- [API 文档 - 11. AI 工具授权管理 API](./07-api.md#11-ai-工具授权管理-api)
- [数据模型 - AI 工具授权](./06-data-model.md#ai-工具授权v22-权限-mvp)
- [UI 交互规范 - AI 工具授权管理页面](./05-ui-interaction-spec.md#11-ai-工具授权管理页面)
- [测试场景 - AI 工具授权](./09-test-scenarios.md#ai-工具授权-api-测试)
- 权限 MVP 总体方案：openclaw 仓 `docs/enterprise-plan/solution/governance/permissions-mvp-plan.md`

**已知边界（v2.3 仍不在范围）**：
- ❌ 部门级授权（v2.3 只有角色级和用户级；部门作为过滤维度而非授权维度）
- ❌ 实时推送（依赖 5 分钟定时 cron 同步）
- ❌ 工具级 deny 单独字段（v2.3 通过 allow 白名单实现等效"不给"）
- ❌ 按模型/provider 的细分授权（未来迭代）

**UI 要求**:
- 详见 [UI 交互规范 - AI 工具授权管理页面](./05-ui-interaction-spec.md#11-ai-工具授权管理页面)

---

### 功能 13: Entra ID SSO 登录（v2.4 新增）

**优先级**: P0
**关联工单**: #334
**版本**: v2.4

> **⚠️ 决策推翻**：本功能**推翻** v2.1.1 「Entra ID 仅作同步通道、不作身份源、登录走 LDAP/AD」的决策。
> v2.4 起 Entra ID **既是同步通道也是 SSO 身份源**，与本地密码登录通道**双通道并存**。

**目标**：

让已经使用公司 Microsoft（Entra ID）账号的员工，能够在 FFOA 登录页点击「使用 Microsoft 登录」，通过浏览器跳转完成 OIDC 单点登录，免去在本系统维护第二份密码。

**功能描述**：

实现基于 OIDC 授权码流程（Authorization Code Flow + PKCE）的 Microsoft Entra ID SSO 登录。前端跳转到 Entra authorize endpoint，用户在 Microsoft 域内完成认证（含 MFA / 条件访问策略，本系统不干预），Entra 回调本系统 `/auth/sso/callback`，本系统校验 ID token 并签发自有 JWT，前端凭 JWT 进入业务页面。

**用户故事**：

- 作为 **AD 用户**，我想要 **在登录页点「使用 Microsoft 登录」一键进入系统**，以便 **不用记 FFOA 的密码**
- 作为 **新入职员工（Entra 已建账号但 FFOA 未同步）**，我想要 **首次走 SSO 时自动建 FFOA 账号**，以便 **入职当天就能用**
- 作为 **HR 管理员**，我想要 **在审计日志看到所有 SSO 登录与自动建账号记录**，以便 **回溯人员变动**
- 作为 **本地用户**，我想要 **保留用户名/密码登录通道**，以便 **在 Entra 不可用时仍能进入系统**

**In-scope（本期实现）**：

- ✅ `/api/v1/auth/sso/start` 与 `/api/v1/auth/sso/callback` 两个端点
- ✅ OIDC 授权码 + **PKCE 必须**（confidential client 也强制）；`sso_state` / `sso_nonce` / `sso_redirect` / `sso_code_verifier` 4 个 HttpOnly cookie 防 CSRF / replay，TTL 15min
- ✅ **OIDC 实现库**：`openid-client`（OpenID Foundation 官方，业界事实标准；自带 discovery / JWKS rotation / PKCE / nonce / clock skew ±5min）；**不**用已弃用的 `passport-azure-ad`，**不**手写 `jose`
- ✅ ID token 校验：签名（JWKS 5min 缓存 + kid miss 重拉 + 30s cooldown） / issuer（用 discovery `metadata.issuer` 模板替换 `{tenantid}` 后严格字符串等于比对，不可硬编码） / audience（= `AZURE_CLIENT_ID`） / nonce（vs cookie 比对） / clock skew ±5min / `tid` claim
- ✅ 身份匹配：email claim（应用层 lower-case 写入 + 查询）精确匹配 `User.email`
- ✅ JIT 建账号：email 域名命中 `SSO_ALLOWED_DOMAINS` → 建 User（Employee 角色 + 默认组织从 `SSO_JIT_DEFAULT_ORG_ID` env 读取；`username = lower(email)` / `region` 继承自默认 org `Organization.region`（默认 `'CN'`） / 建 1 行 `UserRole` 关联 Employee + 默认 org / **不建** `UserDepartment` 行，部门由 admin 后续手工分配）
- ✅ 首次绑定回填：已有 email 用户首次走 SSO，回填 `externalId`（Entra `oid` claim）+ `externalSource = 'entra'`
- ✅ **LDAP 历史 externalId 自动升级**：`externalSource='ldap'` 且 `externalId` 非空且 ≠ 当前 `oid` → 允许覆盖为 entra `oid`，并写 audit `SSO_BINDING_UPGRADED_FROM_LDAP`（避免与 Entra 真冲突搞混）
- ✅ 签发本系统 JWT（复用现有 `JwtService`，含组织上下文、角色、权限；事务提交后再签）
- ✅ **5 个新审计事件**：`SSO_LOGIN_SUCCESS` / `SSO_JIT_CREATED` / `SSO_BINDING_FILLED` / `SSO_BINDING_UPGRADED_FROM_LDAP` / `SSO_BINDING_CONFLICT`
- ✅ **callback 响应协议**：成功 → 302 跳转至 `${sso_redirect}#accessToken=<jwt>&refreshToken=<jwt>`（URL fragment 注入，前端 `/sso/landing` 路由读 `location.hash` → 写 localStorage → `history.replaceState` 清 hash → 跳业务页）；失败 → 302 跳转至 `/login?ssoError=<CODE>`（登录页 useEffect 读 query → showToast → `history.replaceState` 清 query）；**不**使用 JSON 响应体（302 + body 浏览器不暴露给 JS），**不**用 HttpOnly Cookie（不兼容现有 localStorage 存 token）
- ✅ **默认跳转目标**：未带 `sso_redirect` 时跳 `/overview`（与 `frontend/src/lib/auth-redirect.ts` `DEFAULT_POST_LOGIN_PATH` 一致）
- ✅ **数据库 schema 增量**：本 PR 含 1 个 prisma migration —— `ALTER TYPE platform_audit.AuditAction` 新增 5 个枚举值（5 个 SSO_* audit 事件）；含一次性 backfill SQL `UPDATE platform_iam.users SET email = LOWER(email) WHERE email != LOWER(email)`（幂等）；推翻 v2.4 早期"无 schema 变更"声明
- ✅ 前端登录页新增「使用 Microsoft 登录」按钮（位置在用户名/密码表单下方，分隔线区分）
- ✅ 本地密码登录通道、LDAP 登录通道完全保留

**Out-of-scope（本期不做，二期承诺）**：

- ❌ **Entra 禁用立即失效**：本期接受 ≤ 24h 同步窗口，依赖现有 Entra 同步 cron；二期通过 SCIM 协议实现 < 5 分钟失效（独立工单跟踪）
- ❌ **后台运维兜底**：本期 SSO 绑定关系**只读展示**，不支持「强制解绑」「强制 invalidate session」操作（运营如需，临时走数据库工单）
- ❌ **MFA / 条件访问策略校验**：完全相信 Entra 策略，本系统**不**额外校验 `amr` / `acr` claim
- ❌ **移除 entra.service.ts ROPC 流程**：v2.4 OIDC 与现有 ROPC **并存**，是否下线 ROPC 由二期 SCIM 工单一起评估
- ❌ **变更 `User.source` 字段语义**：SSO 成功**不修改** `source`，避免影响现有同步逻辑与历史数据
- ❌ **不为缺 email claim 的 B2B guest / 纯 UPN 账号做 fallback**：当 ID token 无 `email` claim 时一律 400 `SSO_EMAIL_MISSING`，二期 SCIM 评估其它身份属性 fallback 策略

**关键边界**：

1. **双通道并存（铁律）**：本地密码登录链路 100% 保持，新增的 SSO 通道是**并行第二通道**，不修改现有 `/auth/login` 端点
2. **passwordHash 保留**：双通道并存，不删 `User.passwordHash`，本地密码通道仍以 `passwordHash` 为事实源
3. **不转发 Entra token**：前端拿到的是本系统签发的 JWT，**不**包含 Entra access/id token；下游服务无需感知 Entra
4. **身份匹配只用 email**：不用 `oid` 做主键匹配（避免 Entra 租户迁移 / 账号重建导致 `oid` 变化的运维风险）；`oid` 只作辅助回填
5. **JIT 域名白名单（强制）**：JIT 仅在 `SSO_ALLOWED_DOMAINS` 命中时触发；非白名单域名 email 即使 Entra 认证成功也拒绝（403 `SSO_DOMAIN_NOT_ALLOWED`），防止外租户用户误入
6. **email 冲突拒绝**：同 email 已绑定其它 `externalId` → 拒绝并审计（409 `SSO_BINDING_CONFLICT`），不静默覆盖
7. **状态检查复用**：`INACTIVE` / `SUSPENDED` / `TERMINATED` 用户 SSO 登录同样被拒，与本地登录策略一致
8. **session 时效一致**：SSO 签发的 JWT 与本地登录签发的 JWT 走同一套 `Access Token 24h / Refresh Token 7d` 策略

**审计事件**：

| 事件 | 触发时机 | 主要字段 |
|---|---|---|
| `SSO_LOGIN_SUCCESS` | SSO 登录成功（任意路径） | `userId` / `email` / `externalId` / `path: existing\|jit\|binding_filled\|ldap_upgraded` / `entraTid` |
| `SSO_JIT_CREATED` | JIT 自动建账号 | `userId` / `email` / `externalId` / `defaultOrgId` / `entraTid` |
| `SSO_BINDING_FILLED` | 首次回填 externalId | `userId` / `email` / `externalId` / `previousExternalId: null` / `entraTid` |
| `SSO_BINDING_UPGRADED_FROM_LDAP` | LDAP 历史绑定升级为 Entra | `userId` / `email` / `previousExternalId: <LDAP_DN>` / `newExternalId: <oid>` / `entraTid` |
| `SSO_BINDING_CONFLICT` | email 已绑定其它 Entra oid（非 LDAP） | `email` / `existingExternalId` / `attemptedExternalId` / `entraTid` |

**接受标准**：

- [ ] 域内员工点「使用 Microsoft 登录」可在 Entra 完成认证后进入 FFOA，无需输入 FFOA 密码
- [ ] 已有 FFOA 账号（email 匹配）的用户首次走 SSO，自动回填 `externalId` / `externalSource`，并触发 `SSO_BINDING_FILLED`
- [ ] **LDAP 历史 `externalId` 自动升级**：已有用户 `externalSource='ldap'` 且 `externalId ≠ oid` → 覆盖为 entra `oid`，触发 `SSO_BINDING_UPGRADED_FROM_LDAP`，**不**返 409
- [ ] 白名单内新邮箱用户走 SSO，自动建 User（Employee 角色 + 默认组织从 `SSO_JIT_DEFAULT_ORG_ID` 取值；`username = lower(email)` / `region` 继承自默认 org / 1 行 `UserRole` / 不建 `UserDepartment`），并触发 `SSO_JIT_CREATED`
- [ ] 白名单外域名走 SSO，返回 403 `SSO_DOMAIN_NOT_ALLOWED`
- [ ] state/nonce 不匹配 / 过期，返回 401 `SSO_TOKEN_INVALID`
- [ ] Entra discovery/token endpoint 5xx 或超时，返回 503 `SSO_PROVIDER_UNAVAILABLE`
- [ ] **Entra error query 处理**：callback 命中 `query.error` 非空 → `access_denied` 返 403 `SSO_USER_CANCELLED`；`consent_required`/`interaction_required` 返 403 `SSO_CONSENT_REQUIRED`；其它返 502 `SSO_PROVIDER_REJECTED`
- [ ] 本地密码登录通道行为 100% 不变（回归测试通过）
- [ ] **`INACTIVE` / `SUSPENDED` / `TERMINATED` 用户 SSO 被拒** —— callback 命中 User 后检查 `status === ACTIVE`，否则 403 `IAM_USER_SUSPENDED`（复用现有错误码，不自创 `AUTH_USER_DISABLED`），与本地登录一致
- [ ] 启动期 `SsoConfigValidator` fail-fast：`SSO_ALLOWED_DOMAINS` 非空时 `SSO_JIT_DEFAULT_ORG_ID` 必填且存在；`AZURE_TENANT_ID` 必须为 GUID（regex `/^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$/`，不接受 `common`/`organizations`/`consumers`）

**业务规则**：

1. **协议**：OIDC 授权码 flow + **PKCE（必须，confidential client 也强制）**；自签本系统 JWT，不转发 Entra token
2. **身份匹配**：ID token `email` claim → 应用层 lower-case → 精确匹配 `User.email`（lower-case 也写入 DB，不引入 citext / 不动 schema collation；一次性 backfill SQL 见数据模型文档）
3. **JIT 策略**：email 不存在 + 域名命中 `SSO_ALLOWED_DOMAINS` → 建 User（`status=ACTIVE` / `username = lower(email)` / Employee 角色（1 行 `UserRole`，绑 `SSO_JIT_DEFAULT_ORG_ID`） / `passwordHash=null` / `source=ENTRA` / `externalId=<oid>` / `externalSource=entra` / `region` 继承默认 org 的 `Organization.region`（默认 `'CN'`） / **不建** `UserDepartment` 行）；**`SSO_JIT_DEFAULT_ORG_ID` 在 `SSO_ALLOWED_DOMAINS` 非空时为必填**，启动期 fail-fast 校验；运行时 JIT 触发时再查一次（`WHERE deletedAt IS NULL`），缺失 → 503 `SSO_PROVIDER_UNAVAILABLE`
4. **首次绑定回填**：email 已存在但 `externalId` 为 null → 回填 `externalId` + `externalSource='entra'`，**不**修改 `source` 字段；并发用 CAS `UPDATE WHERE id=$1 AND externalId IS NULL`，受影响行 = 0 时重查
5. **冲突处理（细化）**：
   - `externalSource='entra'` 且 `externalId` 非空且 ≠ 当前 `oid` → 真冲突，拒绝（409 `SSO_BINDING_CONFLICT`），运营介入
   - `externalSource='ldap'` 且 `externalId` 非空且 ≠ 当前 `oid` → **允许覆盖**（LDAP 同步用户首次走 SSO），写 audit `SSO_BINDING_UPGRADED_FROM_LDAP`
6. **MFA / CA / clock skew**：相信 Entra MFA / 条件访问策略，不校验 `amr` / `acr`；ID token 时间校验容忍 **±5min**（`openid-client` 默认）
7. **token 时效**：与本地登录通道一致（Access 24h / Refresh 7d）；state/nonce/code_verifier cookie TTL `15min`（容纳 Entra 条件访问 + MFA 慢操作；过期后 callback 自然失败返 401 `SSO_TOKEN_INVALID`）
8. **事务边界（spec deviation 注）**：DB 回填 / JIT 业务写入在 `prisma.$transaction()` 内；**失败路径 audit**（`SSO_BINDING_CONFLICT` / `LOGIN_FAILED`）在 tx 内即时写（合规：业务回滚但 audit 必须保留）；**成功路径 audit**（`SSO_LOGIN_SUCCESS` / `SSO_JIT_CREATED` / `SSO_BINDING_FILLED` / `SSO_BINDING_UPGRADED_FROM_LDAP`）累积后**在 tx 提交后写**（已知 audit 丢失窗口 ≤ 进程崩溃间隙；接受代价：避开 AuditService 独立 prisma 连接看不到未提交 user 的 FK 违例，详见 `.learnings/ERRORS/ERR-20260519-010`）。JWT 在事务提交后签发；事务失败 → 503 `SSO_PROVIDER_UNAVAILABLE` + 前端引导重试。二期 AuditService tx-aware 改造后此项收口为「全部 audit 同事务」。

**二期承诺（独立工单跟踪）**：

- **SCIM（System for Cross-domain Identity Management）**：实现 Entra → FFOA 实时用户生命周期同步（创建 / 更新 / 禁用 / 删除），关闭本期 ≤ 24h 同步窗口；Entra 禁用 → FFOA session 立即失效（< 5 分钟）
- **后台运维强制操作**：管理员可强制解绑 SSO 关联、强制 invalidate 指定用户的所有 session

**环境变量**：

- 沿用：`AZURE_TENANT_ID`（必须为 GUID，启动期 regex 校验，不接受 `common`/`organizations`/`consumers`） / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET`（已在 `.env.example`）
- 新增：`AZURE_REDIRECT_URI`（每环境一套，必须与 Entra 应用注册一致）、`SSO_ALLOWED_DOMAINS`（逗号分隔域名白名单）、`SSO_JIT_DEFAULT_ORG_ID`（`SSO_ALLOWED_DOMAINS` 非空时必填，启动期 fail-fast）

**错误码（本期新增 8 个，详见 08-error-codes.md）**：

| 错误码 | HTTP | 触发条件 |
|---|---|---|
| `SSO_DOMAIN_NOT_ALLOWED` | 403 | email 域名不在 `SSO_ALLOWED_DOMAINS` |
| `SSO_TOKEN_INVALID` | 401 | state/nonce 不匹配或过期 / ID token 签名/iss/aud/exp/nonce 校验失败 / Entra `invalid_grant`（code 已用或过期） |
| `SSO_EMAIL_MISSING` | 400 | ID token 无 `email` claim（B2B guest / 纯 UPN 账号） |
| `SSO_BINDING_CONFLICT` | 409 | `externalSource='entra'` 且 `externalId` 已被其它 `oid` 占用（**LDAP 来源不触发此错误，走自动升级路径**） |
| `SSO_PROVIDER_UNAVAILABLE` | 503 | Entra discovery/token endpoint 5xx 或超时 / 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` 为其它值（provider 端拒绝） |

**UI 要求**：

- 登录页（`frontend/src/app/(auth)/login`）：用户名/密码表单下方加分隔线 + 「使用 Microsoft 登录」按钮（Microsoft 官方品牌按钮规范）
- 错误态：SSO 失败回跳后展示 `SSO_*` 错误码对应的本地化提示（中文 `zh` / 英文 `en` 双语，i18n key 位于 `frontend/src/locales/auth/{zh,en}.ts` 嵌套 TS 对象），不暴露 Entra 原始错误
- 详见 [UI 交互规范 - 登录页面](./05-ui-interaction-spec.md)

---

## 🔄 业务流程

### 主要流程：权限申请与审批

```mermaid
graph TD
    A[员工申请权限] --> B[部门经理审批]
    B -->|拒绝| C[结束]
    B -->|同意| D[HR 管理员审批]
    D -->|拒绝| C
    D -->|同意| E[系统管理员分配角色]
    E --> F[指定组织和角色]
    F --> G[权限立即生效]
    G --> H[记录审计日志]
    H --> I[通知申请人]
```

### 流程说明

1. **申请提交**: 员工通过系统提交权限申请
2. **部门经理审批**: 直属上级审批是否需要该权限
3. **HR 审批**: HR 管理员审批是否符合公司政策
4. **权限分配**: 系统管理员根据审批结果分配角色（v2.1 需指定组织）
5. **生效**: 权限立即生效，用户可以使用新权限
6. **审计**: 所有权限变更记录审计日志

---

### 主要流程：组织架构调整

```mermaid
graph TD
    A[HR 提出调整需求] --> B{调整类型}
    B -->|创建部门| C[设置部门信息]
    B -->|调整层级| D[修改父部门]
    B -->|调整人员| E[更新部门归属]
    C --> F[检查约束条件]
    D --> F
    E --> F
    F -->|验证失败| G[提示错误]
    F -->|验证通过| H[保存变更]
    G --> A
    H --> I[更新组织架构树]
    I --> J[通知相关人员]
    J --> K[记录审计日志]
```

### 流程说明

1. **需求确认**: HR 管理员确定组织架构调整需求
2. **执行调整**: 创建部门、调整层级、或调整人员归属
3. **约束检查**: 系统验证业务规则（如部门归属、汇报关系等）
4. **保存变更**: 验证通过后保存数据
5. **更新展示**: 刷新组织架构树展示
6. **通知**: 通知受影响的员工和管理者
7. **审计**: 记录组织架构变更日志

---

## 📐 非功能需求

### 性能要求

| 场景 | 目标响应时间 | 并发用户 |
|------|------------|---------|
| 用户登录 | < 500ms | 100 并发 |
| 用户搜索 | < 200ms | 50 并发 |
| 权限查询 | < 100ms | 200 并发 |
| 流程角色解析 | < 100ms | 100 并发 |
| 组织架构树加载 | < 300ms | 50 并发 |
| Entra ID 同步（1000 用户）| < 5min | 1 并发 |

### 安全要求

- **API 权限守卫**: 所有管理类 API 必须通过权限守卫
- **操作边界**: 用户自服务和管理员操作边界分清
- **敏感操作审计**: 角色分配、权限变更、状态变更必须记录审计日志
- **密码安全**: 
  - 使用 bcrypt 加密存储
  - 最小长度 8 位
  - 至少包含 2 种字符类型
- **Token 安全**:
  - 使用 JWT 签名
  - 支持 Token 刷新
  - 支持强制登出

### 合规要求

- **数据保留**: TERMINATED 用户数据保留至少 7 年
- **审计日志**: 
  - 权限变更日志保留 7 年
  - 组织架构变更日志保留 5 年
  - 操作日志保留 90 天
- **隐私保护**: 离职员工信息支持脱敏处理
- **访问控制**: 所有敏感操作必须经过权限验证

### 兼容性要求

- **浏览器**: Chrome、Firefox、Safari、Edge（最新两个版本）
- **移动端**: 支持响应式设计，适配移动浏览器
- **屏幕分辨率**: 最小 1366x768
- **外部系统**: 
  - Microsoft Entra ID (Graph API v1.0) - 用户信息同步（仅同步，同步的用户使用 LDAP 认证）
  - LDAP v3 - 身份认证（AD 用户登录）
  - Active Directory 2016+ - 统一的 AD 服务器（Entra ID 和 LDAP 都连接到同一个 AD）

---

## 📊 数据需求

### 核心数据实体

详细的数据模型请参见 [数据模型文档](./06-data-model.md)

| 实体 | 主要字段 | 说明 |
|------|---------|------|
| User | username, email, displayName, status, source, region | 用户基本信息；source 表示身份源（LOCAL 本地用户 / LDAP AD用户）；region 为冗余字段，用于性能优化 |
| Organization | code, name, primaryRegionId, legalName | 组织实体（v2.0） |
| Department | code, name, organizationId, parentId | 部门树 |
| UserDepartment | userId, departmentId, organizationId, isPrimary, positionId, managerId | 多部门归属（v2.0 冗余字段） |
| Role | code, name, permissions | 系统角色 |
| UserRole | userId, roleId, organizationId | 角色分配（v2.1 新增 organizationId） |
| Permission | code, name, resource, action, scope | 权限点（v2.1 预留 scope，v2.2 启用） |
| WorkflowRole | code, name, ruleType, ruleConfig | 流程角色 |
| Region | code, name, timezone, currency | 地理区域 |
| Position | code, name, level, category | 岗位职级 |

### 数据量预估

| 数据类型 | 初期 | 1年后 | 3年后 |
|---------|------|-------|-------|
| 用户 | 500 | 1,000 | 2,000 |
| 组织 | 5 | 10 | 15 |
| 部门 | 50 | 100 | 200 |
| 角色 | 20 | 30 | 50 |
| 审计日志 | 10,000/月 | 30,000/月 | 50,000/月 |

---

## 🔗 依赖关系

### 依赖的模块/系统

| 模块/系统 | 依赖内容 | 影响 |
|----------|---------|------|
| PostgreSQL | 数据存储 | 不可用则系统完全无法使用 |
| Redis | Token 缓存、会话管理、权限缓存 | 不可用则性能下降，但功能可用 |
| Microsoft Entra ID | 用户信息同步（仅同步，同步的用户使用 LDAP 认证） | 不可用则无法同步，但不影响本地用户和登录 |
| LDAP/AD | 身份认证（AD 用户登录） | 不可用则 AD 用户无法登录，本地用户不受影响 |
| 审计模块 | 审计日志 | 不可用则审计日志丢失 |

### 被依赖的模块/系统

| 模块/系统 | 依赖内容 | 影响 |
|----------|---------|------|
| 所有业务模块 | 用户信息、权限验证、Guards | 不可用则所有业务模块无法使用 |
| 审批引擎 | 流程角色解析、组织关系 | 不可用则审批流程无法确定审批人 |
| 表单引擎 | 用户选择、部门选择 | 不可用则表单无法选择人员 |
| KPI 模块 | 用户信息、汇报关系 | 不可用则无法分配 KPI |
| OKR 模块 | 组织架构、部门信息 | 不可用则无法按组织查看 OKR |

---

## ⚠️ 风险与假设

### 假设

1. **假设1**: Microsoft Entra ID API 保持稳定，不会频繁变更
2. **假设2**: 企业组织架构变更频率不高（平均每月 < 10 次重大调整）
3. **假设3**: 用户数量在未来 3 年内不会超过 5,000 人
4. **假设4**: 每个用户的部门归属不超过 5 个
5. **假设5**: HR 管理员熟悉系统操作，能够正确配置权限

### 风险

| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|---------|
| Entra ID 同步失败导致用户信息不一致 | 中 | 中 | 提供手动同步功能，支持同步日志查看和错误处理 |
| 复杂的多部门归属导致用户混淆 | 低 | 中 | 提供清晰的 UI 展示，主部门醒目标记 |
| 权限配置错误导致数据泄露 | 高 | 低 | 所有权限变更记录审计日志，支持权限审查报告 |
| 大规模组织架构调整导致性能问题 | 中 | 低 | 使用 organizationId 冗余字段优化查询，避免递归 |
| LDAP/AD 服务不可用影响登录 | 高 | 低 | 支持多种认证方式，LDAP 故障时可用本地认证 |
| 流程角色解析失败导致审批阻塞 | 高 | 中 | 实现兜底策略，提供管理员干预机制 |

---

## 🗓️ 里程碑

| 里程碑 | 日期 | 交付物 | 状态 |
|--------|------|--------|-----|
| v1.0 需求评审 | 2024-11-01 | PRD 文档 | ✅ 已完成 |
| v1.0 设计评审 | 2024-11-05 | 架构设计、数据模型 | ✅ 已完成 |
| v1.0 开发完成 | 2024-11-30 | 核心功能代码 | ✅ 已完成 |
| v1.0 测试完成 | 2024-12-10 | 测试报告 | ✅ 已完成 |
| v1.0 上线发布 | 2024-12-15 | 生产环境 | ✅ 已完成 |
| v2.0 架构升级 | 2025-12-20 | 独立 Organization 表 | ✅ 已完成 |
| v2.1 权限优化 | 2025-12-26 | 组织级权限隔离 | ✅ 已完成 |
| v2.1.1 文档完善 | 2025-12-26 | 完整功能文档 | ✅ 已完成 |
| v2.2 功能增强 | 2026-01-15 | Scope 系统、细粒度权限控制 | 📋 计划中 |

---

## 🔍 待讨论问题

### Open Questions

1. **Q: v2.2 是否需要支持自定义流程角色脚本？**  
   A: 待产品评审，需要评估安全风险和开发成本

2. **Q: 是否需要支持用户自助申请权限？**  
   A: 建议 v2.2 实现，需要配合审批流程

3. **Q: 多租户 SaaS 支持的优先级？**  
   A: 长期规划，当前仅预留 tenantId 字段，v3.0 考虑实现

4. **Q: 是否需要支持用户导入模板？**  
   A: v0.1 支持**成员关系导入**（用户-部门关系、岗位、主管），v2.2 实现**用户批量创建导入**

5. **Q: 流程角色兜底策略的具体实现方式？**  
   A: 待技术评审，需要确定各种兜底策略的优先级

---

## 📚 参考资料

### 内部文档

- [架构设计文档](./03-architecture.md) - 技术架构和实现细节
- [数据模型文档](./06-data-model.md) - 完整数据库设计
- [API 接口文档](./07-api.md) - 完整 API 定义
- [UI 交互规范](./05-ui-interaction-spec.md) - 前端 UI 设计
- [用户场景文档](./02-user-journey.md) - 用户旅程和操作流程
- [状态机文档](./04-state-machine.md) - 状态流转规则
- [错误码文档](./08-error-codes.md) - 错误码定义
- [测试场景文档](./09-test-scenarios.md) - 测试用例
- [变更日志](./99-changelog.md) - 版本历史

### 外部资源

- [Microsoft Graph API 文档](https://docs.microsoft.com/en-us/graph/api/overview) - 用户信息同步
- [LDAP 协议规范 RFC 4511](https://tools.ietf.org/html/rfc4511) - 身份认证
- [JWT 标准 RFC 7519](https://tools.ietf.org/html/rfc7519)
- [RBAC 模型标准](https://csrc.nist.gov/projects/role-based-access-control)

### 竞品分析

- 钉钉组织架构管理
- 飞书组织管理
- Microsoft 365 Admin Center
- Okta Identity Management

---

## ✅ 评审记录

| 日期 | 参与人 | 评审结论 | 备注 |
|------|--------|---------|------|
| 2024-11-01 | 产品团队、技术团队 | Approved | v1.0 初始版本通过 |
| 2025-12-20 | 产品团队、技术团队 | Approved | v2.0 架构升级通过 |
| 2025-12-26 | 产品团队、技术团队 | Approved | v2.1 权限优化通过 |
| 2025-12-26 | 产品团队、技术团队 | Approved | v2.1.1 功能细节补充通过 |
| 2026-01-05 | 产品团队、技术团队 | Updated | v2.1.2 登录安全策略调整 |
| 2026-01-05 | 产品团队、技术团队 | Updated | v2.1.3 Scope 系统推迟到 v2.2 |
| 2026-01-05 | 产品团队、技术团队 | Updated | v2.1.22 ~ v2.1.25 身份源优化、定时同步、流程优化 |

---

**最后更新**: 2026-05-19  
**产品经理**: FFOA 产品团队  
**审批人**: FFOA 技术委员会  
**文档版本**: v2.4  
**模板符合度**: ✅ 100%
