# 千问千答应用免登录接入说明

## 1. 适用场景

学员已经在其他可信应用完成登录，从该应用进入千问千答时，不再重复输入手机号和验证码。

接入应用只能在自己的服务端申请一次性授权码。应用密钥不得写入网页、App 安装包、小程序或公开仓库。

## 2. 准备工作

管理员在千问千答后台的“应用接入”页面完成：

1. 填写千问千答用户端公网地址；
2. 新增接入应用；
3. 保存系统只展示一次的应用密钥；
4. 如需学员从千问千答返回来源应用，配置完整的回跳地址白名单。
5. 选择是否允许自动注册新学员；开启时必须同时选择默认权益版本。

首次免登录时，接入应用必须提供学员在本应用内的稳定用户 ID 和已经由本应用验证过的手机号。手机号仅用于首次匹配千问千答学员，后续以账号绑定为准。

## 3. 申请一次性授权码

接口：

```text
POST /api/integration/sso/ticket
```

请求体示例：

```json
{"externalUserId":"student-10086","verifiedPhone":"13800138000","displayName":"张同学","returnUrl":"https://student.example.com/home"}
```

请求头：

```text
X-App-Id: 后台生成的应用ID
X-Timestamp: 当前 Unix 秒级时间戳
X-Nonce: 每次请求生成的唯一随机字符串，长度至少16位
X-Signature: HMAC-SHA256 十六进制签名
```

签名原文：

```text
时间戳 + "\n" + 随机字符串 + "\n" + SHA256(原始请求体字节)
```

使用应用密钥对签名原文执行 HMAC-SHA256，输出小写十六进制字符串。必须对实际发送的原始 JSON 字节计算摘要，不要对解析后重新排序的对象签名。

Python 示例：

```python
import hashlib
import hmac
import json
import secrets
import time

body = json.dumps(
    {
        "externalUserId": "student-10086",
        "verifiedPhone": "13800138000",
        "displayName": "张同学",
        "returnUrl": "https://student.example.com/home",
    },
    ensure_ascii=False,
    separators=(",", ":"),
).encode("utf-8")
timestamp = str(int(time.time()))
nonce = secrets.token_urlsafe(24)
canonical = f"{timestamp}\n{nonce}\n{hashlib.sha256(body).hexdigest()}".encode()
signature = hmac.new(APP_SECRET.encode(), canonical, hashlib.sha256).hexdigest()
```

成功响应：

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "code": "一次性授权码",
    "expiresInSeconds": 60,
    "entryUrl": "https://qa.example.com/?sso_code=一次性授权码"
  }
}
```

接入应用收到响应后，让浏览器、App WebView 或小程序 WebView 打开 `entryUrl`。授权码只能使用一次，60 秒后自动失效。

## 4. 用户匹配规则

- 已经绑定：按“应用 ID + 外部用户 ID”直接找到千问千答学员；
- 首次进入：使用已验证手机号匹配现有学员并建立绑定；
- 手机号不在学员名单且应用未开启自动注册：拒绝进入；
- 手机号不在学员名单且应用已开启自动注册：必须同时提供姓名，系统创建学员、分配该应用的默认权益，再建立外部账号绑定；
- 学员已禁用、尚未生效或权益已过期：拒绝进入；
- 同一应用中的一个千问千答学员只能绑定一个外部用户 ID。

绑定错误时，管理员可以在“应用接入 → 账号绑定”中解除绑定，学员下次进入时重新核对手机号。

## 5. 账号与会话数据规则

- 同一手机号只对应一个千问千答学员账号，不会因接入多个应用重复注册；
- 学员的会话、消息、AI 请求和衍生的求助卡/分享稿仍保存在千问千答服务器；
- 会话按登录来源强制隔离：直接登录只能看到直接会话，从应用 A 进入只能看到应用 A 的会话，不能访问应用 B 或直接登录会话；
- 隔离规则由后端根据签名登录态强制执行，不依赖前端传入来源参数；
- 管理员可在“记录审计”按直接访问、第三方应用和具体应用筛选会话。

## 6. 安全规则

- 所有生产接口必须使用 HTTPS；
- 应用密钥只保存在接入应用服务端；
- 请求时间戳允许误差不超过 5 分钟；
- `X-Nonce` 在 5 分钟内不得重复；
- `returnUrl` 必须与后台白名单完全匹配；
- Redis 不可用时停止签发和兑换授权码，不降级为长期 Token；
- 更新应用密钥后，旧密钥立即失效；
- 停用接入应用后，该应用不能继续申请或兑换登录态；
- 登录成功和失败都会写入登录审计。

## 7. 验收清单

- 已登录来源应用的学员点击后直接进入千问千答；
- 首次进入能按已验证手机号正确绑定；
- 开启自动注册时，新手机号能创建学员并获得指定的默认权益；
- 未开启自动注册时，新手机号仍被拒绝；
- 后续进入不再依赖手机号；
- 同一个授权码第二次兑换失败；
- 错误签名、过期时间戳、重复随机数均被拒绝；
- 非白名单回跳地址被拒绝；
- 禁用或过期学员不能进入；
- 停用应用和更新密钥立即生效；
- 手机号验证码登录仍可正常使用。
- 直接登录、应用 A 和应用 B 各自只能查看、修改和删除本来源会话。
