2026-06-19 10:56:52 +08:00
|
|
|
|
# 查询白名单公开接口 — 对接文档
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
> 面向**下游调用方**(API 用户 / 合作方),与上游数据源配置无关。
|
|
|
|
|
|
> 平台侧仅在 `config.yaml` 顶部保留**一份** `query_whitelist.public_api` 配置。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
## 1. 接口一览
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
| 接口 | 方法 | 路径 | 用途 |
|
|
|
|
|
|
|------|------|------|------|
|
|
|
|
|
|
| **创建规则** | POST | `/api/v1/query-whitelist/entries` | 新建一条屏蔽规则;同用户+身份证+姓名已存在则 **拒绝**(`1013`) |
|
|
|
|
|
|
| **追加接口** | POST | `/api/v1/query-whitelist/entries/append` | 向**已有**规则追加 `api_codes`(去重合并),**不新建记录**;规则不存在则 **拒绝**(`1014`) |
|
|
|
|
|
|
|
|
|
|
|
|
两条接口鉴权、加密、响应格式相同,仅业务语义不同。
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
命中白名单后,对应 API 调用将返回 `1000 查询为空`,不调用上游、不扣费。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 2. 鉴权(双重 + IP)
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
### 2.1 请求头(两个接口共用)
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
| 字段 | 类型 | 必填 | 说明 |
|
|
|
|
|
|
|------|------|------|------|
|
|
|
|
|
|
| `Access-Id` | string | 是 | 目标 API 用户的 Access-Id |
|
|
|
|
|
|
| `Whitelist-Mgmt-Key` | string | 是 | 平台下发的**独立管理密钥**(与 Access Key 不同) |
|
|
|
|
|
|
| `Content-Type` | string | 是 | 固定为 `application/json` |
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
### 2.2 请求体(加密,两个接口共用)
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
与业务 API 相同,外层仅传 `data` 字段:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"data": "<AES-128-CBC Base64 密文>"
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 类型 | 必填 | 说明 |
|
|
|
|
|
|
|------|------|------|------|
|
|
|
|
|
|
| `data` | string | 是 | 业务参数的 AES-128-CBC 密文(Base64 编码) |
|
|
|
|
|
|
|
|
|
|
|
|
使用目标用户的 **Access Key**(16 进制 Secret Key)加密。Access Key **仅用于本地加密,不要写入明文 JSON**。
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
### 2.3 明文业务参数(加密前 JSON,两个接口字段相同)
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
**无需在明文里传 `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 位身份证号 |
|
2026-06-19 10:56:52 +08:00
|
|
|
|
| `api_codes` | string[] | 是 | 产品编码列表;**必须为 JSON 数组**,至少 1 个元素;元素为 string;**禁止**非数组类型;**禁止**通配符 `*` |
|
|
|
|
|
|
| `remark` | string | 否 | 备注,最长 500 字符;追加接口传入非空时会覆盖原备注 |
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
> **`api_codes` 约束(公开接口专属)**
|
|
|
|
|
|
>
|
|
|
|
|
|
> | 传参方式 | 是否允许 | 示例 |
|
|
|
|
|
|
> |----------|----------|------|
|
|
|
|
|
|
> | 字符串数组 | ✅ | `["FLXG0V4B", "JRZQ8A2D"]` |
|
|
|
|
|
|
> | 单个字符串 | ❌ | `"FLXG0V4B"` |
|
|
|
|
|
|
> | 通配全部 `*` | ❌ | `["*"]` |
|
|
|
|
|
|
> | 空数组 | ❌ | `[]` |
|
|
|
|
|
|
> | 非字符串元素 | ❌ | `[123]`、`[{}]` |
|
|
|
|
|
|
|
|
|
|
|
|
### 2.4 IP 白名单
|
|
|
|
|
|
|
|
|
|
|
|
生产环境下,调用方 IP 须在该用户控制台配置的 IP 白名单内(与业务 API 一致)。开发环境(`app.env=development`)跳过。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
## 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. 加密算法
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
与业务 API 完全一致(AES-128-CBC + PKCS7 + 随机 IV):
|
|
|
|
|
|
|
|
|
|
|
|
1. Access Key 为 16 进制字符串,解码为 16 字节密钥
|
|
|
|
|
|
2. 随机生成 16 字节 IV,拼在密文前
|
|
|
|
|
|
3. 整体 Base64 编码后放入 `data`
|
|
|
|
|
|
|
|
|
|
|
|
参考示例:`tyapi-frontend/public/examples/nodejs/demo.js`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
## 5. 响应格式
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
与业务 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,便于审计。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
## 6. 错误码
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
| code | message | 说明 |
|
|
|
|
|
|
|------|---------|------|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
| 0 | 业务成功 | 创建或追加成功 |
|
2026-06-19 10:49:13 +08:00
|
|
|
|
| 1002 | 解密失败 | `data` 解密失败 |
|
|
|
|
|
|
| 1003 | 请求参数结构不正确 | 明文参数缺失或格式错误 |
|
|
|
|
|
|
| 1004 | 未经授权的IP | IP 不在白名单 |
|
|
|
|
|
|
| 1005 | 缺少Access-Id | 请求头缺失 |
|
|
|
|
|
|
| 1006 | 未经授权的AccessId | Access-Id 无效、解密失败或账户冻结 |
|
|
|
|
|
|
| 1010 | 缺少管理密钥 | 未传 `Whitelist-Mgmt-Key` |
|
|
|
|
|
|
| 1011 | 管理密钥无效 | 管理密钥错误 |
|
|
|
|
|
|
| 1012 | 公开接口未启用 | 服务端 `public_api.enabled=false` |
|
2026-06-19 10:56:52 +08:00
|
|
|
|
| 1013 | 规则已存在 | 创建接口:同用户下身份证+姓名重复 |
|
|
|
|
|
|
| 1014 | 规则不存在 | 追加接口:须先调用创建接口 |
|
2026-06-19 10:49:13 +08:00
|
|
|
|
| 1001 | 接口异常 | 系统错误 |
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
## 7. Node.js 调用示例
|
|
|
|
|
|
|
|
|
|
|
|
### 7.1 创建规则
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
```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
|
2026-06-19 10:56:52 +08:00
|
|
|
|
const MGMT_KEY = '2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL'; // 平台管理密钥,见文档 §8
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
### 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();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-19 10:49:13 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
## 8. 平台配置(仅一份)
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
`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`。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
## 9. 字段说明补充
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
### `name = *`
|
|
|
|
|
|
|
|
|
|
|
|
通配姓名:只要请求中的**身份证号**与本规则一致,无论传入什么姓名都会命中并返回「查询为空」。
|
|
|
|
|
|
|
|
|
|
|
|
### `name = 具体姓名`
|
|
|
|
|
|
|
|
|
|
|
|
须身份证 + 姓名**同时一致**才命中。
|
|
|
|
|
|
|
|
|
|
|
|
### 用户隔离
|
|
|
|
|
|
|
|
|
|
|
|
使用谁的 `Access-Id` 调用(请求头),规则就只对该用户(`user_id`)下的 API 调用生效,不会影响其他用户,也不能创建 `user_id=*` 的全局规则。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-19 10:56:52 +08:00
|
|
|
|
## 10. 数据库变更
|
2026-06-19 10:49:13 +08:00
|
|
|
|
|
|
|
|
|
|
新增字段 `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 也会自动加列。
|