Files
tyapi-server/docs/查询白名单公开添加接口对接文档.md
2026-06-19 10:56:52 +08:00

350 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 查询白名单公开接口 — 对接文档
> 面向**下游调用方**API 用户 / 合作方),与上游数据源配置无关。
> 平台侧仅在 `config.yaml` 顶部保留**一份** `query_whitelist.public_api` 配置。
---
## 1. 接口一览
| 接口 | 方法 | 路径 | 用途 |
|------|------|------|------|
| **创建规则** | POST | `/api/v1/query-whitelist/entries` | 新建一条屏蔽规则;同用户+身份证+姓名已存在则 **拒绝**`1013` |
| **追加接口** | POST | `/api/v1/query-whitelist/entries/append` | 向**已有**规则追加 `api_codes`(去重合并),**不新建记录**;规则不存在则 **拒绝**`1014` |
两条接口鉴权、加密、响应格式相同,仅业务语义不同。
命中白名单后,对应 API 调用将返回 `1000 查询为空`,不调用上游、不扣费。
---
## 2. 鉴权(双重 + IP
### 2.1 请求头(两个接口共用)
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `Access-Id` | string | 是 | 目标 API 用户的 Access-Id |
| `Whitelist-Mgmt-Key` | string | 是 | 平台下发的**独立管理密钥**(与 Access Key 不同) |
| `Content-Type` | string | 是 | 固定为 `application/json` |
### 2.2 请求体(加密,两个接口共用)
与业务 API 相同,外层仅传 `data` 字段:
```json
{
"data": "<AES-128-CBC Base64 密文>"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `data` | string | 是 | 业务参数的 AES-128-CBC 密文Base64 编码) |
使用目标用户的 **Access Key**16 进制 Secret Key加密。Access Key **仅用于本地加密,不要写入明文 JSON**
### 2.3 明文业务参数(加密前 JSON两个接口字段相同
**无需在明文里传 `key` 或 `access_id`**。身份校验方式与业务 API 相同:请求头携带 `Access-Id`,服务端用该用户 Access Key 解密 `data`,解密成功即证明调用方持有正确密钥。
```json
{
"name": "*",
"id_card": "350681198611130611",
"api_codes": ["FLXG0V4B", "JRZQ8A2D"],
"remark": "可选备注"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 姓名;填 `*` 表示只匹配身份证,不校验姓名 |
| `id_card` | string | 是 | 18 位身份证号 |
| `api_codes` | string[] | 是 | 产品编码列表;**必须为 JSON 数组**,至少 1 个元素;元素为 string**禁止**非数组类型;**禁止**通配符 `*` |
| `remark` | string | 否 | 备注,最长 500 字符;追加接口传入非空时会覆盖原备注 |
> **`api_codes` 约束(公开接口专属)**
>
> | 传参方式 | 是否允许 | 示例 |
> |----------|----------|------|
> | 字符串数组 | ✅ | `["FLXG0V4B", "JRZQ8A2D"]` |
> | 单个字符串 | ❌ | `"FLXG0V4B"` |
> | 通配全部 `*` | ❌ | `["*"]` |
> | 空数组 | ❌ | `[]` |
> | 非字符串元素 | ❌ | `[123]`、`[{}]` |
### 2.4 IP 白名单
生产环境下,调用方 IP 须在该用户控制台配置的 IP 白名单内(与业务 API 一致)。开发环境(`app.env=development`)跳过。
---
## 3. 两个接口的业务区别
规则的唯一键为 **`user_id` + 身份证 + `name`**,每个组合在表里只有**一条**记录,`api_codes` 存在该行的 JSON 数组中。
### 3.1 创建接口 `POST /entries`
用于**首次**为某身份证建立屏蔽规则。
| 场景 | 行为 |
|------|------|
| 规则不存在 | 新建 1 条记录 |
| 同用户+身份证+姓名已存在 | 返回 `1013 规则已存在`**不新建、不合并** |
示例:第一次传 `api_codes: ["FLXG0V4B"]` → 成功新建。
### 3.2 追加接口 `POST /entries/append`
用于在**已有规则**上补充更多生效接口,适合「先挡一个、后续再加」的场景。
| 场景 | 行为 |
|------|------|
| 规则已存在 | **更新原记录**:将本次 `api_codes` 与已有列表**去重合并**(保留原顺序,新编码追加在后),**不新建第二条** |
| 本次编码均已存在 | 仍返回成功(幂等),`api_codes` 不变 |
| 规则不存在 | 返回 `1014 规则不存在,请先调用创建接口` |
| 原规则 `api_codes``*`(全部接口) | 返回 `1003`,公开接口不支持对「全部接口」规则追加 |
**追加示例:**
```
已有记录user_id=U1, id_card=350681..., name=*, api_codes=["FLXG0V4B"]
第二次调用 append
api_codes: ["JRZQ8A2D"]
结果(同一条记录被更新):
api_codes: ["FLXG0V4B", "JRZQ8A2D"] ← 合并去重,非新建
```
```
第三次再 append
api_codes: ["JRZQ8A2D", "FLXG2E8F"]
结果:
api_codes: ["FLXG0V4B", "JRZQ8A2D", "FLXG2E8F"] ← JRZQ8A2D 已存在则跳过
```
### 3.3 推荐调用顺序
1. 首次屏蔽 → 调 **创建接口**,可一次传齐所有 `api_codes`
2. 后续补接口 → 调 **追加接口**,只传**新增**的编码即可
3. 若创建时返回 `1013` → 改调 **追加接口**
---
## 4. 加密算法
与业务 API 完全一致AES-128-CBC + PKCS7 + 随机 IV
1. Access Key 为 16 进制字符串,解码为 16 字节密钥
2. 随机生成 16 字节 IV拼在密文前
3. 整体 Base64 编码后放入 `data`
参考示例:`tyapi-frontend/public/examples/nodejs/demo.js`
---
## 5. 响应格式
与业务 API 一致:
```json
{
"code": 0,
"message": "业务成功",
"transaction_id": "uuid",
"data": "<AES 加密后的规则详情>"
}
```
`data` 解密后为:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"is_global": false,
"name": "*",
"id_card_masked": "350681********0611",
"api_codes": ["FLXG0V4B", "JRZQ8A2D"],
"status": "enabled",
"remark": "",
"operation_ip": "203.0.113.10",
"created_at": "2026-06-18T10:00:00+08:00",
"updated_at": "2026-06-18T10:00:00+08:00"
}
```
`operation_ip` 为本次添加时的客户端 IP便于审计。
---
## 6. 错误码
| code | message | 说明 |
|------|---------|------|
| 0 | 业务成功 | 创建或追加成功 |
| 1002 | 解密失败 | `data` 解密失败 |
| 1003 | 请求参数结构不正确 | 明文参数缺失或格式错误 |
| 1004 | 未经授权的IP | IP 不在白名单 |
| 1005 | 缺少Access-Id | 请求头缺失 |
| 1006 | 未经授权的AccessId | Access-Id 无效、解密失败或账户冻结 |
| 1010 | 缺少管理密钥 | 未传 `Whitelist-Mgmt-Key` |
| 1011 | 管理密钥无效 | 管理密钥错误 |
| 1012 | 公开接口未启用 | 服务端 `public_api.enabled=false` |
| 1013 | 规则已存在 | 创建接口:同用户下身份证+姓名重复 |
| 1014 | 规则不存在 | 追加接口:须先调用创建接口 |
| 1001 | 接口异常 | 系统错误 |
---
## 7. Node.js 调用示例
### 7.1 创建规则
```javascript
const crypto = require('crypto');
const BASE_URL = 'https://api.tianyuanapi.com';
const ACCESS_ID = process.env.ACCESS_ID;
const ACCESS_KEY = process.env.ACCESS_KEY; // 用户 Access Key
const MGMT_KEY = '2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL'; // 平台管理密钥,见文档 §8
function encrypt(data, keyHex) {
const key = Buffer.from(keyHex, 'hex');
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-128-cbc', key, iv);
cipher.setAutoPadding(true);
let enc = cipher.update(data, 'utf8');
enc = Buffer.concat([iv, enc, cipher.final()]);
return enc.toString('base64');
}
function decrypt(data, keyHex) {
const key = Buffer.from(keyHex, 'hex');
const buf = Buffer.from(data, 'base64');
const iv = buf.slice(0, 16);
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
decipher.setAutoPadding(true);
let dec = decipher.update(buf.slice(16));
dec = Buffer.concat([dec, decipher.final()]);
return dec.toString('utf8');
}
async function addWhitelistEntry() {
const payload = {
name: '*',
id_card: '350681198611130611',
api_codes: ['FLXG0V4B', 'FLXG2E8F', 'JRZQ8A2D'],
remark: '外部系统添加',
};
const res = await fetch(`${BASE_URL}/api/v1/query-whitelist/entries`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Id': ACCESS_ID,
'Whitelist-Mgmt-Key': MGMT_KEY,
},
body: JSON.stringify({ data: encrypt(JSON.stringify(payload), ACCESS_KEY) }),
});
const json = await res.json();
console.log('响应:', json);
if (json.code === 0 && json.data) {
console.log('规则详情:', JSON.parse(decrypt(json.data, ACCESS_KEY)));
}
}
addWhitelistEntry();
```
### 7.2 追加生效接口(去重合并)
```javascript
async function appendWhitelistApiCodes() {
const payload = {
name: '*',
id_card: '350681198611130611',
api_codes: ['JRZQ8A2D'], // 只传本次要追加的编码
remark: '',
};
const res = await fetch(`${BASE_URL}/api/v1/query-whitelist/entries/append`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Access-Id': ACCESS_ID,
'Whitelist-Mgmt-Key': MGMT_KEY,
},
body: JSON.stringify({ data: encrypt(JSON.stringify(payload), ACCESS_KEY) }),
});
const json = await res.json();
console.log('追加响应:', json);
if (json.code === 0 && json.data) {
console.log('合并后规则:', JSON.parse(decrypt(json.data, ACCESS_KEY)));
}
}
// appendWhitelistApiCodes();
```
---
## 8. 平台配置(仅一份)
`config.yaml` 顶部(`app` 段之后),**不要**写在上游数据源配置块内:
```yaml
query_whitelist:
public_api:
enabled: true
management_key: "2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL"
```
| 配置项 | 类型 | 说明 |
|--------|------|------|
| `enabled` | bool | 是否启用公开接口 |
| `management_key` | string | 平台独立管理密钥(当前 **48 位**),调用时放入请求头 `Whitelist-Mgmt-Key` |
**当前环境管理密钥(`Whitelist-Mgmt-Key`**
```
2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL
```
> 该密钥与下游用户的 Access Key 无关,仅用于授权调用本公开接口。生产环境轮换密钥后请同步更新本文档与 `config.yaml`。
---
## 9. 字段说明补充
### `name = *`
通配姓名:只要请求中的**身份证号**与本规则一致,无论传入什么姓名都会命中并返回「查询为空」。
### `name = 具体姓名`
须身份证 + 姓名**同时一致**才命中。
### 用户隔离
使用谁的 `Access-Id` 调用(请求头),规则就只对该用户(`user_id`)下的 API 调用生效,不会影响其他用户,也不能创建 `user_id=*` 的全局规则。
---
## 10. 数据库变更
新增字段 `operation_ip`(记录公开接口添加时的客户端 IP
```sql
ALTER TABLE query_whitelist_entries
ADD COLUMN IF NOT EXISTS operation_ip VARCHAR(64) DEFAULT '';
```
脚本:`scripts/migrate_query_whitelist_operation_ip.sql`
`auto_migrate: true`GORM 也会自动加列。