# 平台账号管理接口文档

**版本**: V1.0  
**日期**: 2026-01-XX  
**状态**: 已发布

本文档详细描述了统一认证平台（UAP）的平台账号管理相关接口，包括查询用户账号映射列表和 SSO 单点登录功能。

---

## 1. 概述

平台账号管理接口允许用户查询和管理自己在各个第三方应用中的账号映射关系，并支持通过 SSO 方式快速登录到目标应用。

### 1.1 核心概念

- **账号映射 (User Mapping)**: 统一认证平台用户与第三方应用账号的关联关系
- **映射账号 (Mapped Key)**: 用户在第三方系统中的唯一标识
- **映射邮箱 (Mapped Email)**: 用户在第三方系统中的邮箱地址
- **SSO 登录**: 单点登录，用户无需再次输入密码即可进入第三方应用

### 1.2 接口权限说明

| 接口 | 认证方式 | 说明 |
|------|---------|------|
| `GET /simple/me/launchpad-apps` | Bearer Token (用户认证) | 当前用户可访问应用（映射已启用、协议为 `SIMPLE_API` / `OIDC` / `NONE`、应用未删除），用于快捷导航 |
| `GET /simple/me/mappings` | Bearer Token (用户认证) | 仅用户可查询自己的账号映射 |
| `POST /simple/sso-login` | Bearer Token (可选) 或 用户名密码 | 支持会话认证和凭据认证；支持 `SIMPLE_API` / `OIDC` / `NONE` |

---

## 2. 认证方式

### 2.1 用户认证 (Bearer Token)

适用于前端应用或已登录的用户。

**请求头格式:**
```
Authorization: Bearer {access_token}
```

**获取 Token:**
- 通过 `POST /auth/login/json` 接口登录获取
- 或通过 `POST /simple/login` 接口（不提供 app_id）获取平台 access_token

### 2.2 凭据认证 (用户名密码)

适用于用户未登录 UAP 平台的场景，直接使用用户名和密码进行认证。

---

## 3. 接口定义

### 3.0 查询当前可访问的应用列表（快捷导航）

与「映射列表」不同：本接口**无分页**，仅返回**映射已启用**、**应用未删除**、且协议为 **`SIMPLE_API` / `OIDC` / `NONE`（导航）** 的应用，用于应用中心/快捷导航展示。

- **接口地址**: `GET {{API_BASE_URL}}/simple/me/launchpad-apps`
- **认证方式**: Bearer Token
- **响应字段**: 见帮助中心《客户端接口 API 指南》第 8 节（含 `protocol_type`、`icon_url` 等）

### 3.1 获取我的账号映射列表

查询当前用户在所有第三方应用中的账号映射关系。

- **接口地址**: `GET {{API_BASE_URL}}/simple/me/mappings`
- **认证方式**: Bearer Token (用户认证)
- **Content-Type**: `application/json`

#### 3.1.1 请求参数 (Query Parameters)

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `skip` | integer | 否 | 0 | 跳过的记录数（用于分页） |
| `limit` | integer | 否 | 10 | 每页返回的记录数 |
| `app_name` | string | 否 | - | 应用名称（支持模糊搜索） |

#### 3.1.2 响应格式

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

```json
{
  "total": 25,
  "items": [
    {
      "app_name": "OA办公系统",
      "app_id": "oa_system_001",
      "protocol_type": "SIMPLE_API",
      "mapped_key": "zhangsan_oa",
      "mapped_email": "zhangsan@company.com",
      "is_active": true
    },
    {
      "app_name": "CRM客户管理",
      "app_id": "crm_system_001",
      "protocol_type": "SIMPLE_API",
      "mapped_key": "zhangsan_crm",
      "mapped_email": null,
      "is_active": true
    }
  ]
}
```

**响应字段说明:**

| 字段 | 类型 | 说明 |
|------|------|------|
| `total` | integer | 总记录数 |
| `items` | array | 映射列表 |
| `items[].app_name` | string | 应用名称 |
| `items[].app_id` | string | 应用ID |
| `items[].protocol_type` | string | 协议类型：`SIMPLE_API`、`OIDC` 或 `NONE`（仅导航） |
| `items[].mapped_key` | string | 映射账号（第三方系统账号） |
| `items[].mapped_email` | string\|null | 映射邮箱（可能为空） |
| `items[].is_active` | boolean | 是否激活（true: 正常, false: 已禁用） |

#### 3.1.3 错误响应

**401 Unauthorized - 未认证:**
```json
{
  "detail": "Not authenticated"
}
```

**403 Forbidden - 无权限:**
```json
{
  "detail": "Not enough permissions"
}
```

#### 3.1.4 请求示例

**cURL:**
```bash
curl -X GET "{{API_BASE_URL}}/simple/me/mappings?skip=0&limit=10&app_name=OA" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```

**JavaScript (Fetch API):**
```javascript
const response = await fetch('{{API_BASE_URL}}/simple/me/mappings?skip=0&limit=10&app_name=OA', {
  method: 'GET',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  }
});

const data = await response.json();
console.log('总记录数:', data.total);
console.log('映射列表:', data.items);
```

**Python (requests):**
```python
import requests

headers = {
    'Authorization': f'Bearer {access_token}',
    'Content-Type': 'application/json'
}

params = {
    'skip': 0,
    'limit': 10,
    'app_name': 'OA'  # 可选，模糊搜索
}

response = requests.get(
    '{{API_BASE_URL}}/simple/me/mappings',
    headers=headers,
    params=params
)

data = response.json()
print(f"总记录数: {data['total']}")
for mapping in data['items']:
    print(f"应用: {mapping['app_name']}, 映射账号: {mapping['mapped_key']}")
```

---

### 3.2 SSO 单点登录

通过 SSO 方式登录到目标应用，支持两种认证模式：
1. **会话认证模式**: 用户已登录 UAP 平台，使用 Bearer Token
2. **凭据认证模式**: 用户未登录，使用用户名和密码

- **接口地址**: `POST {{API_BASE_URL}}/simple/sso-login`
- **认证方式**: 
  - Bearer Token (会话认证，推荐)
  - 或 用户名密码 (凭据认证)
- **Content-Type**: `application/json`
- **限制**: 支持 `SIMPLE_API`（`redirect_url` 含一次性 `ticket`）、`OIDC`（`redirect_url` 为 `scheme://host`）、`NONE`（`redirect_url` 为导航 URL，无 `ticket`）。其它协议值将返回错误。

#### 3.2.1 请求参数 (Request Body)

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `app_id` | string | 是 | 目标应用的ID |
| `username` | string | 条件必填 | 用户名（手机号、映射key或映射email）。如果已登录UAP平台，可不提供 |
| `password` | string | 条件必填 | 用户密码。如果已登录UAP平台，可不提供 |

**注意:**
- 如果用户已登录 UAP 平台（请求头包含有效的 Bearer Token），则 `username` 和 `password` 可以省略
- 如果用户未登录，则必须提供 `username` 和 `password`

#### 3.2.2 响应格式

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

`redirect_url` 随协议变化：`SIMPLE_API` 通常为带 `ticket` 的回调 URL；`OIDC` 为 `scheme://host`；`NONE` 为配置的导航地址原文。

```json
{
  "redirect_url": "https://oa.company.com/login?ticket=TICKET-7f8e9d0a-1234-5678-9abc-def012345678"
}
```

**响应字段说明:**

| 字段 | 类型 | 说明 |
|------|------|------|
| `redirect_url` | string | 前端应直接跳转的完整 URL（是否含 `ticket` 取决于协议，见上文） |

#### 3.2.3 错误响应

**400 Bad Request - 应用未找到或协议不支持:**
```json
{
  "detail": "应用未找到"
}
```
或
```json
{
  "detail": "协议类型不支持该 SSO 登录方式"
}
```

**401 Unauthorized - 认证失败:**
```json
{
  "detail": "认证失败"
}
```

**400 Bad Request - 用户已禁用:**
```json
{
  "detail": "用户已禁用"
}
```

**400 Bad Request - 应用未配置重定向URI:**
```json
{
  "detail": "应用未配置重定向 URI"
}
```

#### 3.2.4 请求示例

**场景1: 用户已登录 UAP 平台（会话认证）**

**cURL:**
```bash
curl -X POST "{{API_BASE_URL}}/simple/sso-login" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
  -H "Content-Type: application/json" \
  -d '{
    "app_id": "oa_system_001"
  }'
```

**JavaScript (Fetch API):**
```javascript
// 用户已登录，使用 Bearer Token
const response = await fetch('{{API_BASE_URL}}/simple/sso-login', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${accessToken}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    app_id: 'oa_system_001'
  })
});

const data = await response.json();
if (data.redirect_url) {
  // 在新标签页中打开
  window.open(data.redirect_url, '_blank');
}
```

**场景2: 用户未登录（凭据认证）**

**cURL:**
```bash
curl -X POST "{{API_BASE_URL}}/simple/sso-login" \
  -H "Content-Type: application/json" \
  -d '{
    "app_id": "oa_system_001",
    "username": "13800138000",
    "password": "user_password_123"
  }'
```

**JavaScript (Fetch API):**
```javascript
// 用户未登录，使用用户名密码
const response = await fetch('{{API_BASE_URL}}/simple/sso-login', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    app_id: 'oa_system_001',
    username: '13800138000',
    password: 'user_password_123'
  })
});

const data = await response.json();
if (data.redirect_url) {
  window.open(data.redirect_url, '_blank');
}
```

**Python (requests):**
```python
import requests

# 场景1: 会话认证（已登录）
headers = {
    'Authorization': f'Bearer {access_token}',
    'Content-Type': 'application/json'
}

data = {
    'app_id': 'oa_system_001'
}

response = requests.post(
    '{{API_BASE_URL}}/simple/sso-login',
    headers=headers,
    json=data
)

result = response.json()
print(f"跳转URL: {result['redirect_url']}")

# 场景2: 凭据认证（未登录）
data = {
    'app_id': 'oa_system_001',
    'username': '13800138000',
    'password': 'user_password_123'
}

response = requests.post(
    '{{API_BASE_URL}}/simple/sso-login',
    headers={'Content-Type': 'application/json'},
    json=data
)

result = response.json()
print(f"跳转URL: {result['redirect_url']}")
```

---

## 4. 完整集成示例

### 4.1 前端 Vue.js 示例

```vue
<template>
  <div class="mappings-container">
    <el-table :data="mappings" v-loading="loading">
      <el-table-column prop="app_name" label="应用名称" />
      <el-table-column prop="mapped_key" label="映射账号" />
      <el-table-column prop="is_active" label="状态">
        <template #default="scope">
          <el-tag :type="scope.row.is_active ? 'success' : 'danger'">
            {{ scope.row.is_active ? '正常' : '已禁用' }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作">
        <template #default="scope">
          <el-button 
            type="primary" 
            size="small"
            :disabled="!scope.row.is_active"
            :loading="loginLoading[scope.row.app_id]"
            @click="handleSsoLogin(scope.row)"
          >
            进入
          </el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import api from '../utils/request'

interface Mapping {
  app_name: string
  app_id: string
  protocol_type: string
  mapped_key: string
  mapped_email: string | null
  is_active: boolean
}

const mappings = ref<Mapping[]>([])
const loading = ref(false)
const loginLoading = reactive<Record<string, boolean>>({})

// 获取账号映射列表
const fetchMappings = async () => {
  loading.value = true
  try {
    const res = await api.get('/simple/me/mappings', {
      params: { skip: 0, limit: 100 }
    })
    if (res.data) {
      mappings.value = res.data.items
    }
  } catch (error: any) {
    ElMessage.error(error.response?.data?.detail || '获取列表失败')
  } finally {
    loading.value = false
  }
}

// SSO 登录
const handleSsoLogin = async (mapping: Mapping) => {
  if (!mapping.is_active) {
    ElMessage.warning('该账号已禁用，无法登录')
    return
  }
  
  const allowed = ['SIMPLE_API', 'OIDC', 'NONE']
  if (!allowed.includes(mapping.protocol_type)) {
    ElMessage.warning('该应用协议不支持通过 sso-login 跳转')
    return
  }

  loginLoading[mapping.app_id] = true
  
  try {
    // 使用会话认证（用户已登录 UAP 平台）
    const res = await api.post('/simple/sso-login', {
      app_id: mapping.app_id
    }, {
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('access_token')}`
      }
    })
    
    if (res.data && res.data.redirect_url) {
      // 在新标签页中打开目标应用
      window.open(res.data.redirect_url, '_blank')
      ElMessage.success('正在新标签页中打开应用...')
    } else {
      ElMessage.error('SSO 登录失败：未返回跳转地址')
    }
  } catch (error: any) {
    console.error('SSO 登录失败:', error)
    ElMessage.error(error.response?.data?.detail || 'SSO 登录失败')
  } finally {
    loginLoading[mapping.app_id] = false
  }
}

onMounted(() => {
  fetchMappings()
})
</script>
```

### 4.2 后端 Python 示例

```python
import requests
from typing import List, Dict, Optional

class UAPAccountManager:
    """统一认证平台账号管理客户端"""
    
    def __init__(self, base_url: str, access_token: str):
        self.base_url = base_url.rstrip('/')
        self.access_token = access_token
        self.headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }
    
    def get_my_mappings(
        self, 
        skip: int = 0, 
        limit: int = 10, 
        app_name: Optional[str] = None
    ) -> Dict:
        """
        获取我的账号映射列表
        
        Args:
            skip: 跳过的记录数
            limit: 每页返回的记录数
            app_name: 应用名称（模糊搜索）
        
        Returns:
            {
                'total': 总记录数,
                'items': [映射列表]
            }
        """
        params = {'skip': skip, 'limit': limit}
        if app_name:
            params['app_name'] = app_name
        
        response = requests.get(
            f'{self.base_url}/simple/me/mappings',
            headers=self.headers,
            params=params
        )
        response.raise_for_status()
        return response.json()
    
    def sso_login(
        self, 
        app_id: str, 
        username: Optional[str] = None,
        password: Optional[str] = None
    ) -> str:
        """
        SSO 单点登录
        
        Args:
            app_id: 目标应用ID
            username: 用户名（可选，如果已登录UAP可不提供）
            password: 密码（可选，如果已登录UAP可不提供）
        
        Returns:
            跳转URL
        """
        data = {'app_id': app_id}
        if username and password:
            data['username'] = username
            data['password'] = password
        
        # 如果提供了用户名密码，不使用 Bearer Token
        headers = self.headers if not username else {'Content-Type': 'application/json'}
        
        response = requests.post(
            f'{self.base_url}/simple/sso-login',
            headers=headers,
            json=data
        )
        response.raise_for_status()
        result = response.json()
        return result['redirect_url']


# 使用示例
if __name__ == '__main__':
    # 初始化客户端
    manager = UAPAccountManager(
        base_url='{{API_BASE_URL}}',
        access_token='your_access_token_here'
    )
    
    # 获取账号映射列表
    result = manager.get_my_mappings(skip=0, limit=10, app_name='OA')
    print(f"总记录数: {result['total']}")
    for mapping in result['items']:
        print(f"应用: {mapping['app_name']}, 映射账号: {mapping['mapped_key']}")
    
    # SSO 登录（会话认证）
    redirect_url = manager.sso_login(app_id='oa_system_001')
    print(f"跳转URL: {redirect_url}")
    
    # SSO 登录（凭据认证）
    redirect_url = manager.sso_login(
        app_id='oa_system_001',
        username='13800138000',
        password='user_password_123'
    )
    print(f"跳转URL: {redirect_url}")
```

---

## 5. 注意事项

### 5.1 协议类型与接口

- `GET /simple/me/launchpad-apps`：返回 `SIMPLE_API`、`OIDC`、`NONE` 且映射已启用、应用未删除的应用
- `GET /simple/me/mappings`：返回所有协议类型的映射（含已禁用等，以实际数据为准）
- `POST /simple/sso-login`：支持 `SIMPLE_API`、`OIDC`、`NONE`；`redirect_url` 含义见 §3.2.2
- `POST /simple/login`：**提供 `app_id` 时**仅向 `SIMPLE_API` / `OIDC` 返回 `ticket`；`NONE` 需使用 `sso-login`
- 完整 OIDC 授权码与 Token 流程请走 Hydra 标准端点

### 5.2 账号状态

- 只有 `is_active: true` 的账号映射才能进行 SSO 登录
- 如果账号被禁用，SSO 登录会返回错误

### 5.3 安全建议

1. **Token 安全**: Bearer Token 应妥善保管，避免泄露
2. **HTTPS**: 生产环境必须使用 HTTPS 协议
3. **Token 过期**: 注意处理 Token 过期的情况，及时刷新或重新登录
4. **错误处理**: 建议实现完善的错误处理和重试机制

### 5.4 分页建议

- 默认每页返回 10 条记录
- 建议根据实际需求设置合适的 `limit` 值（最大建议 100）
- 使用 `skip` 和 `limit` 实现分页导航

---

## 6. 常见问题 (FAQ)

### Q1: 为什么 SSO 登录返回 400 错误，提示「协议不支持」？

**A:** `POST /simple/sso-login` 仅支持 `SIMPLE_API`、`OIDC` 与 `NONE`。若应用协议为其它值将返回该错误。需要完整 OIDC 授权码流程时请使用 Hydra。

### Q2: 如何判断用户是否已登录 UAP 平台？

**A:** 如果您的应用中有有效的 `access_token`，可以在请求头中携带 `Authorization: Bearer {token}`。如果 Token 有效，则无需提供 `username` 和 `password`。

### Q3: 账号映射列表中的 `mapped_email` 为什么可能为空？

**A:** 不是所有第三方应用都要求或提供邮箱信息，因此 `mapped_email` 字段可能为 `null`。这是正常情况。

### Q4: 如何实现"记住我"功能？

**A:** 建议在客户端（浏览器）安全地存储 `access_token`（如使用 httpOnly Cookie 或安全的 localStorage），并在每次请求时携带该 Token。

### Q5: SSO 登录后，Ticket 的有效期是多久？

**A:** Ticket 的有效期由平台配置决定，通常为 5-10 分钟。建议在获取 `redirect_url` 后立即跳转，避免 Ticket 过期。

---

## 7. 更新日志

| 版本 | 日期 | 更新内容 |
|------|------|---------|
| V1.0 | 2026-01-XX | 初始版本发布 |

---

## 8. 技术支持

如有问题或建议，请联系技术支持团队或查看项目文档。

**文档地址**: `{{DOCS_BASE_URL}}/docs/account_management_api.md`
