f
This commit is contained in:
16
config.yaml
16
config.yaml
@@ -8,6 +8,13 @@ app:
|
|||||||
# 子账号入口与主站可同域;邀请链接 {sub_portal_base_url}/sub/auth/register?invite=...
|
# 子账号入口与主站可同域;邀请链接 {sub_portal_base_url}/sub/auth/register?invite=...
|
||||||
sub_portal_base_url: "http://localhost:5173/"
|
sub_portal_base_url: "http://localhost:5173/"
|
||||||
|
|
||||||
|
# 查询白名单公开添加接口(面向下游调用方,与上游数据源配置无关)
|
||||||
|
query_whitelist:
|
||||||
|
public_api:
|
||||||
|
enabled: true
|
||||||
|
# 平台独立管理密钥,请求头 Whitelist-Mgmt-Key(48 位,见对接文档)
|
||||||
|
management_key: "2R12TmEc1e8P3p69RdIoN5Ykjk%H@4orPy7DZv7MXpGByoEL"
|
||||||
|
|
||||||
server:
|
server:
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
port: "8080"
|
port: "8080"
|
||||||
@@ -330,7 +337,6 @@ westdex:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🌍 羽山配置
|
# 🌍 羽山配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -364,7 +370,6 @@ yushan:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 💰 支付宝支付配置
|
# 💰 支付宝支付配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -447,7 +452,6 @@ zhicha:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🌐 木子数据配置
|
# 🌐 木子数据配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -509,7 +513,6 @@ xingwei:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# ✨ 极光配置
|
# ✨ 极光配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -545,7 +548,6 @@ jiguang:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 📄 PDF生成服务配置
|
# 📄 PDF生成服务配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -608,7 +610,6 @@ shumai:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# ✨ 数据宝配置走实时接口
|
# ✨ 数据宝配置走实时接口
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -641,7 +642,6 @@ shujubao:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# ✨ 汇博(BHSC)配置
|
# ✨ 汇博(BHSC)配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -681,7 +681,6 @@ huibo:
|
|||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🌐 诺尔智汇配置
|
# 🌐 诺尔智汇配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
@@ -713,7 +712,6 @@ nuoer:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# 🌐 海宇API(上游数据源)配置
|
# 🌐 海宇API(上游数据源)配置
|
||||||
# ===========================================
|
# ===========================================
|
||||||
|
|||||||
261
docs/查询白名单公开添加接口对接文档.md
Normal file
261
docs/查询白名单公开添加接口对接文档.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# 查询白名单公开添加接口 — 对接文档
|
||||||
|
|
||||||
|
> 面向**下游调用方**(API 用户 / 合作方),与上游数据源配置无关。
|
||||||
|
> 平台侧仅在 `config.yaml` 顶部保留**一份** `query_whitelist.public_api` 配置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 接口概述
|
||||||
|
|
||||||
|
| 项 | 说明 |
|
||||||
|
|----|------|
|
||||||
|
| 地址 | `POST /api/v1/query-whitelist/entries` |
|
||||||
|
| 用途 | 为**当前 access_id 对应用户**添加查询白名单规则 |
|
||||||
|
| 生效范围 | **仅对该用户生效**(`user_id` 自动绑定,不可创建全局规则) |
|
||||||
|
| 计费 | 否 |
|
||||||
|
|
||||||
|
命中白名单后,对应 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;**禁止**传字符串/对象等非数组类型;**禁止**包含通配符 `*`(不可写 `["*"]` 或与其他编码混用) |
|
||||||
|
|
||||||
|
> **`api_codes` 约束(公开接口专属)**
|
||||||
|
>
|
||||||
|
> | 传参方式 | 是否允许 | 示例 |
|
||||||
|
> |----------|----------|------|
|
||||||
|
> | 字符串数组 | ✅ | `["FLXG0V4B", "JRZQ8A2D"]` |
|
||||||
|
> | 单个字符串 | ❌ | `"FLXG0V4B"` |
|
||||||
|
> | 通配全部 `*` | ❌ | `["*"]` |
|
||||||
|
> | 空数组 | ❌ | `[]` |
|
||||||
|
> | 非字符串元素 | ❌ | `[123]`、`[{}]` |
|
||||||
|
| `remark` | string | 否 | 备注,最长 500 字符 |
|
||||||
|
|
||||||
|
### 2.4 IP 白名单
|
||||||
|
|
||||||
|
生产环境下,调用方 IP 须在该用户控制台配置的 IP 白名单内(与业务 API 一致)。开发环境(`app.env=development`)跳过。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 加密算法
|
||||||
|
|
||||||
|
与业务 API 完全一致(AES-128-CBC + PKCS7 + 随机 IV):
|
||||||
|
|
||||||
|
1. Access Key 为 16 进制字符串,解码为 16 字节密钥
|
||||||
|
2. 随机生成 16 字节 IV,拼在密文前
|
||||||
|
3. 整体 Base64 编码后放入 `data`
|
||||||
|
|
||||||
|
参考示例:`tyapi-frontend/public/examples/nodejs/demo.js`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 响应格式
|
||||||
|
|
||||||
|
与业务 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,便于审计。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 错误码
|
||||||
|
|
||||||
|
| 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 | 规则已存在 | 同用户下身份证+姓名重复 |
|
||||||
|
| 1001 | 接口异常 | 系统错误 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Node.js 调用示例
|
||||||
|
|
||||||
|
```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'; // 平台管理密钥,见文档 §7
|
||||||
|
|
||||||
|
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. 平台配置(仅一份)
|
||||||
|
|
||||||
|
`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`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 字段说明补充
|
||||||
|
|
||||||
|
### `name = *`
|
||||||
|
|
||||||
|
通配姓名:只要请求中的**身份证号**与本规则一致,无论传入什么姓名都会命中并返回「查询为空」。
|
||||||
|
|
||||||
|
### `name = 具体姓名`
|
||||||
|
|
||||||
|
须身份证 + 姓名**同时一致**才命中。
|
||||||
|
|
||||||
|
### 用户隔离
|
||||||
|
|
||||||
|
使用谁的 `Access-Id` 调用(请求头),规则就只对该用户(`user_id`)下的 API 调用生效,不会影响其他用户,也不能创建 `user_id=*` 的全局规则。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 数据库变更
|
||||||
|
|
||||||
|
新增字段 `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 也会自动加列。
|
||||||
@@ -34,6 +34,7 @@ type QueryWhitelistEntryResponse struct {
|
|||||||
APICodes []string `json:"api_codes"`
|
APICodes []string `json:"api_codes"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
|
OperationIP string `json:"operation_ip,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
@@ -51,6 +52,19 @@ type QueryWhitelistImportLegacyResponse struct {
|
|||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueryWhitelistPublicEncryptedRequest 公开接口外层请求(data 为 AES 密文)
|
||||||
|
type QueryWhitelistPublicEncryptedRequest struct {
|
||||||
|
Data string `json:"data" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryWhitelistPublicPayload 公开接口解密后的业务参数(不含 key/access_id,身份由请求头 + 解密成功证明)
|
||||||
|
type QueryWhitelistPublicPayload struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
IDCard string `json:"id_card"`
|
||||||
|
APICodes []string `json:"api_codes"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
func NewQueryWhitelistEntryResponse(entry *entities.QueryWhitelistEntry) QueryWhitelistEntryResponse {
|
func NewQueryWhitelistEntryResponse(entry *entities.QueryWhitelistEntry) QueryWhitelistEntryResponse {
|
||||||
apiCodes := []string(entry.APICodes)
|
apiCodes := []string(entry.APICodes)
|
||||||
if apiCodes == nil {
|
if apiCodes == nil {
|
||||||
@@ -65,6 +79,7 @@ func NewQueryWhitelistEntryResponse(entry *entities.QueryWhitelistEntry) QueryWh
|
|||||||
APICodes: apiCodes,
|
APICodes: apiCodes,
|
||||||
Status: entry.Status,
|
Status: entry.Status,
|
||||||
Remark: entry.Remark,
|
Remark: entry.Remark,
|
||||||
|
OperationIP: entry.OperationIP,
|
||||||
CreatedAt: entry.CreatedAt,
|
CreatedAt: entry.CreatedAt,
|
||||||
UpdatedAt: entry.UpdatedAt,
|
UpdatedAt: entry.UpdatedAt,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ var (
|
|||||||
ErrBusiness = errors.New("业务失败")
|
ErrBusiness = errors.New("业务失败")
|
||||||
ErrSubordinateLinkNotFound = errors.New("非子账号,无法使用master_accessid")
|
ErrSubordinateLinkNotFound = errors.New("非子账号,无法使用master_accessid")
|
||||||
ErrSubordinateParentMismatch = errors.New("master_accessid与主账号不匹配")
|
ErrSubordinateParentMismatch = errors.New("master_accessid与主账号不匹配")
|
||||||
|
ErrMissingMgmtKey = errors.New("缺少管理密钥")
|
||||||
|
ErrInvalidMgmtKey = errors.New("管理密钥无效")
|
||||||
|
ErrPublicAPIDisabled = errors.New("公开接口未启用")
|
||||||
|
ErrWhitelistExists = errors.New("规则已存在")
|
||||||
)
|
)
|
||||||
|
|
||||||
// 错误码映射 - 严格按照用户要求
|
// 错误码映射 - 严格按照用户要求
|
||||||
@@ -50,6 +54,10 @@ var ErrorCodeMap = map[error]int{
|
|||||||
ErrBusiness: 2001,
|
ErrBusiness: 2001,
|
||||||
ErrSubordinateLinkNotFound: 1301,
|
ErrSubordinateLinkNotFound: 1301,
|
||||||
ErrSubordinateParentMismatch: 1302,
|
ErrSubordinateParentMismatch: 1302,
|
||||||
|
ErrMissingMgmtKey: 1010,
|
||||||
|
ErrInvalidMgmtKey: 1011,
|
||||||
|
ErrPublicAPIDisabled: 1012,
|
||||||
|
ErrWhitelistExists: 1013,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetErrorCode 获取错误对应的错误码
|
// GetErrorCode 获取错误对应的错误码
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"tyapi-server/internal/application/api/dto"
|
"tyapi-server/internal/application/api/dto"
|
||||||
|
"tyapi-server/internal/config"
|
||||||
"tyapi-server/internal/domains/api/entities"
|
"tyapi-server/internal/domains/api/entities"
|
||||||
"tyapi-server/internal/domains/api/repositories"
|
"tyapi-server/internal/domains/api/repositories"
|
||||||
api_services "tyapi-server/internal/domains/api/services"
|
api_services "tyapi-server/internal/domains/api/services"
|
||||||
@@ -18,6 +19,7 @@ import (
|
|||||||
|
|
||||||
type QueryWhitelistApplicationService interface {
|
type QueryWhitelistApplicationService interface {
|
||||||
CreateEntry(ctx context.Context, adminUserID string, req *dto.QueryWhitelistEntryRequest) (*dto.QueryWhitelistEntryResponse, error)
|
CreateEntry(ctx context.Context, adminUserID string, req *dto.QueryWhitelistEntryRequest) (*dto.QueryWhitelistEntryResponse, error)
|
||||||
|
CreateEntryPublic(ctx context.Context, headerAccessID, managementKey, clientIP, encryptedData string) (*dto.QueryWhitelistEntryResponse, string, error)
|
||||||
UpdateEntry(ctx context.Context, adminUserID, id string, req *dto.QueryWhitelistEntryUpdateRequest) (*dto.QueryWhitelistEntryResponse, error)
|
UpdateEntry(ctx context.Context, adminUserID, id string, req *dto.QueryWhitelistEntryUpdateRequest) (*dto.QueryWhitelistEntryResponse, error)
|
||||||
UpdateEntryStatus(ctx context.Context, adminUserID, id, status string) (*dto.QueryWhitelistEntryResponse, error)
|
UpdateEntryStatus(ctx context.Context, adminUserID, id, status string) (*dto.QueryWhitelistEntryResponse, error)
|
||||||
DeleteEntry(ctx context.Context, id string) error
|
DeleteEntry(ctx context.Context, id string) error
|
||||||
@@ -29,17 +31,23 @@ type QueryWhitelistApplicationService interface {
|
|||||||
type QueryWhitelistApplicationServiceImpl struct {
|
type QueryWhitelistApplicationServiceImpl struct {
|
||||||
repo repositories.QueryWhitelistRepository
|
repo repositories.QueryWhitelistRepository
|
||||||
queryWhitelistSvc api_services.QueryWhitelistService
|
queryWhitelistSvc api_services.QueryWhitelistService
|
||||||
|
apiUserService api_services.ApiUserAggregateService
|
||||||
|
config *config.Config
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewQueryWhitelistApplicationService(
|
func NewQueryWhitelistApplicationService(
|
||||||
repo repositories.QueryWhitelistRepository,
|
repo repositories.QueryWhitelistRepository,
|
||||||
queryWhitelistSvc api_services.QueryWhitelistService,
|
queryWhitelistSvc api_services.QueryWhitelistService,
|
||||||
|
apiUserService api_services.ApiUserAggregateService,
|
||||||
|
config *config.Config,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) QueryWhitelistApplicationService {
|
) QueryWhitelistApplicationService {
|
||||||
return &QueryWhitelistApplicationServiceImpl{
|
return &QueryWhitelistApplicationServiceImpl{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
queryWhitelistSvc: queryWhitelistSvc,
|
queryWhitelistSvc: queryWhitelistSvc,
|
||||||
|
apiUserService: apiUserService,
|
||||||
|
config: config,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,35 +57,7 @@ func (s *QueryWhitelistApplicationServiceImpl) CreateEntry(
|
|||||||
adminUserID string,
|
adminUserID string,
|
||||||
req *dto.QueryWhitelistEntryRequest,
|
req *dto.QueryWhitelistEntryRequest,
|
||||||
) (*dto.QueryWhitelistEntryResponse, error) {
|
) (*dto.QueryWhitelistEntryResponse, error) {
|
||||||
if err := validateQueryWhitelistRequest(req.UserID, req.Name, req.IDCard, req.APICodes); err != nil {
|
return s.createEntry(ctx, adminUserID, req, "")
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
idCardHash := api_services.HashIDCard(req.IDCard)
|
|
||||||
exists, err := s.repo.ExistsByUserIDCardHashAndName(ctx, req.UserID, idCardHash, strings.TrimSpace(req.Name), "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
return nil, fmt.Errorf("该用户下已存在相同的身份证与姓名规则")
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := &entities.QueryWhitelistEntry{
|
|
||||||
UserID: strings.TrimSpace(req.UserID),
|
|
||||||
Name: normalizeWhitelistName(req.Name),
|
|
||||||
IDCardHash: idCardHash,
|
|
||||||
IDCardMasked: api_services.MaskIDCard(req.IDCard),
|
|
||||||
APICodes: entities.APICodeList(req.APICodes),
|
|
||||||
Status: entities.QueryWhitelistStatusEnabled,
|
|
||||||
Remark: strings.TrimSpace(req.Remark),
|
|
||||||
CreatedBy: &adminUserID,
|
|
||||||
}
|
|
||||||
if err := s.repo.Create(ctx, entry); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.queryWhitelistSvc.InvalidateCache(entry.UserID, idCardHash)
|
|
||||||
resp := dto.NewQueryWhitelistEntryResponse(entry)
|
|
||||||
return &resp, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *QueryWhitelistApplicationServiceImpl) UpdateEntry(
|
func (s *QueryWhitelistApplicationServiceImpl) UpdateEntry(
|
||||||
|
|||||||
213
internal/application/api/query_whitelist_public_service.go
Normal file
213
internal/application/api/query_whitelist_public_service.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"tyapi-server/internal/application/api/dto"
|
||||||
|
"tyapi-server/internal/config"
|
||||||
|
"tyapi-server/internal/domains/api/entities"
|
||||||
|
api_services "tyapi-server/internal/domains/api/services"
|
||||||
|
"tyapi-server/internal/shared/crypto"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const queryWhitelistMgmtKeyHeader = "Whitelist-Mgmt-Key"
|
||||||
|
|
||||||
|
// QueryWhitelistMgmtKeyHeader 公开接口管理密钥请求头名
|
||||||
|
func QueryWhitelistMgmtKeyHeader() string {
|
||||||
|
return queryWhitelistMgmtKeyHeader
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEntryPublic 公开接口:解密业务参数后为当前 access_id 对应用户添加规则(仅对该用户生效)
|
||||||
|
func (s *QueryWhitelistApplicationServiceImpl) CreateEntryPublic(
|
||||||
|
ctx context.Context,
|
||||||
|
headerAccessID, managementKey, clientIP, encryptedData string,
|
||||||
|
) (*dto.QueryWhitelistEntryResponse, string, error) {
|
||||||
|
if !s.config.QueryWhitelist.PublicAPI.Enabled {
|
||||||
|
return nil, "", ErrPublicAPIDisabled
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(managementKey) == "" {
|
||||||
|
return nil, "", ErrMissingMgmtKey
|
||||||
|
}
|
||||||
|
if !constantTimeEqual(managementKey, s.config.QueryWhitelist.PublicAPI.ManagementKey) {
|
||||||
|
return nil, "", ErrInvalidMgmtKey
|
||||||
|
}
|
||||||
|
headerAccessID = strings.TrimSpace(headerAccessID)
|
||||||
|
if headerAccessID == "" {
|
||||||
|
return nil, "", ErrMissingAccessId
|
||||||
|
}
|
||||||
|
|
||||||
|
apiUser, err := s.apiUserService.LoadApiUserByAccessId(ctx, headerAccessID)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("公开白名单接口 AccessId 无效", zap.String("access_id", headerAccessID), zap.Error(err))
|
||||||
|
return nil, "", ErrInvalidAccessId
|
||||||
|
}
|
||||||
|
if apiUser.IsFrozen() {
|
||||||
|
return nil, "", ErrFrozenAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err := crypto.AesDecrypt(encryptedData, apiUser.SecretKey)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Warn("公开白名单接口解密失败", zap.String("access_id", headerAccessID), zap.Error(err))
|
||||||
|
return nil, "", ErrDecryptFail
|
||||||
|
}
|
||||||
|
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(decrypted, &raw); err != nil {
|
||||||
|
return nil, "", ErrRequestParam
|
||||||
|
}
|
||||||
|
if err := validatePublicAPICodesField(raw["api_codes"]); err != nil {
|
||||||
|
return nil, "", MapWhitelistAppError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload dto.QueryWhitelistPublicPayload
|
||||||
|
if err := json.Unmarshal(decrypted, &payload); err != nil {
|
||||||
|
return nil, "", ErrRequestParam
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.config.App.IsDevelopment() && !apiUser.IsWhiteListed(clientIP) {
|
||||||
|
s.logger.Warn("公开白名单接口 IP 未授权",
|
||||||
|
zap.String("access_id", headerAccessID),
|
||||||
|
zap.String("client_ip", clientIP))
|
||||||
|
return nil, "", ErrInvalidIP
|
||||||
|
}
|
||||||
|
|
||||||
|
createdBy := "public_api:" + headerAccessID
|
||||||
|
resp, err := s.createEntry(ctx, createdBy, &dto.QueryWhitelistEntryRequest{
|
||||||
|
UserID: apiUser.UserId,
|
||||||
|
Name: payload.Name,
|
||||||
|
IDCard: payload.IDCard,
|
||||||
|
APICodes: payload.APICodes,
|
||||||
|
Remark: payload.Remark,
|
||||||
|
}, clientIP)
|
||||||
|
return resp, apiUser.SecretKey, MapWhitelistAppError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *QueryWhitelistApplicationServiceImpl) createEntry(
|
||||||
|
ctx context.Context,
|
||||||
|
createdBy string,
|
||||||
|
req *dto.QueryWhitelistEntryRequest,
|
||||||
|
operationIP string,
|
||||||
|
) (*dto.QueryWhitelistEntryResponse, error) {
|
||||||
|
if err := validateQueryWhitelistRequest(req.UserID, req.Name, req.IDCard, req.APICodes); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idCardHash := api_services.HashIDCard(req.IDCard)
|
||||||
|
exists, err := s.repo.ExistsByUserIDCardHashAndName(ctx, req.UserID, idCardHash, strings.TrimSpace(req.Name), "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("该用户下已存在相同的身份证与姓名规则")
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := &entities.QueryWhitelistEntry{
|
||||||
|
UserID: strings.TrimSpace(req.UserID),
|
||||||
|
Name: normalizeWhitelistName(req.Name),
|
||||||
|
IDCardHash: idCardHash,
|
||||||
|
IDCardMasked: api_services.MaskIDCard(req.IDCard),
|
||||||
|
APICodes: entities.APICodeList(req.APICodes),
|
||||||
|
Status: entities.QueryWhitelistStatusEnabled,
|
||||||
|
Remark: strings.TrimSpace(req.Remark),
|
||||||
|
OperationIP: strings.TrimSpace(operationIP),
|
||||||
|
CreatedBy: &createdBy,
|
||||||
|
}
|
||||||
|
if err := s.repo.Create(ctx, entry); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.queryWhitelistSvc.InvalidateCache(entry.UserID, idCardHash)
|
||||||
|
resp := dto.NewQueryWhitelistEntryResponse(entry)
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func constantTimeEqual(a, b string) bool {
|
||||||
|
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePublicAPICodesField 公开接口:api_codes 必须为 string 数组,且禁止通配符 *
|
||||||
|
func validatePublicAPICodesField(raw json.RawMessage) error {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return fmt.Errorf("api_codes 必填且必须为 string 数组")
|
||||||
|
}
|
||||||
|
var probe interface{}
|
||||||
|
if err := json.Unmarshal(raw, &probe); err != nil {
|
||||||
|
return fmt.Errorf("api_codes 必须为 string 数组")
|
||||||
|
}
|
||||||
|
arr, ok := probe.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("api_codes 必须为 string 数组,不可传字符串或其他类型")
|
||||||
|
}
|
||||||
|
if len(arr) == 0 {
|
||||||
|
return fmt.Errorf("api_codes 不能为空")
|
||||||
|
}
|
||||||
|
codes := make([]string, 0, len(arr))
|
||||||
|
for _, item := range arr {
|
||||||
|
s, ok := item.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("api_codes 数组元素必须为 string")
|
||||||
|
}
|
||||||
|
code := strings.TrimSpace(s)
|
||||||
|
if code == "" {
|
||||||
|
return fmt.Errorf("api_codes 不能包含空值")
|
||||||
|
}
|
||||||
|
if code == "*" {
|
||||||
|
return fmt.Errorf("公开接口不允许 api_codes 使用通配符 *")
|
||||||
|
}
|
||||||
|
codes = append(codes, code)
|
||||||
|
}
|
||||||
|
return validateAPICodes(codes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePublicAPIManagementKey 校验平台管理密钥
|
||||||
|
func ValidatePublicAPIManagementKey(cfg *config.Config, key string) error {
|
||||||
|
if !cfg.QueryWhitelist.PublicAPI.Enabled {
|
||||||
|
return ErrPublicAPIDisabled
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(key) == "" {
|
||||||
|
return ErrMissingMgmtKey
|
||||||
|
}
|
||||||
|
if !constantTimeEqual(key, cfg.QueryWhitelist.PublicAPI.ManagementKey) {
|
||||||
|
return ErrInvalidMgmtKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapWhitelistAppError 将校验错误映射为公开 API 错误码
|
||||||
|
func MapWhitelistAppError(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
switch {
|
||||||
|
case strings.Contains(msg, "身份证号格式不正确"),
|
||||||
|
strings.Contains(msg, "api_codes"),
|
||||||
|
strings.Contains(msg, "name 不能为空"),
|
||||||
|
strings.Contains(msg, "user_id 不能为空"):
|
||||||
|
return ErrRequestParam
|
||||||
|
case strings.Contains(msg, "已存在"):
|
||||||
|
return ErrWhitelistExists
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicAPIErrorMessage 公开接口错误文案
|
||||||
|
func PublicAPIErrorMessage(err error) string {
|
||||||
|
if err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
switch err {
|
||||||
|
case ErrWhitelistExists:
|
||||||
|
return "规则已存在"
|
||||||
|
case ErrRequestParam:
|
||||||
|
return "请求参数结构不正确"
|
||||||
|
default:
|
||||||
|
return GetErrorMessage(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidatePublicAPICodesField(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
raw string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{name: "valid array", raw: `["FLXG0V4B","JRZQ8A2D"]`},
|
||||||
|
{name: "string not array", raw: `"FLXG0V4B"`, wantErr: true},
|
||||||
|
{name: "wildcard star", raw: `["*"]`, wantErr: true},
|
||||||
|
{name: "star mixed", raw: `["FLXG0V4B","*"]`, wantErr: true},
|
||||||
|
{name: "empty array", raw: `[]`, wantErr: true},
|
||||||
|
{name: "missing field", raw: "", wantErr: true},
|
||||||
|
{name: "number element", raw: `[123]`, wantErr: true},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
var raw json.RawMessage
|
||||||
|
if tt.raw != "" {
|
||||||
|
raw = json.RawMessage(tt.raw)
|
||||||
|
}
|
||||||
|
err := validatePublicAPICodesField(raw)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Fatalf("validatePublicAPICodesField() err=%v wantErr=%v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,18 @@ type Config struct {
|
|||||||
Huibo HuiboConfig `mapstructure:"huibo"`
|
Huibo HuiboConfig `mapstructure:"huibo"`
|
||||||
Nuoer NuoerConfig `mapstructure:"nuoer"`
|
Nuoer NuoerConfig `mapstructure:"nuoer"`
|
||||||
Haiyuapi HaiyuapiConfig `mapstructure:"haiyuapi"`
|
Haiyuapi HaiyuapiConfig `mapstructure:"haiyuapi"`
|
||||||
|
QueryWhitelist QueryWhitelistConfig `mapstructure:"query_whitelist"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryWhitelistConfig 查询白名单配置
|
||||||
|
type QueryWhitelistConfig struct {
|
||||||
|
PublicAPI QueryWhitelistPublicAPIConfig `mapstructure:"public_api"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryWhitelistPublicAPIConfig 查询白名单公开添加接口配置
|
||||||
|
type QueryWhitelistPublicAPIConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
ManagementKey string `mapstructure:"management_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig HTTP服务器配置
|
// ServerConfig HTTP服务器配置
|
||||||
|
|||||||
@@ -813,7 +813,15 @@ func NewContainer() *Container {
|
|||||||
// API域应用服务
|
// API域应用服务
|
||||||
fx.Provide(
|
fx.Provide(
|
||||||
fx.Annotate(
|
fx.Annotate(
|
||||||
api_app.NewQueryWhitelistApplicationService,
|
func(
|
||||||
|
repo domain_api_repo.QueryWhitelistRepository,
|
||||||
|
queryWhitelistSvc api_services.QueryWhitelistService,
|
||||||
|
apiUserService api_services.ApiUserAggregateService,
|
||||||
|
cfg *config.Config,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) api_app.QueryWhitelistApplicationService {
|
||||||
|
return api_app.NewQueryWhitelistApplicationService(repo, queryWhitelistSvc, apiUserService, cfg, logger)
|
||||||
|
},
|
||||||
fx.As(new(api_app.QueryWhitelistApplicationService)),
|
fx.As(new(api_app.QueryWhitelistApplicationService)),
|
||||||
),
|
),
|
||||||
// API应用服务 - 绑定到接口
|
// API应用服务 - 绑定到接口
|
||||||
@@ -1321,6 +1329,7 @@ func NewContainer() *Container {
|
|||||||
// 管理员安全HTTP处理器
|
// 管理员安全HTTP处理器
|
||||||
handlers.NewAdminSecurityHandler,
|
handlers.NewAdminSecurityHandler,
|
||||||
handlers.NewAdminQueryWhitelistHandler,
|
handlers.NewAdminQueryWhitelistHandler,
|
||||||
|
handlers.NewPublicQueryWhitelistHandler,
|
||||||
// 文章HTTP处理器
|
// 文章HTTP处理器
|
||||||
func(
|
func(
|
||||||
appService article.ArticleApplicationService,
|
appService article.ArticleApplicationService,
|
||||||
@@ -1416,6 +1425,7 @@ func NewContainer() *Container {
|
|||||||
// 管理员安全路由
|
// 管理员安全路由
|
||||||
routes.NewAdminSecurityRoutes,
|
routes.NewAdminSecurityRoutes,
|
||||||
routes.NewAdminQueryWhitelistRoutes,
|
routes.NewAdminQueryWhitelistRoutes,
|
||||||
|
routes.NewPublicQueryWhitelistRoutes,
|
||||||
// PDFG路由
|
// PDFG路由
|
||||||
routes.NewPDFGRoutes,
|
routes.NewPDFGRoutes,
|
||||||
// 企业报告页面路由
|
// 企业报告页面路由
|
||||||
@@ -1537,6 +1547,7 @@ func RegisterRoutes(
|
|||||||
statisticsRoutes *routes.StatisticsRoutes,
|
statisticsRoutes *routes.StatisticsRoutes,
|
||||||
adminSecurityRoutes *routes.AdminSecurityRoutes,
|
adminSecurityRoutes *routes.AdminSecurityRoutes,
|
||||||
adminQueryWhitelistRoutes *routes.AdminQueryWhitelistRoutes,
|
adminQueryWhitelistRoutes *routes.AdminQueryWhitelistRoutes,
|
||||||
|
publicQueryWhitelistRoutes *routes.PublicQueryWhitelistRoutes,
|
||||||
pdfgRoutes *routes.PDFGRoutes,
|
pdfgRoutes *routes.PDFGRoutes,
|
||||||
qyglReportRoutes *routes.QYGLReportRoutes,
|
qyglReportRoutes *routes.QYGLReportRoutes,
|
||||||
jwtAuth *middleware.JWTAuthMiddleware,
|
jwtAuth *middleware.JWTAuthMiddleware,
|
||||||
@@ -1566,6 +1577,7 @@ func RegisterRoutes(
|
|||||||
statisticsRoutes.Register(router)
|
statisticsRoutes.Register(router)
|
||||||
adminSecurityRoutes.Register(router)
|
adminSecurityRoutes.Register(router)
|
||||||
adminQueryWhitelistRoutes.Register(router)
|
adminQueryWhitelistRoutes.Register(router)
|
||||||
|
publicQueryWhitelistRoutes.Register(router)
|
||||||
pdfgRoutes.Register(router)
|
pdfgRoutes.Register(router)
|
||||||
qyglReportRoutes.Register(router)
|
qyglReportRoutes.Register(router)
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ type QueryWhitelistEntry struct {
|
|||||||
APICodes APICodeList `gorm:"type:json;not null" json:"api_codes"`
|
APICodes APICodeList `gorm:"type:json;not null" json:"api_codes"`
|
||||||
Status string `gorm:"type:varchar(20);not null;default:'enabled'" json:"status"`
|
Status string `gorm:"type:varchar(20);not null;default:'enabled'" json:"status"`
|
||||||
Remark string `gorm:"type:varchar(500)" json:"remark"`
|
Remark string `gorm:"type:varchar(500)" json:"remark"`
|
||||||
|
OperationIP string `gorm:"type:varchar(64)" json:"operation_ip"`
|
||||||
CreatedBy *string `gorm:"type:varchar(36)" json:"created_by,omitempty"`
|
CreatedBy *string `gorm:"type:varchar(36)" json:"created_by,omitempty"`
|
||||||
UpdatedBy *string `gorm:"type:varchar(36)" json:"updated_by,omitempty"`
|
UpdatedBy *string `gorm:"type:varchar(36)" json:"updated_by,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
api_app "tyapi-server/internal/application/api"
|
||||||
|
"tyapi-server/internal/application/api/dto"
|
||||||
|
"tyapi-server/internal/shared/crypto"
|
||||||
|
"tyapi-server/internal/shared/interfaces"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PublicQueryWhitelistHandler 查询白名单公开添加接口(面向下游调用方)
|
||||||
|
type PublicQueryWhitelistHandler struct {
|
||||||
|
appService api_app.QueryWhitelistApplicationService
|
||||||
|
validator interfaces.RequestValidator
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPublicQueryWhitelistHandler(
|
||||||
|
appService api_app.QueryWhitelistApplicationService,
|
||||||
|
validator interfaces.RequestValidator,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *PublicQueryWhitelistHandler {
|
||||||
|
return &PublicQueryWhitelistHandler{
|
||||||
|
appService: appService,
|
||||||
|
validator: validator,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateEntry 公开添加查询白名单规则
|
||||||
|
func (h *PublicQueryWhitelistHandler) CreateEntry(c *gin.Context) {
|
||||||
|
accessID := c.GetHeader("Access-Id")
|
||||||
|
if accessID == "" {
|
||||||
|
c.JSON(200, dto.NewErrorResponse(1005, "缺少Access-Id", ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mgmtKey := c.GetHeader(api_app.QueryWhitelistMgmtKeyHeader())
|
||||||
|
if mgmtKey == "" {
|
||||||
|
c.JSON(200, dto.NewErrorResponse(api_app.GetErrorCode(api_app.ErrMissingMgmtKey), "缺少管理密钥", ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req dto.QueryWhitelistPublicEncryptedRequest
|
||||||
|
if err := h.validator.BindAndValidate(c, &req); err != nil {
|
||||||
|
c.JSON(200, dto.NewErrorResponse(1003, "请求参数结构不正确", ""))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionID := uuid.New().String()
|
||||||
|
result, secretKey, err := h.appService.CreateEntryPublic(
|
||||||
|
c.Request.Context(),
|
||||||
|
accessID,
|
||||||
|
mgmtKey,
|
||||||
|
c.ClientIP(),
|
||||||
|
req.Data,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
code := api_app.GetErrorCode(err)
|
||||||
|
message := api_app.PublicAPIErrorMessage(err)
|
||||||
|
c.JSON(200, dto.NewErrorResponse(code, message, transactionID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("序列化公开白名单响应失败", zap.Error(err))
|
||||||
|
c.JSON(200, dto.NewErrorResponse(1001, "接口异常", transactionID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted, err := crypto.AesEncrypt(respBytes, secretKey)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error("加密公开白名单响应失败", zap.Error(err))
|
||||||
|
c.JSON(200, dto.NewErrorResponse(1001, "接口异常", transactionID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(200, dto.NewSuccessResponse(transactionID, encrypted))
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package routes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tyapi-server/internal/infrastructure/http/handlers"
|
||||||
|
sharedhttp "tyapi-server/internal/shared/http"
|
||||||
|
"tyapi-server/internal/shared/middleware"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PublicQueryWhitelistRoutes 查询白名单公开接口路由(面向下游调用方)
|
||||||
|
type PublicQueryWhitelistRoutes struct {
|
||||||
|
handler *handlers.PublicQueryWhitelistHandler
|
||||||
|
domainAuthMiddleware *middleware.DomainAuthMiddleware
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPublicQueryWhitelistRoutes(
|
||||||
|
handler *handlers.PublicQueryWhitelistHandler,
|
||||||
|
domainAuthMiddleware *middleware.DomainAuthMiddleware,
|
||||||
|
logger *zap.Logger,
|
||||||
|
) *PublicQueryWhitelistRoutes {
|
||||||
|
return &PublicQueryWhitelistRoutes{
|
||||||
|
handler: handler,
|
||||||
|
domainAuthMiddleware: domainAuthMiddleware,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *PublicQueryWhitelistRoutes) Register(router *sharedhttp.GinRouter) {
|
||||||
|
group := router.GetEngine().Group("/api/v1/query-whitelist")
|
||||||
|
group.Use(r.domainAuthMiddleware.Handle(""))
|
||||||
|
group.POST("/entries", r.handler.CreateEntry)
|
||||||
|
r.logger.Info("查询白名单公开接口路由注册完成")
|
||||||
|
}
|
||||||
3
scripts/migrate_query_whitelist_operation_ip.sql
Normal file
3
scripts/migrate_query_whitelist_operation_ip.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- 为 query_whitelist_entries 增加操作 IP 字段(公开接口添加时记录调用方 IP)
|
||||||
|
ALTER TABLE query_whitelist_entries
|
||||||
|
ADD COLUMN IF NOT EXISTS operation_ip VARCHAR(64) DEFAULT '';
|
||||||
Reference in New Issue
Block a user