# 用户与组织架构管理 - 状态机文档

> **版本**: v2.1.26  
> **最后更新**: 2026-01-25  
> **维护者**: FFOA 开发团队

---

## 📋 文档变更记录

| 版本 | 日期 | 修改人 | 修改内容 |
|------|------|--------|---------|
| 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 | 补充组织/部门/岗位/身份源状态管理 |
| v2.1.25 | 2026-01-05 | FFOA Team | 同步 PRD v2.1.25：身份源简化为两种 |

---

## 📋 概述

### 状态机用途

本文档定义用户（User）、角色（Role）、部门归属（UserDepartment）等核心实体的状态流转规则，包括合法流转路径、权限控制、前端展示规范和API设计。

### 核心对象

本模块涉及的核心实体状态包括：

|| 对象类型 | 状态字段 | 数据库表 | Schema | 文档章节 |
||---------|---------|---------|--------|---------|
|| **用户** | `status` (UserStatus) | `users` | `platform_iam` | §2-5 |
|| **角色** | `enabled` (Boolean) | `roles` | `platform_iam` | §9 |
|| **部门归属** | `leftAt` (DateTime?) | `user_departments` | `corp_hr` | §6 |
|| **组织** | `status` (Status?) | `organizations` | `corp_hr` | §10 ⭐ NEW |
|| **部门** | `status` (Status?) | `departments` | `corp_hr` | §11 ⭐ NEW |
|| **岗位** | `enabled` (Boolean) | `positions` | `corp_hr` | §12 ⭐ NEW |
|| **身份源** | `source` (UserSource) | `users.source` | `platform_iam` | §13 ⭐ NEW |

**状态枚举**:
- **UserStatus**: ACTIVE, INACTIVE, SUSPENDED, TERMINATED
- **Role**: enabled (true/false)
- **UserDepartment**: 通过 `joinedAt` 和 `leftAt` 时间字段管理
- **Organization/Department**: status (ACTIVE/INACTIVE) - 待确认 ⭐
- **Position**: enabled (true/false)
- **UserSource**: LOCAL, LDAP (v2.1.25: 仅两种)

---

## 🎯 用户状态定义

### 状态列表

> **业务规则定义**: 详见 [PRD - 用户状态枚举](./01-prd.md#status用户状态)  
> 本节重点描述状态流转规则和技术实现

| 状态代码 | 说明 | 可登录 | 终态 | 技术约束 |
|---------|------|--------|-----|---------|
| `ACTIVE` | 激活 | ✅ | ❌ | 创建用户的默认状态 |
| `INACTIVE` | 停用 | ❌ | ❌ | 可恢复到 ACTIVE |
| `SUSPENDED` | 暂停 | ❌ | ❌ | 可恢复到 ACTIVE |
| `TERMINATED` | 离职 | ❌ | ✅ | 不可逆转，仅软删除 |

**说明**：
- ✅ 终态（不可再变更）/ ❌ 中间态（可继续流转）
- 完整的业务含义和使用场景请参见 [PRD](./01-prd.md#功能1-用户管理)

---

## 🔄 用户状态流转图

### Mermaid 状态图

```mermaid
stateDiagram-v2
    [*] --> ACTIVE : 创建用户
    
    ACTIVE --> INACTIVE : 管理员停用
    ACTIVE --> SUSPENDED : 违规暂停
    ACTIVE --> TERMINATED : 员工离职
    
    INACTIVE --> ACTIVE : 管理员激活
    INACTIVE --> TERMINATED : 离职处理
    
    SUSPENDED --> ACTIVE : 恢复正常
    SUSPENDED --> TERMINATED : 离职处理
    
    TERMINATED --> [*] : 软删除
    
    note right of ACTIVE
        正常状态
        - 可登录
        - 可使用所有功能
        - 可被搜索和查看
    end note
    
    note right of INACTIVE
        临时停用
        - 无法登录
        - 保留所有数据
        - 可随时恢复
    end note
    
    note right of SUSPENDED
        账号暂停
        - 无法登录
        - 因违规等原因
        - 需审批后恢复
    end note
    
    note right of TERMINATED
        最终状态
        - 无法登录
        - 历史数据保留
        - 不可恢复
    end note
```

---

## ✅ 合法状态流转

> **业务规则**: 详见 [PRD - 用户状态流转业务规则](./01-prd.md#业务规则)  
> 本节重点描述技术实现和前端交互

### 流转规则表

| 当前状态 | 可流转到 | 触发动作 | 执行者 | 前置条件 | API |
|---------|---------|---------|--------|---------|-----|
| `[*]` | `ACTIVE` | 创建用户 | HR/管理员 | `user:create` 权限 | POST /users |
| `ACTIVE` | `INACTIVE` | 停用 | 管理员 | `user:update` 权限，不能停用自己 | PATCH /users/:id/status |
| `ACTIVE` | `SUSPENDED` | 暂停 | 管理员 | `user:suspend` 权限，需要理由 | PATCH /users/:id/status |
| `ACTIVE` | `TERMINATED` | 离职 | HR/管理员 | `user:terminate` 权限，需要日期和理由 | PATCH /users/:id/status |
| `INACTIVE` | `ACTIVE` | 激活 | 管理员 | `user:update` 权限 | PATCH /users/:id/status |
| `INACTIVE` | `TERMINATED` | 离职 | HR/管理员 | `user:terminate` 权限 | PATCH /users/:id/status |
| `SUSPENDED` | `ACTIVE` | 恢复 | 管理员 | `user:suspend` 权限，需要说明 | PATCH /users/:id/status |
| `SUSPENDED` | `TERMINATED` | 离职 | HR/管理员 | `user:terminate` 权限 | PATCH /users/:id/status |

**参考文档**:
- 业务规则详细说明: [01-prd.md#业务规则](./01-prd.md#业务规则)
- API 接口定义: [07-api.md#PATCH-users-id-status](./07-api.md)

### 详细流转逻辑

#### 1. ACTIVE → INACTIVE（停用）

**触发条件**:
- 管理员手动停用
- 长期未使用（可选自动化规则）
- 临时离开（如休假、借调）

**前置检查**:
- ✅ 操作者拥有 `user:update` 权限
- ✅ 目标用户不是自己
- ✅ 目标用户不是最后一个管理员

**执行动作**:
1. 更新用户状态为 INACTIVE
2. 更新 `updatedAt` 时间戳
3. 记录审计日志（操作者、原因、时间）
4. 可选：发送通知给该用户的上级

**影响范围**:
- ❌ 无法登录系统
- ✅ 历史数据保留（审批记录、表单提交等）
- ✅ 作为审批人的流程继续（需要转交或跳过）
- ✅ 部门归属保留

**API 示例**:
```http
PATCH /api/v1/users/:userId/status
Content-Type: application/json

{
  "status": "INACTIVE",
  "reason": "长期休假"
}
```

---

#### 2. INACTIVE → ACTIVE（激活）

**触发条件**:
- 管理员手动激活
- 用户重新回岗

**前置检查**:
- ✅ 操作者拥有 `user:update` 权限
- ✅ 用户邮箱仍然有效
- ✅ 至少有一个有效的部门归属

**执行动作**:
1. 更新用户状态为 ACTIVE
2. 更新 `updatedAt` 时间戳
3. 记录审计日志
4. 发送激活通知给用户

**影响范围**:
- ✅ 恢复登录权限
- ✅ 恢复所有功能权限
- ✅ 重新出现在组织架构中

**API 示例**:
```http
PATCH /api/v1/users/:userId/status
Content-Type: application/json

{
  "status": "ACTIVE"
}
```

---

#### 3. ACTIVE → SUSPENDED（暂停）

**触发条件**:
- 违反公司政策
- 安全事件调查期间
- 待审查

**前置检查**:
- ✅ 操作者拥有 `user:suspend` 权限
- ✅ 必须提供暂停原因

**执行动作**:
1. 更新用户状态为 SUSPENDED
2. 记录暂停原因
3. 记录审计日志（包含详细原因）
4. 可选：发送通知给 HR 和用户上级
5. 可选：终止所有活跃会话

**影响范围**:
- ❌ 无法登录系统
- ❌ 待处理的审批可能需要转交
- ✅ 数据完全保留
- ⚠️ 显示"账号已暂停"标记

**API 示例**:
```http
PATCH /api/v1/users/:userId/status
Content-Type: application/json

{
  "status": "SUSPENDED",
  "reason": "安全审查中",
  "suspendedBy": "admin-user-id",
  "terminateActiveSessions": true
}
```

---

#### 4. SUSPENDED → ACTIVE（恢复）

**触发条件**:
- 调查完成，无违规
- 审查通过

**前置检查**:
- ✅ 操作者拥有 `user:suspend` 权限
- ✅ 必须提供恢复说明

**执行动作**:
1. 更新用户状态为 ACTIVE
2. 记录恢复说明
3. 记录审计日志
4. 发送通知给用户

**影响范围**:
- ✅ 恢复所有权限
- ✅ 移除暂停标记

---

#### 5. ACTIVE/INACTIVE/SUSPENDED → TERMINATED（离职）

**触发条件**:
- 员工正式离职
- 合同到期
- 终止雇佣关系

**前置检查**:
- ✅ 操作者拥有 `user:terminate` 权限
- ✅ 必须提供离职日期

**执行动作**:
1. 更新用户状态为 TERMINATED
2. 设置 `terminatedAt` 时间戳
3. 更新所有部门归属的 `leftAt` 字段
4. 记录审计日志（包含离职原因）
5. 可选：自动转交待处理审批
6. 可选：归档用户相关数据
7. 发送离职通知给 HR 和相关主管

**影响范围**:
- ❌ 无法登录系统
- ✅ 历史数据完全保留
- ⚠️ 不再出现在组织架构（活跃人员）中
- ✅ 可在历史查询中找到
- ❌ 不可恢复为其他状态（终态）

**API 示例**:
```http
PATCH /api/v1/users/:userId/status
Content-Type: application/json

{
  "status": "TERMINATED",
  "terminatedAt": "2025-12-31T23:59:59Z",
  "reason": "正常离职",
  "transferApprovals": true
}
```

---

---

### 状态转换权限矩阵

| 从状态 → 到状态 | 需要权限 | 特殊要求 |
|----------------|----------|----------|
| `[*]` → `ACTIVE` | `user:create` | 创建用户 |
| `ACTIVE` → `INACTIVE` | `user:update` | 不能停用自己 |
| `INACTIVE` → `ACTIVE` | `user:update` | - |
| `ACTIVE` → `SUSPENDED` | `user:suspend` | 需要理由 |
| `SUSPENDED` → `ACTIVE` | `user:suspend` | 需要说明 |
| `*` → `TERMINATED` | `user:terminate` | 需要日期和理由 |

---

## ❌ 非法状态流转

### 禁止的流转

| 当前状态 | 不可流转到 | 原因 |
|---------|-----------|------|
| `TERMINATED` | 任何状态 | 终态，离职不可逆 |
| `ACTIVE` | `ACTIVE` | 无意义的流转 |
| `INACTIVE` | `INACTIVE` | 无意义的流转 |
| `SUSPENDED` | `SUSPENDED` | 无意义的流转 |
| `INACTIVE` | `SUSPENDED` | 逻辑冲突，应先激活 |
| `SUSPENDED` | `INACTIVE` | 逻辑冲突，应先恢复 |

### 错误处理

当尝试非法流转时：

**HTTP 状态码**: 400 Bad Request

**错误响应**:
```json
{
  "success": false,
  "error": {
    "code": "INVALID_STATE_TRANSITION",
    "message": "不允许从 TERMINATED 流转到 ACTIVE",
    "details": {
      "currentState": "TERMINATED",
      "targetState": "ACTIVE",
      "reason": "TERMINATED 是终态，离职用户不可恢复",
      "allowedTransitions": []
    }
  }
}
```

**常见错误场景**:

1. **尝试恢复离职用户**:
   ```json
   {
     "code": "INVALID_STATE_TRANSITION",
     "message": "离职用户不可恢复，如需重新入职请创建新账号"
   }
   ```

2. **尝试停用自己**:
   ```json
   {
     "code": "CANNOT_DEACTIVATE_SELF",
     "message": "不能停用自己的账号"
   }
   ```

3. **尝试停用最后一个管理员**:
   ```json
   {
     "code": "LAST_ADMIN_PROTECTION",
     "message": "不能停用最后一个系统管理员"
   }
   ```

---

## 🏢 部门归属状态管理

部门归属不使用显式状态字段，而是通过时间字段管理：

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| `joinedAt` | DateTime | 加入部门时间 |
| `leftAt` | DateTime? | 离开部门时间（null = 当前仍在） |

### 状态判断

```typescript
// 当前在职
function isCurrentMember(ud: UserDepartment): boolean {
  return ud.leftAt === null;
}

// 历史归属
function isFormerMember(ud: UserDepartment): boolean {
  return ud.leftAt !== null;
}

// 特定时间点的在职状态
function wasActiveMemberAt(ud: UserDepartment, date: Date): boolean {
  return ud.joinedAt <= date && (ud.leftAt === null || ud.leftAt > date);
}
```

### 状态流转场景

#### 1. 添加部门归属

**操作**: 创建 UserDepartment 记录

```typescript
{
  userId: "user-uuid",
  departmentId: "dept-uuid",
  positionId: "position-uuid",
  managerId: "manager-uuid",
  isPrimary: false,
  joinedAt: new Date(),    // 当前时间
  leftAt: null              // null 表示在职
}
```

#### 2. 调整部门（转岗）

**方式 1: 保留历史** （推荐）
1. 将原归属的 `leftAt` 设为调整日期
2. 创建新的归属记录（`joinedAt` 为调整日期）

**方式 2: 直接修改**
- 修改现有归属的 `departmentId`
- 不推荐，会丢失历史

#### 3. 离开部门

**操作**: 更新 `leftAt` 字段

```typescript
{
  leftAt: new Date("2025-12-31")
}
```

**场景**:
- 兼职结束
- 项目组解散
- 转岗（配合方式1）

#### 4. 员工离职

**操作**: 更新所有归属的 `leftAt` 字段

**业务规则**:
- 用户状态变更为 TERMINATED 时，系统自动更新该用户所有部门归属的 `leftAt` 字段为 `terminatedAt` 时间
- 确保历史数据完整性，离职用户的所有部门归属都有明确的离开时间

```typescript
// 批量更新
UserDepartment.updateMany({
  where: { userId: "user-uuid", leftAt: null },
  data: { leftAt: user.terminatedAt }
})
```

**影响范围**:
- ✅ 所有部门归属的 `leftAt` 字段自动更新
- ✅ 用户不再出现在任何部门的活跃成员列表中
- ✅ 历史归属记录完整保留，可用于审计和统计

---

## 🎨 前端展示规则

### 用户状态徽章样式

| 状态 | 颜色 | 背景色 | 图标 | 文案 |
|------|------|--------|------|------|
| `ACTIVE` | `#00b42a` | `rgba(0, 180, 42, 0.1)` | 🟢 | 激活 |
| `INACTIVE` | `#ff7d00` | `rgba(255, 125, 0, 0.1)` | 🟡 | 停用 |
| `SUSPENDED` | `#f53f3f` | `rgba(245, 63, 63, 0.1)` | ⚠️ | 暂停 |
| `TERMINATED` | `#86909c` | `rgba(134, 144, 156, 0.1)` | ❌ | 已离职 |

**Tailwind CSS 示例**:
```tsx
const statusStyles = {
  ACTIVE: "text-green-600 bg-green-50",
  INACTIVE: "text-orange-500 bg-orange-50",
  SUSPENDED: "text-red-500 bg-red-50",
  TERMINATED: "text-gray-500 bg-gray-50"
};

<span className={`px-2 py-1 rounded-full text-xs font-medium ${statusStyles[user.status]}`}>
  {statusLabels[user.status]}
</span>
```

### 按钮显示规则

| 状态 | 显示按钮 | 禁用按钮 | 隐藏功能 |
|------|---------|---------|---------|
| `ACTIVE` | 编辑、停用、暂停、离职、分配角色 | - | - |
| `INACTIVE` | 编辑、激活、离职 | 停用、暂停、分配角色 | - |
| `SUSPENDED` | 编辑、恢复、离职 | 停用、暂停、分配角色 | - |
| `TERMINATED` | 查看详情 | 所有操作按钮 | 编辑、角色管理、部门管理 |

**React 组件示例**:
```tsx
function UserActions({ user }: { user: User }) {
  const canEdit = ['ACTIVE', 'INACTIVE', 'SUSPENDED'].includes(user.status);
  const canAssignRole = user.status === 'ACTIVE';
  const canTerminate = user.status !== 'TERMINATED';
  
  return (
    <div className="flex gap-2">
      {canEdit && <Button onClick={handleEdit}>编辑</Button>}
      {canAssignRole && <Button onClick={handleAssignRole}>分配角色</Button>}
      
      {user.status === 'ACTIVE' && (
        <>
          <Button onClick={() => changeStatus('INACTIVE')}>停用</Button>
          <Button onClick={() => changeStatus('SUSPENDED')}>暂停</Button>
        </>
      )}
      
      {user.status === 'INACTIVE' && (
        <Button onClick={() => changeStatus('ACTIVE')}>激活</Button>
      )}
      
      {user.status === 'SUSPENDED' && (
        <Button onClick={() => changeStatus('ACTIVE')}>恢复</Button>
      )}
      
      {canTerminate && (
        <Button variant="danger" onClick={handleTerminate}>离职</Button>
      )}
    </div>
  );
}
```

### 列表页筛选规则

**默认显示**: 仅显示 ACTIVE 状态用户

**筛选选项**:
```tsx
const statusFilters = [
  { label: '全部状态', value: null },
  { label: '激活', value: 'ACTIVE' },
  { label: '停用', value: 'INACTIVE' },
  { label: '暂停', value: 'SUSPENDED' },
  { label: '已离职', value: 'TERMINATED', badge: 'warning' }
];
```

**URL 参数**:
```
/users                           # 默认：仅 ACTIVE
/users?status=ACTIVE,INACTIVE    # 激活和停用
/users?status=TERMINATED         # 仅离职
/users?includeTerminated=true    # 包含离职用户
```

---

## 🔌 API 影响

### 状态相关 API

| API | 方法 | 说明 | 状态变更 | 权限 |
|-----|------|------|---------|------|
| `POST /api/v1/users` | POST | 创建用户 | → `ACTIVE` | `user:create` |
| `PATCH /api/v1/users/:id/status` | PATCH | 更新状态（通用） | 根据 body.status | 根据目标状态 |
| `POST /api/v1/users/:id/activate` | POST | 激活 | `INACTIVE` → `ACTIVE` | `user:update` |
| `POST /api/v1/users/:id/deactivate` | POST | 停用 | `ACTIVE` → `INACTIVE` | `user:update` |
| `POST /api/v1/users/:id/suspend` | POST | 暂停 | `ACTIVE` → `SUSPENDED` | `user:suspend` |
| `POST /api/v1/users/:id/unsuspend` | POST | 恢复 | `SUSPENDED` → `ACTIVE` | `user:suspend` |
| `POST /api/v1/users/:id/terminate` | POST | 离职 | `*` → `TERMINATED` | `user:terminate` |

### 状态更新 API 示例

**通用状态更新**:
```http
PATCH /api/v1/users/:userId/status
Content-Type: application/json

{
  "status": "INACTIVE",
  "reason": "长期休假",
  "effectiveDate": "2025-12-31T00:00:00Z"
}
```

**专用API（推荐）**:
```http
POST /api/v1/users/:userId/terminate
Content-Type: application/json

{
  "terminatedAt": "2025-12-31T23:59:59Z",
  "reason": "正常离职",
  "transferApprovals": true,
  "transferTo": "manager-user-id"
}
```

### 查询过滤

**按状态查询**:
```http
# 单个状态
GET /api/v1/users?status=ACTIVE

# 多个状态
GET /api/v1/users?status=ACTIVE,INACTIVE

# 排除某些状态
GET /api/v1/users?excludeStatus=TERMINATED

# 包含离职用户
GET /api/v1/users?includeTerminated=true
```

**TypeScript 类型定义**:
```typescript
interface ListUsersQuery {
  status?: UserStatus | UserStatus[];
  excludeStatus?: UserStatus | UserStatus[];
  includeTerminated?: boolean;
  page?: number;
  pageSize?: number;
}

interface UpdateUserStatusDto {
  status: UserStatus;
  reason?: string;
  effectiveDate?: string;
}

interface TerminateUserDto {
  terminatedAt: string;
  reason: string;
  transferApprovals?: boolean;
  transferTo?: string;
}
```

---

## 🧪 测试要点

### 状态流转测试

- [ ] **测试所有合法的状态流转（7个路径）**
  - [ ] `[*]` → `ACTIVE` (创建用户)
  - [ ] `ACTIVE` → `INACTIVE` (停用)
  - [ ] `INACTIVE` → `ACTIVE` (激活)
  - [ ] `ACTIVE` → `SUSPENDED` (暂停)
  - [ ] `SUSPENDED` → `ACTIVE` (恢复)
  - [ ] `ACTIVE/INACTIVE/SUSPENDED` → `TERMINATED` (离职)

- [ ] **测试所有非法的状态流转（应该返回 400 错误）**
  - [ ] `TERMINATED` → 任何状态（应该失败）
  - [ ] `INACTIVE` → `SUSPENDED`（应该失败）
  - [ ] `SUSPENDED` → `INACTIVE`（应该失败）
  - [ ] 相同状态流转（如 `ACTIVE` → `ACTIVE`）

- [ ] **测试并发状态变更**
  - [ ] 两个请求同时修改同一用户状态
  - [ ] 验证数据一致性
  - [ ] 验证审计日志完整性

- [ ] **测试权限控制**
  - [ ] 无权限用户尝试修改状态（应该返回 403）
  - [ ] 停用自己（应该失败）
  - [ ] 停用最后一个管理员（应该失败）

### 边界条件测试

- [ ] **最后一个管理员保护**
- [ ] 系统只有一个 Administrator 角色用户时，尝试停用/暂停/离职（应该失败）
  - [ ] 有两个管理员时，可以停用其中一个

- [ ] **审批流程转交**
  - [ ] 离职用户是审批人时，待审批任务是否正确转交
  - [ ] 验证转交逻辑（转给上级/部门主管/指定人员）
  - [ ] 验证转交日志完整性

- [ ] **部门归属自动更新**
  - [ ] 离职时所有部门归属的 `leftAt` 字段是否更新
  - [ ] 验证 `leftAt` 时间与 `terminatedAt` 一致

- [ ] **Token 失效**
  - [ ] 停用/暂停/离职后，用户的 JWT Token 是否立即失效
  - [ ] 验证 Token 黑名单机制

### 性能测试

- [ ] **批量状态更新**
  - [ ] 批量离职 100 个用户的性能
  - [ ] 验证事务完整性

- [ ] **状态查询性能**
  - [ ] 10万用户时按状态查询的性能
  - [ ] 验证索引有效性

---

## 📊 数据库设计

### 状态字段定义

```sql
-- 用户状态枚举
CREATE TYPE platform_iam.user_status AS ENUM (
  'ACTIVE',
  'INACTIVE',
  'SUSPENDED',
  'TERMINATED'
);

-- users 表的状态字段
CREATE TABLE platform_iam.users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  username VARCHAR(255) NOT NULL UNIQUE,
  status platform_iam.user_status NOT NULL DEFAULT 'ACTIVE',
  terminated_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  deleted_at TIMESTAMPTZ,
  
  -- 状态约束
  CONSTRAINT check_terminated_at 
    CHECK ((status = 'TERMINATED' AND terminated_at IS NOT NULL) OR 
           (status != 'TERMINATED' AND terminated_at IS NULL))
);

-- 状态索引
CREATE INDEX idx_users_status ON platform_iam.users(status) 
  WHERE deleted_at IS NULL;

-- 活跃用户索引
CREATE INDEX idx_users_active ON platform_iam.users(id) 
  WHERE status = 'ACTIVE' AND deleted_at IS NULL;
```

### 状态变更历史（通过审计系统记录）

本模块使用全局审计系统记录所有状态变更，不需要单独的历史表。

**审计日志查询**:
```typescript
// 查询用户状态变更历史
GET /api/v1/audit/entities/user/:userId/history?field=status

// 审计日志响应示例
{
  "logs": [
    {
      "id": "audit-log-uuid",
      "action": "UPDATE",
      "entityType": "User",
      "entityId": "user-uuid",
      "field": "status",
      "oldValue": "ACTIVE",
      "newValue": "TERMINATED",
      "operatorId": "admin-uuid",
      "reason": "正常离职",
      "createdAt": "2025-12-31T23:59:59Z"
    }
  ]
}
```

**状态统计查询**:
```sql
-- 用户状态分布
SELECT 
  status,
  COUNT(*) as count,
  ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) as percentage
FROM platform_iam.users
WHERE deleted_at IS NULL
GROUP BY status;

-- 近30天离职人数
SELECT COUNT(*) as terminated_count
FROM platform_iam.users
WHERE status = 'TERMINATED'
  AND terminated_at >= NOW() - INTERVAL '30 days';
```

---

## 🎭 角色状态管理

角色使用 `enabled` Boolean 字段管理状态：

### 状态定义

| 状态 | 字段值 | 说明 |
|------|--------|------|
| 启用 | `enabled: true` | 可正常使用，可分配给用户 |
| 禁用 | `enabled: false` | 不可分配，已分配的用户仍保留 |

### 状态转换

```mermaid
stateDiagram-v2
    [*] --> Enabled : 创建角色
    
    Enabled --> Disabled : 禁用
    Disabled --> Enabled : 启用
    
    Disabled --> [*] : 删除（谨慎）
    
    note right of Enabled
        - 可见可选
        - 可分配给用户
        - 权限正常生效
    end note
    
    note right of Disabled
        - 列表中隐藏
        - 不可新分配
        - 已分配用户保留
    end note
```

### 禁用角色的影响

**禁用时**:
- ❌ 不再出现在角色选择列表
- ❌ 不能分配给新用户
- ✅ 已拥有该角色的用户保留（权限仍生效）
- ⚠️ 建议提前通知受影响的用户

**使用场景**:
- 废弃的旧角色
- 临时关闭某些权限
- 角色重构过渡期

**API 示例**:
```http
PATCH /api/v1/roles/:roleId
Content-Type: application/json

{
  "enabled": false,
  "reason": "角色已废弃，请使用新角色"
}
```

---

## 🔄 状态查询优化

### 1. 获取活跃用户

```prisma
// Prisma 查询
User.findMany({
  where: {
    status: 'ACTIVE',
    deletedAt: null
  }
})
```

```sql
-- SQL 查询
SELECT * FROM platform_iam.users
WHERE status = 'ACTIVE'
  AND deleted_at IS NULL;
```

### 2. 获取用户当前部门

```prisma
UserDepartment.findMany({
  where: {
    userId: "user-uuid",
    leftAt: null
  },
  include: {
    department: true,
    position: true,
    manager: {
      select: { id: true, displayName: true }
    }
  }
})
```

### 3. 获取特定时间点的部门成员

```prisma
UserDepartment.findMany({
  where: {
    departmentId: "dept-uuid",
    joinedAt: { lte: targetDate },
    OR: [
      { leftAt: null },
      { leftAt: { gt: targetDate } }
    ]
  }
})
```

---

## ⚠️ 状态转换注意事项

### 1. 最后一个管理员保护

```typescript
async function canDeactivateUser(userId: string): Promise<boolean> {
  const user = await User.findUnique({
    where: { id: userId },
    include: { roles: { include: { role: true } } }
  });
  
  const isAdmin = user.roles.some(ur => ur.role.code === 'Administrator');
  
  if (!isAdmin) return true;
  
  // 检查是否还有其他活跃的管理员
  const activeAdminCount = await User.count({
    where: {
      status: 'ACTIVE',
      id: { not: userId },
      roles: {
        some: {
          role: { code: 'Administrator' }
        }
      }
    }
  });
  
  return activeAdminCount > 0;
}
```

### 2. 审批流程处理

用户状态变更时，需要处理待审批事项：

**离职时**:
- 查询作为审批人的待办任务
- 选项1: 自动转交给其直属上级
- 选项2: 自动转交给部门主管
- 选项3: 手动指定接替人
- 记录转交日志

**暂停时**:
- 待审批任务暂时保留
- 系统标记该审批人不可用
- 可选：自动提醒管理员处理

### 3. 批量操作事务性

```typescript
async function bulkTerminateUsers(
  userIds: string[],
  terminatedAt: Date,
  reason: string
) {
  await prisma.$transaction(async (tx) => {
    // 1. 更新用户状态
    await tx.user.updateMany({
      where: { id: { in: userIds } },
      data: {
        status: 'TERMINATED',
        terminatedAt
      }
    });
    
    // 2. 更新部门归属
    await tx.userDepartment.updateMany({
      where: {
        userId: { in: userIds },
        leftAt: null
      },
      data: { leftAt: terminatedAt }
    });
    
    // 3. 记录审计日志
    await tx.auditLog.createMany({
      data: userIds.map(userId => ({
        action: 'TERMINATE',
        userId,
        entityType: 'User',
        entityId: userId,
        oldValue: { status: 'ACTIVE' },
        newValue: { status: 'TERMINATED', terminatedAt, reason }
      }))
    });
  });
}
```

---

## 📊 状态统计查询

### Dashboard 统计

```sql
-- 用户状态分布
SELECT 
  status,
  COUNT(*) as count,
  ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 2) as percentage
FROM platform_iam.users
WHERE deleted_at IS NULL
GROUP BY status;

-- 近30天离职人数
SELECT COUNT(*) as terminated_count
FROM platform_iam.users
WHERE status = 'TERMINATED'
  AND terminated_at >= NOW() - INTERVAL '30 days';

-- 部门在职人数
SELECT 
  d.name as department_name,
  COUNT(DISTINCT ud.user_id) as active_members
FROM corp_hr.departments d
LEFT JOIN corp_hr.user_departments ud ON d.id = ud.department_id
INNER JOIN platform_iam.users u ON ud.user_id = u.id
WHERE ud.left_at IS NULL
  AND u.status = 'ACTIVE'
  AND u.deleted_at IS NULL
GROUP BY d.id, d.name
ORDER BY active_members DESC;
```

---

## 📊 组织状态管理（v2.0）⭐ NEW

### 状态设计讨论

**当前状态**: 架构文档未明确组织是否有状态字段

**建议方案 1**: 不设置状态字段（推荐）
- **理由**: 组织是法人实体，一旦创建通常不会"停用"，只有注销（软删除）
- **优点**: 简化设计，避免状态管理复杂度
- **缺点**: 无法临时"禁用"组织（如暂停运营）

**建议方案 2**: 添加状态字段
```typescript
enum OrganizationStatus {
  ACTIVE,    // 正常运营
  INACTIVE,  // 暂停运营
  DISSOLVED  // 已注销（等同软删除）
}
```

**推荐**: **方案 1**（不设置状态），理由：
1. 组织状态变更极少（场景7: 区域配置为低频操作）
2. 软删除（deletedAt）已足够
3. 如需临时禁用，可通过业务逻辑控制

**如采用方案 2，流转规则**:
```
[*] --> ACTIVE (创建组织)
ACTIVE --> INACTIVE (暂停运营)
INACTIVE --> ACTIVE (恢复运营)
ACTIVE/INACTIVE --> DISSOLVED (注销)
```

---

## 🏢 部门状态管理 ⭐ NEW

### 状态设计讨论

**当前状态**: 架构文档未明确部门是否有状态字段

**建议方案 1**: 不设置状态字段（推荐）
- **理由**: 部门调整通过软删除（deletedAt）管理即可
- **优点**: 简化设计
- **场景**: 场景4（组织架构调整）中，部门的创建/删除已足够

**建议方案 2**: 添加状态字段
```typescript
enum DepartmentStatus {
  ACTIVE,    // 正常运作
  INACTIVE,  // 暂时停用（如筹建中、重组中）
  DISSOLVED  // 已撤销
}
```

**推荐**: **方案 1**（不设置状态）

**如采用方案 2，影响范围**:
- INACTIVE 部门：
  - ❌ 不能添加新成员
  - ✅ 现有成员保留
  - ❌ 不出现在部门选择列表
  - ⚠️ 审批流程中的部门关系解析可能失败

---

## 📋 岗位状态管理 ⭐ NEW

### 状态字段: `enabled` (Boolean)

| 状态 | 字段值 | 说明 | 影响范围 |
|------|--------|------|---------|
| **启用** | `enabled: true` | 正常使用 | 可分配给用户 |
| **禁用** | `enabled: false` | 不可新分配 | 已分配用户保留 |

### 状态转换

```mermaid
stateDiagram-v2
    [*] --> Enabled : 创建岗位
    
    Enabled --> Disabled : 禁用
    Disabled --> Enabled : 启用
    
    Disabled --> [*] : 删除（有删除保护）
    
    note right of Enabled
        - 可见可选
        - 可分配给用户
        - 出现在岗位列表
    end note
    
    note right of Disabled
        - 列表中隐藏（或标记）
        - 不可新分配
        - 已分配用户保留
        - 为删除做准备
    end note
```

### 禁用岗位的影响

**禁用时**:
- ❌ 不再出现在岗位选择列表（前端过滤）
- ❌ 不能分配给新用户（API 校验）
- ✅ 已拥有该岗位的用户保留
- ⚠️ 建议提前通知受影响的用户

**使用场景**:
- 废弃的旧岗位
- 职级体系调整过渡期
- 为删除做准备（先禁用，确认无影响后删除）

**删除保护**:
```typescript
// 即使 enabled = false，如果有用户使用，仍然不能删除
const usageCount = await prisma.userDepartment.count({
  where: { positionId, leftAt: null }
});

if (usageCount > 0) {
  throw new BadRequestException(
    `无法删除岗位，该岗位被 ${usageCount} 名员工使用`
  );
}
```

---

## 👤 身份源状态管理 ⭐ NEW

### 身份源枚举: `source` (UserSource)

| 身份源 | 代码 | 创建方式 | 认证方式 | 密码管理 | 状态影响 |
|--------|------|---------|---------|---------|---------|
| **本地用户** | `LOCAL` | 手动创建 | 本地密码验证 | ✅ 可修改 | 标准状态流转 |
| **LDAP用户** | `LDAP` | 手动创建 / Entra 同步 | LDAP/AD认证 | ❌ 不可修改 | 标准状态流转 |

### 身份源对状态的影响

#### 1. 用户创建（[*] → ACTIVE）

```typescript
// LOCAL 用户
{
  source: 'LOCAL',
  passwordHash: await bcrypt.hash(password, 10),
  externalId: null,
  status: 'ACTIVE'
}

// LDAP 用户
{
  source: 'LDAP',
  passwordHash: null,  // 无密码
  externalId: ldapDN,  // LDAP DN
  status: 'ACTIVE'
}

// LDAP 用户（Entra 同步创建）
{
  source: 'LDAP',
  passwordHash: null,
  externalId: entraObjectId,
  externalSource: 'ENTRA_ID',
  status: 'ACTIVE' or 'SUSPENDED', // 根据 Entra 状态
  ldapSyncedAt: new Date()
}
```

#### 2. 用户状态同步（Entra 同步）

**Entra ID 同步状态映射**:

| Entra ID 状态 | 本地用户状态 | 规则 |
|--------------|-------------|------|
| accountEnabled: true | ACTIVE | 自动更新 |
| accountEnabled: false | SUSPENDED | 自动更新 |
| 用户被删除 | 跳过 | 不自动删除本地用户 |
| - | TERMINATED | **不自动复活**（保护） |

**同步规则**:
```typescript
async syncUserFromEntra(entraUser: EntraUser) {
  const localUser = await findUserByExternalId(entraUser.id);
  
  if (!localUser) {
    // 新用户：创建
    return createUser({
      source: 'LDAP',
      externalSource: 'ENTRA_ID',
      status: entraUser.accountEnabled ? 'ACTIVE' : 'SUSPENDED',
      ...
    });
  }
  
  // 已存在用户：更新
  
  // 保护规则：TERMINATED 用户不自动复活
  if (localUser.status === 'TERMINATED') {
    logger.warn(`Skipped Entra user sync: local user is TERMINATED`, {
      userId: localUser.id,
      entraId: entraUser.id
    });
    return; // 跳过
  }
  
  // 更新状态
  const newStatus = entraUser.accountEnabled ? 'ACTIVE' : 'SUSPENDED';
  
  if (localUser.status !== newStatus) {
    await updateUser(localUser.id, {
      status: newStatus,
      ldapSyncedAt: new Date()
    });
    
    logger.log(`User status synced from Entra`, {
      userId: localUser.id,
      oldStatus: localUser.status,
      newStatus
    });
  }
}
```

#### 3. 修改密码限制

```typescript
async changePassword(userId: string, oldPassword: string, newPassword: string) {
  const user = await findUser(userId);
  
  // 检查身份源
  if (user.source !== 'LOCAL') {
    const errorMessages = {
      'LDAP': 'LDAP 用户不能通过本系统修改密码，请联系 IT 管理员',
      'LDAP': 'LDAP 用户不能通过本系统修改密码，请联系 IT 管理员',
      'ENTRA_ID': 'Entra 同步用户不能通过本系统修改密码，请通过 Microsoft 365 门户修改'
    };
    
    const errorMessage = user.externalSource === 'ENTRA_ID'
      ? errorMessages.ENTRA_ID
      : errorMessages[user.source];
    throw new BadRequestException(errorMessage || '当前用户类型不支持修改密码');
  }
  
  // LOCAL 用户：正常修改密码流程
  // ...
}
```

#### 4. 前端显示差异

```typescript
// 根据身份源显示不同的 UI
function UserProfilePage({ user }) {
  return (
    <div>
      <h2>个人设置</h2>
      
      {/* 身份源标识 */}
      <Badge>
        {user.source === 'LOCAL' && '本地账号'}
        {user.source === 'LDAP' && 'LDAP账号'}
        {user.externalSource === 'ENTRA_ID' && 'Entra 同步账号'}
      </Badge>
      
      {/* 修改密码：仅 LOCAL 用户显示 */}
      {user.source === 'LOCAL' ? (
        <Button onClick={openChangePassword}>修改密码</Button>
      ) : (
        <Alert type="info">
          {user.source === 'LDAP' && '请联系 IT 管理员修改 LDAP 密码'}
          {user.externalSource === 'ENTRA_ID' && (
            <>
              请通过 <a href="https://portal.microsoft.com">Microsoft 365</a> 修改密码
            </>
          )}
        </Alert>
      )}
    </div>
  );
}
```

### 身份源状态总结

**状态流转规则**:
- LOCAL/LDAP: 遵循标准用户状态流转（ACTIVE ↔ INACTIVE ↔ SUSPENDED → TERMINATED）
- LDAP（Entra 同步）: 标准流转 + **同步更新**（Entra 状态 → 本地状态）

**特殊保护**:
- TERMINATED 用户不会因 Entra 同步而自动复活
- LDAP（含 Entra 同步）用户不能修改密码（BadRequestException）

---

## 🔗 相关文档

- [产品需求文档](./01-prd.md) - 业务需求和功能规格
- [用户旅程](./02-user-journey.md) - 用户操作场景流程
- [架构设计](./03-architecture.md) - 技术实现细节
- [数据模型](./06-data-model.md) - 数据库表设计
- [API 文档](./07-api.md) - 状态更新接口定义
- [测试场景](./09-test-scenarios.md) - 状态转换测试用例

---

**最后更新**: 2025-12-26  
**维护者**: FFOA 开发团队  
**版本**: v2.1.1  
**状态**: ✅ 已完成（完整状态管理规范）
