Compare commits
92 Commits
bfa8bbcfcb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c5970da195 | |||
| 1bcb4a9c2e | |||
| 8877cf9691 | |||
| f63e6df9f9 | |||
| a00fe12141 | |||
| 6b80182986 | |||
| bf4c114ee2 | |||
| 4cd3954574 | |||
| d7a5589873 | |||
| b0ec75d1af | |||
| 57d18be972 | |||
| 3d8775b6dc | |||
| f40950f890 | |||
| ba21a8f965 | |||
| 7d2716da7a | |||
| 9a7bda9527 | |||
| abdae033f0 | |||
| 96abacd392 | |||
| 4e6c93413e | |||
| 9c8dbd458f | |||
| 9e9cee02f5 | |||
| 360bd579ce | |||
| db889ccba0 | |||
| 25a4961328 | |||
| 578e68a76b | |||
| 019e47896d | |||
| c0898e6829 | |||
| 4ee6e891cd | |||
| 44b5f6b145 | |||
| 677b7362cf | |||
| 02dbc02fe8 | |||
| 374143995e | |||
| 7a957a6b87 | |||
| c885d562ee | |||
| 9f36cd8b63 | |||
| 4122f874fc | |||
| 9a32387b21 | |||
| 7bf9150cfc | |||
| fecd5a38fd | |||
| 2636d9dff6 | |||
| 927b08b871 | |||
| dedd4a60a4 | |||
| a54a19e439 | |||
| 6dd392f673 | |||
| 8d5da9d88e | |||
| bc6dce21ee | |||
| 5630d93de6 | |||
| d12529307b | |||
| f17e22f4c8 | |||
| 8c0c16006e | |||
| 532503ffe3 | |||
| af37f8620c | |||
| fb495bc0ca | |||
| 2c57ac0dab | |||
| 4b437ecf56 | |||
| abf6284482 | |||
| e6ab833099 | |||
| 47cbc5b3a5 | |||
| f400052f95 | |||
| a38c58c357 | |||
| ede446fb90 | |||
| e23897a13f | |||
| debd0a973f | |||
| b424082f01 | |||
| 4afbd7dec7 | |||
| 070310cfa7 | |||
| 62e220a7b8 | |||
| c37cf2b54a | |||
| 25a3b4a761 | |||
| 1c83102e06 | |||
| d55520e69c | |||
| 4ea7cf4ecb | |||
| 3eae08a576 | |||
| c9126dd780 | |||
| f48289b32b | |||
| 0091e01574 | |||
| d2b806eda0 | |||
| ff86cb6fb9 | |||
| a6f858dbd3 | |||
| 6a1a59de8d | |||
| dbcecde4e0 | |||
| 3158bf8c04 | |||
| 860756b767 | |||
| 3c90144d51 | |||
| 29c49d6a00 | |||
| 5bff33547c | |||
| f50e11a052 | |||
| d1e06984ac | |||
| 2325a110b6 | |||
| 167dd63a7a | |||
| 360fed3907 | |||
| 88787c6145 |
@@ -53,9 +53,8 @@ COPY --from=builder /app/tyapi-server .
|
||||
COPY config.yaml .
|
||||
COPY configs/ ./configs/
|
||||
|
||||
# 复制资源文件(直接从构建上下文复制,与配置文件一致)
|
||||
COPY resources/etc ./resources/etc
|
||||
COPY resources/pdf ./resources/pdf
|
||||
# 复制资源文件(报告模板、PDF、组件等)
|
||||
COPY resources ./resources
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8080
|
||||
|
||||
50
config.yaml
50
config.yaml
@@ -129,6 +129,14 @@ sms:
|
||||
code_length: 6
|
||||
expire_time: 5m
|
||||
mock_enabled: false
|
||||
# 签名验证配置(用于防止接口被刷)
|
||||
signature_enabled: true # 是否启用签名验证
|
||||
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
|
||||
# 滑块验证码配置
|
||||
captcha_enabled: true # 是否启用滑块验证码
|
||||
captcha_secret: "" # 阿里云验证码密钥(加密模式时需要,可选)EKEY
|
||||
captcha_endpoint: "captcha.cn-shanghai.aliyuncs.com" # 阿里云验证码服务Endpoint
|
||||
scene_id: "wynt39to" # 阿里云验证码场景ID
|
||||
rate_limit:
|
||||
daily_limit: 10
|
||||
hourly_limit: 5
|
||||
@@ -192,6 +200,7 @@ daily_ratelimit:
|
||||
- "python" # Python脚本
|
||||
- "java" # Java脚本
|
||||
- "go-http-client" # Go HTTP客户端
|
||||
- "LangShen"
|
||||
|
||||
enable_referer: true # 是否检查Referer
|
||||
allowed_referers: # 允许的Referer
|
||||
@@ -233,7 +242,7 @@ development:
|
||||
|
||||
# 企业微信配置
|
||||
wechat_work:
|
||||
webhook_url: ""
|
||||
webhook_url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=649bf737-28ca-4f30-ad5f-cfb65b2af113"
|
||||
secret: ""
|
||||
|
||||
# ===========================================
|
||||
@@ -267,6 +276,8 @@ wallet:
|
||||
default_credit_limit: 50.00
|
||||
min_amount: "100.00" # 生产环境最低充值金额
|
||||
max_amount: "100000.00" # 单次最高充值金额
|
||||
recharge_bonus_enabled: true # 是否启用充值赠送,设为 false 时仅展示商务洽谈提示
|
||||
api_store_recharge_tip: "" # 关闭赠送时展示的提示文案,为空则使用默认文案
|
||||
# 支付宝充值赠送配置
|
||||
alipay_recharge_bonus:
|
||||
- recharge_amount: 1000.00 # 充值1000元
|
||||
@@ -593,3 +604,40 @@ shumai:
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
|
||||
# ===========================================
|
||||
# ✨ 数据宝配置走实时接口
|
||||
# ===========================================
|
||||
shujubao:
|
||||
url: "https://api.chinadatapay.com"
|
||||
app_secret: "iOk0ALBX0BSdTSTf"
|
||||
sign_method: "md5" # 签名方法:md5 或 hmac,默认 hmac
|
||||
timeout: 60s # 请求超时时间,默认 60 秒
|
||||
# 数据宝日志配置
|
||||
logging:
|
||||
enabled: true
|
||||
log_dir: "logs/external_services"
|
||||
service_name: "shujubao"
|
||||
use_daily: true
|
||||
enable_level_separation: true
|
||||
# 各级别配置
|
||||
level_configs:
|
||||
info:
|
||||
max_size: 100
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
error:
|
||||
max_size: 200
|
||||
max_backups: 10
|
||||
max_age: 90
|
||||
compress: true
|
||||
warn:
|
||||
max_size: 100
|
||||
max_backups: 5
|
||||
max_age: 30
|
||||
compress: true
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -108,6 +108,8 @@ wallet:
|
||||
default_credit_limit: 0.01
|
||||
min_amount: "0.01" # 生产环境最低充值金额
|
||||
max_amount: "100000.00" # 单次最高充值金额
|
||||
recharge_bonus_enabled: false # 开发环境可设为 true 测试赠送
|
||||
api_store_recharge_tip: "尊敬的客户,若您的充值金额较大或有批量调价需求,为获取专属商务优惠方案,请直接联系我司商务团队进行洽谈。感谢您的支持!"
|
||||
# 支付宝充值赠送配置
|
||||
alipay_recharge_bonus:
|
||||
- recharge_amount: 0.01 # 充值1000元
|
||||
@@ -174,3 +176,13 @@ daily_ratelimit:
|
||||
enable_user_agent: false # 开发环境禁用User-Agent检查
|
||||
enable_referer: false # 开发环境禁用Referer检查
|
||||
enable_proxy_check: false # 开发环境禁用代理检查
|
||||
|
||||
# ===========================================
|
||||
# 📱 短信服务配置
|
||||
# ===========================================
|
||||
sms:
|
||||
# 滑块验证码配置
|
||||
captcha_enabled: true # 是否启用滑块验证码
|
||||
captcha_secret: "" # 阿里云验证码密钥(可选)
|
||||
scene_id: "wynt39to" # 阿里云验证码场景ID
|
||||
|
||||
@@ -109,7 +109,9 @@ wallet:
|
||||
default_credit_limit: 50.00
|
||||
min_amount: "100.00" # 生产环境最低充值金额
|
||||
max_amount: "100000.00" # 单次最高充值金额
|
||||
# 支付宝充值赠送配置
|
||||
recharge_bonus_enabled: false # 暂不赠送,展示商务洽谈提示
|
||||
api_store_recharge_tip: "尊敬的客户,若您的充值金额较大或有批量调价需求,为获取专属商务优惠方案,请直接联系我司商务团队进行洽谈。感谢您的支持!"
|
||||
# 支付宝充值赠送配置(recharge_bonus_enabled 为 true 时生效)
|
||||
alipay_recharge_bonus:
|
||||
- recharge_amount: 1000.00 # 充值1000元
|
||||
bonus_amount: 50.00 # 赠送50元
|
||||
@@ -148,6 +150,7 @@ daily_ratelimit:
|
||||
- "curl" # 阻止curl请求
|
||||
- "wget" # 阻止wget请求
|
||||
- "python-requests" # 阻止Python requests
|
||||
- "LangShen" # 阻止LangShen请求
|
||||
|
||||
enable_referer: true # 启用Referer检查
|
||||
allowed_referers: # 允许的Referer
|
||||
@@ -157,3 +160,12 @@ daily_ratelimit:
|
||||
enable_geo_block: false # 生产环境暂时不启用地理位置阻止
|
||||
enable_proxy_check: true # 启用代理检查
|
||||
|
||||
# ===========================================
|
||||
# 📱 短信服务配置
|
||||
# ===========================================
|
||||
sms:
|
||||
# 滑块验证码配置
|
||||
captcha_enabled: true # 是否启用滑块验证码
|
||||
captcha_secret: "" # 阿里云验证码密钥(可选)
|
||||
scene_id: "wynt39to" # 阿里云验证码场景ID
|
||||
|
||||
|
||||
@@ -37,3 +37,12 @@ logger:
|
||||
# ===========================================
|
||||
jwt:
|
||||
secret: test-jwt-secret-key-for-testing-only
|
||||
|
||||
# ===========================================
|
||||
# 📱 短信服务配置
|
||||
# ===========================================
|
||||
sms:
|
||||
# 滑块验证码配置
|
||||
captcha_enabled: true # 是否启用滑块验证码
|
||||
captcha_secret: "" # 阿里云验证码密钥(可选)
|
||||
scene_id: "wynt39to" # 阿里云验证码场景ID
|
||||
|
||||
@@ -89,7 +89,10 @@ services:
|
||||
- "25000:8080"
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./resources/Pure_Component:/app/resources/Pure_Component
|
||||
# 挂载完整 resources 目录(包含 qiye.html、Pure_Component、pdf 等)
|
||||
- ./resources:/app/resources
|
||||
# 持久化PDF缓存目录,确保生成的PDF在容器重启后仍然存在
|
||||
- ./storage/pdfg-cache:/app/storage/pdfg-cache
|
||||
# user: "1001:1001" # 注释掉,使用root权限运行
|
||||
networks:
|
||||
- tyapi-network
|
||||
|
||||
347
docs/短信接口签名验证使用指南.md
Normal file
347
docs/短信接口签名验证使用指南.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# 短信接口签名验证使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
为了防止短信发送接口被恶意刷取,系统实现了基于HMAC-SHA256的签名验证机制。所有发送短信的请求必须包含有效的签名,否则请求将被拒绝。
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. **前端生成签名**:使用密钥对请求参数进行HMAC-SHA256签名
|
||||
2. **后端验证签名**:后端使用相同密钥重新计算签名并比对
|
||||
3. **时间戳验证**:防止重放攻击,时间戳必须在5分钟内有效
|
||||
4. **随机数验证**:每次请求必须包含唯一的随机字符串(nonce)
|
||||
5. **参数编码传输**(推荐):将所有参数(包括签名)编码成Base64字符串后传输,隐藏参数结构,增加安全性
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 后端配置
|
||||
|
||||
在 `config.yaml` 中配置签名相关参数:
|
||||
|
||||
```yaml
|
||||
sms:
|
||||
# ... 其他配置 ...
|
||||
# 签名验证配置
|
||||
signature_enabled: true # 是否启用签名验证
|
||||
signature_secret: "TyApi2024SMSSecretKey!@#$%^&*()_+QWERTYUIOP" # 签名密钥(请修改为复杂密钥)
|
||||
```
|
||||
|
||||
**重要提示**:
|
||||
- 生产环境必须修改 `signature_secret` 为复杂的随机字符串
|
||||
- 密钥长度建议至少32个字符
|
||||
- 密钥应包含大小写字母、数字和特殊字符
|
||||
|
||||
## 签名算法
|
||||
|
||||
### 1. 构建待签名字符串
|
||||
|
||||
将请求参数(排除`signature`字段)按key排序,拼接成以下格式:
|
||||
|
||||
```
|
||||
key1=value1&key2=value2×tamp=1234567890&nonce=random_string
|
||||
```
|
||||
|
||||
### 2. 计算HMAC-SHA256签名
|
||||
|
||||
使用配置的密钥对待签名字符串进行HMAC-SHA256计算,结果转换为hex编码。
|
||||
|
||||
### 3. 请求参数
|
||||
|
||||
系统支持两种请求方式:
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
发送请求时直接传递所有字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"phone": "13800138000",
|
||||
"scene": "register",
|
||||
"timestamp": 1704067200,
|
||||
"nonce": "a1b2c3d4e5f6g7h8",
|
||||
"signature": "abc123def456..."
|
||||
}
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐,更安全)
|
||||
|
||||
将所有参数(包括签名)编码成Base64字符串后传输,只传递一个`data`字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": "eyJwaG9uZSI6IjEzODAwMTM4MDAwIiwic2NlbmUiOiJyZWdpc3RlciIsInRpbWVzdGFtcCI6MTcwNDA2NzIwMCwibm9uY2UiOiJhMWIyYzNkNGE1ZjYiLCJzaWduYXR1cmUiOiJhYmMxMjNkZWY0NTYifQ=="
|
||||
}
|
||||
```
|
||||
|
||||
**编码传输的优势**:
|
||||
- 隐藏参数结构,增加破解难度
|
||||
- 参数不可见,防止参数被直接修改
|
||||
- 增加一层编码保护
|
||||
|
||||
## 前端实现
|
||||
|
||||
### Node.js 示例
|
||||
|
||||
参考文件:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js`
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
function generateSignature(params, secretKey, timestamp, nonce) {
|
||||
// 1. 构建待签名字符串
|
||||
const keys = Object.keys(params)
|
||||
.filter(k => k !== 'signature')
|
||||
.sort();
|
||||
|
||||
const parts = keys.map(k => `${k}=${params[k]}`);
|
||||
parts.push(`timestamp=${timestamp}`);
|
||||
parts.push(`nonce=${nonce}`);
|
||||
|
||||
const signString = parts.join('&');
|
||||
|
||||
// 2. 计算HMAC-SHA256签名
|
||||
const signature = crypto
|
||||
.createHmac('sha256', secretKey)
|
||||
.update(signString)
|
||||
.digest('hex');
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateRandomString(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 发送请求
|
||||
const requestBody = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐)
|
||||
|
||||
```javascript
|
||||
// 1. 生成签名(同上)
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateRandomString(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 2. 构建包含所有参数的JSON对象
|
||||
const allParams = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
|
||||
// 3. 编码为Base64
|
||||
const jsonString = JSON.stringify(allParams);
|
||||
const encodedData = Buffer.from(jsonString).toString('base64');
|
||||
|
||||
// 4. 发送请求(只传递data字段)
|
||||
const requestBody = {
|
||||
data: encodedData,
|
||||
};
|
||||
```
|
||||
|
||||
### 浏览器 JavaScript 示例
|
||||
|
||||
参考文件:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js`
|
||||
|
||||
#### 方式1:直接传递参数
|
||||
|
||||
```javascript
|
||||
async function generateSignature(params, secretKey, timestamp, nonce) {
|
||||
// 1. 构建待签名字符串
|
||||
const keys = Object.keys(params)
|
||||
.filter(k => k !== 'signature')
|
||||
.sort();
|
||||
|
||||
const parts = keys.map(k => `${k}=${params[k]}`);
|
||||
parts.push(`timestamp=${timestamp}`);
|
||||
parts.push(`nonce=${nonce}`);
|
||||
|
||||
const signString = parts.join('&');
|
||||
|
||||
// 2. 使用Web Crypto API计算HMAC-SHA256签名
|
||||
const encoder = new TextEncoder();
|
||||
const keyData = encoder.encode(secretKey);
|
||||
const messageData = encoder.encode(signString);
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyData,
|
||||
{ name: 'HMAC', hash: 'SHA-256' },
|
||||
false,
|
||||
['sign']
|
||||
);
|
||||
|
||||
const signature = await crypto.subtle.sign('HMAC', cryptoKey, messageData);
|
||||
const hashArray = Array.from(new Uint8Array(signature));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateNonce(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = await generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 发送请求
|
||||
const requestBody = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
```
|
||||
|
||||
#### 方式2:编码后传输(推荐)
|
||||
|
||||
```javascript
|
||||
// 1. 生成签名(同上)
|
||||
const params = { phone: '13800138000', scene: 'register' };
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const nonce = generateNonce(16);
|
||||
const secretKey = 'your_secret_key';
|
||||
const signature = await generateSignature(params, secretKey, timestamp, nonce);
|
||||
|
||||
// 2. 构建包含所有参数的JSON对象
|
||||
const allParams = {
|
||||
phone: '13800138000',
|
||||
scene: 'register',
|
||||
timestamp: timestamp,
|
||||
nonce: nonce,
|
||||
signature: signature,
|
||||
};
|
||||
|
||||
// 3. 编码为Base64(浏览器环境)
|
||||
const jsonString = JSON.stringify(allParams);
|
||||
const encodedData = btoa(unescape(encodeURIComponent(jsonString)));
|
||||
|
||||
// 4. 发送请求(只传递data字段)
|
||||
const requestBody = {
|
||||
data: encodedData,
|
||||
};
|
||||
```
|
||||
|
||||
## 密钥隐藏策略
|
||||
|
||||
由于前端代码可以被查看,完全隐藏密钥是不可能的。但可以通过以下方式增加破解难度:
|
||||
|
||||
### 1. 字符串拆分和拼接
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const part1 = 'TyApi2024';
|
||||
const part2 = 'SMSSecret';
|
||||
const part3 = 'Key!@#$%^';
|
||||
return part1 + part2 + part3;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 字符数组拼接
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const chars = ['T', 'y', 'A', 'p', 'i', ...];
|
||||
return chars.join('');
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Base64编码混淆
|
||||
|
||||
```javascript
|
||||
function getSecretKey() {
|
||||
const encoded = 'base64_encoded_string';
|
||||
return atob(encoded);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 代码混淆
|
||||
|
||||
使用构建工具(如webpack、rollup等)进行代码混淆和压缩,使密钥更难被发现。
|
||||
|
||||
### 5. 后端代理(推荐)
|
||||
|
||||
将签名逻辑放在后端代理接口中,前端只调用代理接口,不直接包含密钥。
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **定期更换密钥**:建议每3-6个月更换一次签名密钥
|
||||
2. **监控异常请求**:监控签名验证失败的请求,及时发现攻击行为
|
||||
3. **结合其他防护措施**:
|
||||
- IP限流
|
||||
- 设备指纹识别
|
||||
- 验证码(图形验证码)
|
||||
- 行为分析
|
||||
|
||||
4. **日志记录**:记录所有签名验证失败的请求,包括IP、User-Agent等信息
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 常见错误
|
||||
|
||||
1. **签名字段缺失**:返回 `"签名字段缺失"`
|
||||
2. **时间戳无效**:返回 `"时间戳无效"`
|
||||
3. **请求已过期**:返回 `"请求已过期,时间戳超出容差范围"`
|
||||
4. **签名验证失败**:返回 `"签名验证失败"`
|
||||
|
||||
### 时间戳容差
|
||||
|
||||
系统允许的时间戳容差为 **5分钟**(300秒)。如果请求时间戳与服务器时间差超过5分钟,请求将被拒绝。
|
||||
|
||||
## 测试
|
||||
|
||||
### 测试签名生成
|
||||
|
||||
```bash
|
||||
# 使用Node.js示例
|
||||
node tyapi-frontend/public/examples/nodejs/sms_signature_demo.js
|
||||
```
|
||||
|
||||
### 测试API调用
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/v1/users/send-code \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone": "13800138000",
|
||||
"scene": "register",
|
||||
"timestamp": 1704067200,
|
||||
"nonce": "a1b2c3d4e5f6g7h8",
|
||||
"signature": "计算得到的签名"
|
||||
}'
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **时间同步**:确保客户端和服务器时间同步,避免时间戳验证失败
|
||||
2. **随机数唯一性**:每次请求的nonce应该是唯一的,可以使用UUID或时间戳+随机数
|
||||
3. **密钥安全**:生产环境密钥不要提交到代码仓库,应使用环境变量或密钥管理服务
|
||||
4. **向后兼容**:如果需要在开发环境禁用签名验证,可以设置 `signature_enabled: false`
|
||||
|
||||
## 相关文件
|
||||
|
||||
- 后端签名工具:`internal/shared/crypto/signature.go`
|
||||
- 后端Handler:`internal/infrastructure/http/handlers/user_handler.go`
|
||||
- 配置结构:`internal/config/config.go`
|
||||
- Node.js示例:`tyapi-frontend/public/examples/nodejs/sms_signature_demo.js`
|
||||
- 浏览器示例:`tyapi-frontend/public/examples/javascript/sms_signature_demo.js`
|
||||
|
||||
9
go.mod
9
go.mod
@@ -3,6 +3,9 @@ module tyapi-server
|
||||
go 1.23.4
|
||||
|
||||
require (
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13
|
||||
github.com/alibabacloud-go/tea v1.3.13
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107
|
||||
github.com/gin-contrib/cors v1.7.6
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
@@ -46,11 +49,16 @@ require (
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.13.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
@@ -104,6 +112,7 @@ require (
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/tiendc/go-deepcopy v1.6.0 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/xuri/efp v0.0.1 // indirect
|
||||
|
||||
187
go.sum
187
go.sum
@@ -1,4 +1,6 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
@@ -14,8 +16,54 @@ github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWX
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4=
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6 h1:eIf+iGJxdU4U9ypaUfbtOWCsZSbTb8AUHvyPrxu6mAA=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-pop v0.0.6/go.mod h1:4EUIoxs/do24zMOGGqYVWgw0s9NtiylnJglOeEB5UJo=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4/go.mod h1:sCavSAvdzOjul4cEqeVtvlSaSScfNsTQ+46HwlTL1hc=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 h1:zE8vH9C7JiZLNJJQ5OwjU9mSi4T9ef9u3BURT6LCLC8=
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5/go.mod h1:tWnyE9AjF8J8qqLk645oUmVUnFybApTQWklQmi5tY6g=
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3 h1:0Aobw12m3x28aeDMPjwjXsfF8MuLvRjlQ4Hhoy5hFOY=
|
||||
github.com/alibabacloud-go/captcha-20230305 v1.1.3/go.mod h1:ydzBIN2OiM7eeQPpAFyBrv1H5TY1MtUP2rQig44C4UQ=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0 h1:vR8s7b1fWAQIjEjWnuF0JiKsCvclSRTfDzZHTYqfufY=
|
||||
github.com/alibabacloud-go/darabonba-array v0.1.0/go.mod h1:BLKxr0brnggqOJPqT09DFJ8g3fsDshapUD3C3aOEFaI=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2 h1:1uJGrbsGEVqWcWxrS9MyC2NG0Ax+GpOM5gtupki31XE=
|
||||
github.com/alibabacloud-go/darabonba-encode-util v0.0.2/go.mod h1:JiW9higWHYXm7F4PKuMgEUETNZasrDM6vqVr/Can7H8=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2 h1:qvPnGB4+dJbJIxOOfawxzF3hzMnIpjmafa0qOTp6udc=
|
||||
github.com/alibabacloud-go/darabonba-map v0.0.2/go.mod h1:28AJaX8FOE/ym8OUFWga+MtEzBunJwQGceGQlvaPGPc=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 h1:Q00FU3H94Ts0ZIHDmY+fYGgB7dV9D/YX6FGsgorQPgw=
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13/go.mod h1:lxFGfobinVsQ49ntjpgWghXmIF0/Sm4+wvBJ1h5RtaE=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7 h1:UzCnKvsjPFzApvODDNEYqBHMFt1w98wC7FOo0InLyxg=
|
||||
github.com/alibabacloud-go/darabonba-signature-util v0.0.7/go.mod h1:oUzCYV2fcCH797xKdL6BDH8ADIHlzrtKVjeRtunBNTQ=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2 h1:E714wms5ibdzCqGeYJ9JCFywE5nDyvIXIIQbZVFkkqo=
|
||||
github.com/alibabacloud-go/darabonba-string v1.0.2/go.mod h1:93cTfV3vuPhhEwGGpKKqhVW4jLe7tDpo3LUM0i0g6mA=
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68/go.mod h1:6pb/Qy8c+lqua8cFpEy7g39NRRqOWc3rOwAy8m5Y2BY=
|
||||
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
|
||||
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0 h1:r/4D3VSw888XGaeNpP994zDUaxdgTSHBbVfZlzf6b5Q=
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.0/go.mod h1:O5FuCALmCKs2Ff7JFJMudHs0I5EBgecXXxZRyswlEjE=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0 h1:0z75cIULkDrdEhkLWgi9tnLe+KhAFE/r5Pb3312/eAY=
|
||||
github.com/alibabacloud-go/openapi-util v0.1.0/go.mod h1:sQuElr4ywwFRlCCberQwKRFhRzIyG4QTP/P4y1CJ6Ws=
|
||||
github.com/alibabacloud-go/tea v1.1.0/go.mod h1:IkGyUSX4Ba1V+k4pCtJUc6jDpZLFph9QMy2VUPTwukg=
|
||||
github.com/alibabacloud-go/tea v1.1.7/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.8/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.11/go.mod h1:/tmnEaQMyb4Ky1/5D+SE1BAsa5zj/KeGOFfwYm3N/p4=
|
||||
github.com/alibabacloud-go/tea v1.1.17/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.1.20/go.mod h1:nXxjm6CIFkBhwW4FQkNrolwbfon8Svy6cujmKFUq98A=
|
||||
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
|
||||
github.com/alibabacloud-go/tea v1.3.13 h1:WhGy6LIXaMbBM6VBYcsDCz6K/TPsT1Ri2hPmmZffZ94=
|
||||
github.com/alibabacloud-go/tea v1.3.13/go.mod h1:A560v/JTQ1n5zklt2BEpurJzZTI8TUT+Psg2drWlxRg=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1 h1:iWQeRzRheqCMuiF3+XkfybB3kTgUXkXX+JMrqfLeB2I=
|
||||
github.com/alibabacloud-go/tea-utils v1.3.1/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQdSngxrpF8rKUDJjPE=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.5/go.mod h1:dL6vbUT35E4F4bFTHL845eUloqaerYBYPsdWR2/jhe4=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 h1:WDx5qW3Xa5ZgJ1c8NfqJkF6w+AU5wB8835UdhPr6Ax0=
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7/go.mod h1:qxn986l+q33J5VkialKMqT/TTs3E+U9MJpd001iWQ9I=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
|
||||
github.com/aliyun/credentials-go v1.3.1/go.mod h1:8jKYhQuDawt8x2+fusqa1Y6mPxemTsBEN04dgcAcYz0=
|
||||
github.com/aliyun/credentials-go v1.3.6/go.mod h1:1LxUuX7L5YrZUWzBrRyk0SwSdH4OmPrib8NVePL3fxM=
|
||||
github.com/aliyun/credentials-go v1.4.5 h1:O76WYKgdy1oQYYiJkERjlA2dxGuvLRrzuO2ScrtGWSk=
|
||||
github.com/aliyun/credentials-go v1.4.5/go.mod h1:Jm6d+xIgwJVLVWT561vy67ZRP4lPTQxMbEYRuT2Ti1U=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
@@ -29,11 +77,16 @@ github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCN
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
|
||||
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/dave/jennifer v1.6.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -41,6 +94,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
@@ -98,14 +154,32 @@ github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptG
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
|
||||
@@ -132,8 +206,10 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
|
||||
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
|
||||
@@ -167,6 +243,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
@@ -183,6 +261,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
@@ -220,6 +299,9 @@ github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw=
|
||||
github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
|
||||
github.com/smartwalle/nsign v1.0.9 h1:8poAgG7zBd8HkZy9RQDwasC6XZvJpDGQWSjzL2FZL6E=
|
||||
github.com/smartwalle/nsign v1.0.9/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
@@ -231,10 +313,12 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -258,6 +342,9 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tiendc/go-deepcopy v1.6.0 h1:0UtfV/imoCwlLxVsyfUd4hNHnB3drXsfle+wzSCA5Wo=
|
||||
github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I=
|
||||
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
|
||||
@@ -274,6 +361,8 @@ github.com/xuri/excelize/v2 v2.9.1 h1:VdSGk+rraGmgLHGFaGG9/9IWu1nj4ufjJ7uwMDtj8Q
|
||||
github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s=
|
||||
github.com/xuri/nfp v0.0.1 h1:MDamSGatIvp8uOmDP8FnmjuQpu90NzdJxo7242ANR9Q=
|
||||
github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
@@ -309,12 +398,24 @@ golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
|
||||
golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
@@ -323,26 +424,63 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -350,42 +488,88 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200509030707-2212a7e161a5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
|
||||
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
|
||||
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
|
||||
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
|
||||
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -394,6 +578,7 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
@@ -412,6 +597,8 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
|
||||
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w=
|
||||
modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
|
||||
@@ -260,6 +260,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
|
||||
// api
|
||||
&apiEntities.ApiUser{},
|
||||
&apiEntities.ApiCall{},
|
||||
&apiEntities.Report{},
|
||||
|
||||
// 任务域
|
||||
&taskEntities.AsyncTask{},
|
||||
|
||||
@@ -609,8 +609,6 @@ func (s *ApiApplicationServiceImpl) GetUserApiCalls(ctx context.Context, userID
|
||||
// 转换为响应DTO
|
||||
var items []dto.ApiCallRecordResponse
|
||||
for _, call := range calls {
|
||||
// 出于安全考虑,不再在数据库中存储或解密真实请求参数
|
||||
// 这里只保留数据库中的原始占位值(通常为空字符串)
|
||||
requestParamsStr := call.RequestParams
|
||||
|
||||
item := dto.ApiCallRecordResponse{
|
||||
|
||||
@@ -16,13 +16,13 @@ type ApiCallValidationResult struct {
|
||||
SecretKey string `json:"secret_key"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
ErrorMessage string `json:"error_message"`
|
||||
|
||||
|
||||
// 新增字段
|
||||
ContractCode string `json:"contract_code"`
|
||||
ApiCall *api_entities.ApiCall `json:"api_call"`
|
||||
RequestParams map[string]interface{} `json:"request_params"`
|
||||
Product *product_entities.Product `json:"product"`
|
||||
Subscription *product_entities.Subscription `json:"subscription"`
|
||||
ContractCode string `json:"contract_code"`
|
||||
ApiCall *api_entities.ApiCall `json:"api_call"`
|
||||
RequestParams map[string]interface{} `json:"request_params"`
|
||||
Product *product_entities.Product `json:"product"`
|
||||
Subscription *product_entities.Subscription `json:"subscription"`
|
||||
}
|
||||
|
||||
// GetUserID 获取用户ID
|
||||
@@ -99,6 +99,6 @@ func (r *ApiCallValidationResult) SetContractCode(code string) {
|
||||
// SetSubscription 设置订阅信息(包含实际扣费金额)
|
||||
func (r *ApiCallValidationResult) SetSubscription(subscription *product_entities.Subscription) {
|
||||
r.SubscriptionID = subscription.ID
|
||||
r.Amount = subscription.Price // 使用订阅价格作为扣费金额
|
||||
r.Amount = subscription.Price // 使用订阅价格作为扣费金额
|
||||
r.Subscription = subscription
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
"tyapi-server/internal/domains/certification/services"
|
||||
finance_service "tyapi-server/internal/domains/finance/services"
|
||||
user_entities "tyapi-server/internal/domains/user/entities"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/infrastructure/external/notification"
|
||||
"tyapi-server/internal/infrastructure/external/storage"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/esign"
|
||||
@@ -47,7 +49,8 @@ type CertificationApplicationServiceImpl struct {
|
||||
enterpriseInfoSubmitRecordRepo repositories.EnterpriseInfoSubmitRecordRepository
|
||||
txManager *database.TransactionManager
|
||||
|
||||
logger *zap.Logger
|
||||
wechatWorkService *notification.WeChatWorkService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCertificationApplicationService 创建认证应用服务
|
||||
@@ -67,7 +70,12 @@ func NewCertificationApplicationService(
|
||||
ocrService sharedOCR.OCRService,
|
||||
txManager *database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) CertificationApplicationService {
|
||||
var wechatSvc *notification.WeChatWorkService
|
||||
if cfg != nil && cfg.WechatWork.WebhookURL != "" {
|
||||
wechatSvc = notification.NewWeChatWorkService(cfg.WechatWork.WebhookURL, cfg.WechatWork.Secret, logger)
|
||||
}
|
||||
return &CertificationApplicationServiceImpl{
|
||||
aggregateService: aggregateService,
|
||||
userAggregateService: userAggregateService,
|
||||
@@ -83,6 +91,7 @@ func NewCertificationApplicationService(
|
||||
enterpriseInfoSubmitRecordService: enterpriseInfoSubmitRecordService,
|
||||
ocrService: ocrService,
|
||||
txManager: txManager,
|
||||
wechatWorkService: wechatSvc,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -1104,6 +1113,35 @@ func (s *CertificationApplicationServiceImpl) completeUserActivationWithoutContr
|
||||
return err
|
||||
}
|
||||
|
||||
// 企业认证成功企业微信通知(仅展示企业名称和联系手机)
|
||||
if s.wechatWorkService != nil {
|
||||
user, err := s.userAggregateService.GetUserWithEnterpriseInfo(ctx, cert.UserID)
|
||||
if err == nil {
|
||||
companyName := "未知企业"
|
||||
phone := ""
|
||||
if user.EnterpriseInfo != nil {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
if user.EnterpriseInfo.LegalPersonPhone != "" {
|
||||
phone = user.EnterpriseInfo.LegalPersonPhone
|
||||
}
|
||||
}
|
||||
if user.Phone != "" && phone == "" {
|
||||
phone = user.Phone
|
||||
}
|
||||
content := fmt.Sprintf(
|
||||
"### 【天远API】企业认证成功\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 完成时间:%s\n"+
|
||||
"\n该企业已完成认证,请相关同事同步更新内部系统。",
|
||||
companyName,
|
||||
phone,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkService.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -108,8 +108,10 @@ type AlipayRechargeOrderResponse struct {
|
||||
|
||||
// RechargeConfigResponse 充值配置响应
|
||||
type RechargeConfigResponse struct {
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
MinAmount string `json:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `json:"max_amount"` // 最高充值金额
|
||||
RechargeBonusEnabled bool `json:"recharge_bonus_enabled"` // 是否启用充值赠送
|
||||
ApiStoreRechargeTip string `json:"api_store_recharge_tip"` // API 商店充值提示(大额/批量联系商务)
|
||||
AlipayRechargeBonus []AlipayRechargeBonusRuleResponse `json:"alipay_recharge_bonus"`
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
finance_services "tyapi-server/internal/domains/finance/services"
|
||||
product_repositories "tyapi-server/internal/domains/product/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/infrastructure/external/notification"
|
||||
"tyapi-server/internal/shared/component_report"
|
||||
"tyapi-server/internal/shared/database"
|
||||
"tyapi-server/internal/shared/export"
|
||||
@@ -43,6 +44,7 @@ type FinanceApplicationServiceImpl struct {
|
||||
exportManager *export.ExportManager
|
||||
logger *zap.Logger
|
||||
config *config.Config
|
||||
wechatWorkService *notification.WeChatWorkService
|
||||
}
|
||||
|
||||
// NewFinanceApplicationService 创建财务应用服务
|
||||
@@ -63,6 +65,11 @@ func NewFinanceApplicationService(
|
||||
config *config.Config,
|
||||
exportManager *export.ExportManager,
|
||||
) FinanceApplicationService {
|
||||
var wechatSvc *notification.WeChatWorkService
|
||||
if config != nil && config.WechatWork.WebhookURL != "" {
|
||||
wechatSvc = notification.NewWeChatWorkService(config.WechatWork.WebhookURL, config.WechatWork.Secret, logger)
|
||||
}
|
||||
|
||||
return &FinanceApplicationServiceImpl{
|
||||
aliPayClient: aliPayClient,
|
||||
wechatPayService: wechatPayService,
|
||||
@@ -79,9 +86,46 @@ func NewFinanceApplicationService(
|
||||
exportManager: exportManager,
|
||||
logger: logger,
|
||||
config: config,
|
||||
wechatWorkService: wechatSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// getUserContactInfo 获取企业名称和联系手机号(尽量用企业信息里的手机号,退化到用户登录手机号)
|
||||
func (s *FinanceApplicationServiceImpl) getUserContactInfo(ctx context.Context, userID string) (companyName, phone string) {
|
||||
companyName = "未知企业"
|
||||
phone = ""
|
||||
|
||||
if userID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByIDWithEnterpriseInfo(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Warn("获取用户企业信息失败,使用默认企业名称",
|
||||
zap.String("user_id", userID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// 登录手机号
|
||||
if user.Phone != "" {
|
||||
phone = user.Phone
|
||||
}
|
||||
|
||||
// 企业名称和企业手机号
|
||||
if user.EnterpriseInfo != nil {
|
||||
if user.EnterpriseInfo.CompanyName != "" {
|
||||
companyName = user.EnterpriseInfo.CompanyName
|
||||
}
|
||||
if user.EnterpriseInfo.LegalPersonPhone != "" {
|
||||
phone = user.EnterpriseInfo.LegalPersonPhone
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *FinanceApplicationServiceImpl) CreateWallet(ctx context.Context, cmd *commands.CreateWalletCommand) (*responses.WalletResponse, error) {
|
||||
// 调用钱包聚合服务创建钱包
|
||||
wallet, err := s.walletService.CreateWallet(ctx, cmd.UserID)
|
||||
@@ -936,6 +980,33 @@ func (s *FinanceApplicationServiceImpl) processAlipayPaymentSuccess(ctx context.
|
||||
zap.String("amount", amount.String()),
|
||||
)
|
||||
|
||||
// 充值成功企业微信通知(仅充值订单,且忽略发送错误)
|
||||
if s.wechatWorkService != nil {
|
||||
// 再次获取充值记录,拿到用户ID
|
||||
rechargeRecord, err := s.rechargeRecordRepo.GetByID(ctx, alipayOrder.RechargeID)
|
||||
if err == nil {
|
||||
companyName, phone := s.getUserContactInfo(ctx, rechargeRecord.UserID)
|
||||
content := fmt.Sprintf(
|
||||
"### 【天远API】用户充值成功通知\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 充值渠道:支付宝\n"+
|
||||
"> 充值金额:%s 元\n"+
|
||||
"> 时间:%s\n",
|
||||
companyName,
|
||||
phone,
|
||||
amount.String(),
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkService.SendMarkdownMessage(ctx, content)
|
||||
} else {
|
||||
s.logger.Warn("获取充值记录失败,跳过企业微信充值通知",
|
||||
zap.String("out_trade_no", outTradeNo),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1266,17 +1337,26 @@ func (s *FinanceApplicationServiceImpl) GetAdminRechargeRecords(ctx context.Cont
|
||||
|
||||
// GetRechargeConfig 获取充值配置
|
||||
func (s *FinanceApplicationServiceImpl) GetRechargeConfig(ctx context.Context) (*responses.RechargeConfigResponse, error) {
|
||||
bonus := make([]responses.AlipayRechargeBonusRuleResponse, 0, len(s.config.Wallet.AliPayRechargeBonus))
|
||||
for _, rule := range s.config.Wallet.AliPayRechargeBonus {
|
||||
bonus = append(bonus, responses.AlipayRechargeBonusRuleResponse{
|
||||
RechargeAmount: rule.RechargeAmount,
|
||||
BonusAmount: rule.BonusAmount,
|
||||
})
|
||||
bonus := make([]responses.AlipayRechargeBonusRuleResponse, 0)
|
||||
if s.config.Wallet.RechargeBonusEnabled && len(s.config.Wallet.AliPayRechargeBonus) > 0 {
|
||||
bonus = make([]responses.AlipayRechargeBonusRuleResponse, 0, len(s.config.Wallet.AliPayRechargeBonus))
|
||||
for _, rule := range s.config.Wallet.AliPayRechargeBonus {
|
||||
bonus = append(bonus, responses.AlipayRechargeBonusRuleResponse{
|
||||
RechargeAmount: rule.RechargeAmount,
|
||||
BonusAmount: rule.BonusAmount,
|
||||
})
|
||||
}
|
||||
}
|
||||
tip := s.config.Wallet.ApiStoreRechargeTip
|
||||
if tip == "" && !s.config.Wallet.RechargeBonusEnabled {
|
||||
tip = "尊敬的客户,若您的充值金额较大或有批量调价需求,为获取专属商务优惠方案,请直接联系我司商务团队进行洽谈。感谢您的支持!"
|
||||
}
|
||||
return &responses.RechargeConfigResponse{
|
||||
MinAmount: s.config.Wallet.MinAmount,
|
||||
MaxAmount: s.config.Wallet.MaxAmount,
|
||||
AlipayRechargeBonus: bonus,
|
||||
MinAmount: s.config.Wallet.MinAmount,
|
||||
MaxAmount: s.config.Wallet.MaxAmount,
|
||||
RechargeBonusEnabled: s.config.Wallet.RechargeBonusEnabled,
|
||||
ApiStoreRechargeTip: tip,
|
||||
AlipayRechargeBonus: bonus,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1580,9 +1660,9 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
return nil
|
||||
}
|
||||
|
||||
// 计算充值赠送金额(复用支付宝的赠送逻辑)
|
||||
// 计算充值赠送金额(复用支付宝的赠送逻辑,受 recharge_bonus_enabled 开关控制)
|
||||
bonusAmount := decimal.Zero
|
||||
if len(s.config.Wallet.AliPayRechargeBonus) > 0 {
|
||||
if s.config.Wallet.RechargeBonusEnabled && len(s.config.Wallet.AliPayRechargeBonus) > 0 {
|
||||
for i := len(s.config.Wallet.AliPayRechargeBonus) - 1; i >= 0; i-- {
|
||||
rule := s.config.Wallet.AliPayRechargeBonus[i]
|
||||
if amount.GreaterThanOrEqual(decimal.NewFromFloat(rule.RechargeAmount)) {
|
||||
@@ -1681,6 +1761,24 @@ func (s *FinanceApplicationServiceImpl) processWechatPaymentSuccess(ctx context.
|
||||
zap.String("user_id", rechargeRecord.UserID),
|
||||
)
|
||||
|
||||
// 微信充值成功企业微信通知(忽略发送错误)
|
||||
if s.wechatWorkService != nil {
|
||||
companyName, phone := s.getUserContactInfo(ctx, rechargeRecord.UserID)
|
||||
content := fmt.Sprintf(
|
||||
"### 【天远API】用户充值成功通知\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 充值渠道:微信\n"+
|
||||
"> 充值金额:%s 元\n"+
|
||||
"> 时间:%s\n",
|
||||
companyName,
|
||||
phone,
|
||||
amount.String(),
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkService.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/application/finance/dto"
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/finance/entities"
|
||||
finance_repo "tyapi-server/internal/domains/finance/repositories"
|
||||
"tyapi-server/internal/domains/finance/services"
|
||||
"tyapi-server/internal/domains/finance/value_objects"
|
||||
user_repo "tyapi-server/internal/domains/user/repositories"
|
||||
user_service "tyapi-server/internal/domains/user/services"
|
||||
"tyapi-server/internal/infrastructure/external/notification"
|
||||
"tyapi-server/internal/infrastructure/external/storage"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
@@ -59,8 +61,9 @@ type InvoiceApplicationServiceImpl struct {
|
||||
userAggregateService user_service.UserAggregateService
|
||||
|
||||
// 外部服务依赖
|
||||
storageService *storage.QiNiuStorageService
|
||||
logger *zap.Logger
|
||||
storageService *storage.QiNiuStorageService
|
||||
logger *zap.Logger
|
||||
wechatWorkServer *notification.WeChatWorkService
|
||||
}
|
||||
|
||||
// NewInvoiceApplicationService 创建发票应用服务
|
||||
@@ -76,7 +79,13 @@ func NewInvoiceApplicationService(
|
||||
userInvoiceInfoService services.UserInvoiceInfoService,
|
||||
storageService *storage.QiNiuStorageService,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) InvoiceApplicationService {
|
||||
var wechatSvc *notification.WeChatWorkService
|
||||
if cfg != nil && cfg.WechatWork.WebhookURL != "" {
|
||||
wechatSvc = notification.NewWeChatWorkService(cfg.WechatWork.WebhookURL, cfg.WechatWork.Secret, logger)
|
||||
}
|
||||
|
||||
return &InvoiceApplicationServiceImpl{
|
||||
invoiceRepo: invoiceRepo,
|
||||
userInvoiceInfoRepo: userInvoiceInfoRepo,
|
||||
@@ -89,6 +98,7 @@ func NewInvoiceApplicationService(
|
||||
userInvoiceInfoService: userInvoiceInfoService,
|
||||
storageService: storageService,
|
||||
logger: logger,
|
||||
wechatWorkServer: wechatSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +185,7 @@ func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID
|
||||
}
|
||||
|
||||
// 10. 构建响应DTO
|
||||
return &dto.InvoiceApplicationResponse{
|
||||
resp := &dto.InvoiceApplicationResponse{
|
||||
ID: application.ID,
|
||||
UserID: application.UserID,
|
||||
InvoiceType: application.InvoiceType,
|
||||
@@ -183,7 +193,33 @@ func (s *InvoiceApplicationServiceImpl) ApplyInvoice(ctx context.Context, userID
|
||||
Status: application.Status,
|
||||
InvoiceInfo: invoiceInfo,
|
||||
CreatedAt: application.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 11. 企业微信通知(忽略发送错误),只使用企业名称和联系电话
|
||||
if s.wechatWorkServer != nil {
|
||||
companyName := userWithEnterprise.EnterpriseInfo.CompanyName
|
||||
phone := user.Phone
|
||||
if userWithEnterprise.EnterpriseInfo.LegalPersonPhone != "" {
|
||||
phone = userWithEnterprise.EnterpriseInfo.LegalPersonPhone
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(
|
||||
"### 【天远API】用户申请开发票\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 申请开票金额:%s 元\n"+
|
||||
"> 发票类型:%s\n"+
|
||||
"> 申请时间:%s\n",
|
||||
companyName,
|
||||
phone,
|
||||
application.Amount.String(),
|
||||
string(application.InvoiceType),
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkServer.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetUserInvoiceInfo 获取用户发票信息
|
||||
|
||||
@@ -43,10 +43,20 @@ type ResetPasswordCommand struct {
|
||||
}
|
||||
|
||||
// SendCodeCommand 发送验证码命令
|
||||
// @Description 发送短信验证码请求参数
|
||||
// @Description 发送短信验证码请求参数。只接收编码后的data字段(使用自定义编码方案,非Base64)
|
||||
type SendCodeCommand struct {
|
||||
Phone string `json:"phone" binding:"required,phone" example:"13800138000"`
|
||||
Scene string `json:"scene" binding:"required,oneof=register login change_password reset_password bind unbind certification" example:"register"`
|
||||
// 编码后的数据(使用自定义编码方案的JSON字符串,包含所有参数:phone, scene, timestamp, nonce, signature)
|
||||
Data string `json:"data" binding:"required" example:"K8mN9vP2sL7kH3oB6yC1zA5uF0qE9tW..."` // 自定义编码后的数据
|
||||
|
||||
// 阿里云滑块验证码参数(直接接收,不参与编码)
|
||||
CaptchaVerifyParam string `json:"captchaVerifyParam,omitempty" example:"..."` // 滑块验证码验证参数
|
||||
|
||||
// 以下字段从data解码后填充,不直接接收
|
||||
Phone string `json:"-"` // 从data解码后获取
|
||||
Scene string `json:"-"` // 从data解码后获取
|
||||
Timestamp int64 `json:"-"` // 从data解码后获取
|
||||
Nonce string `json:"-"` // 从data解码后获取
|
||||
Signature string `json:"-"` // 从data解码后获取
|
||||
}
|
||||
|
||||
// UpdateProfileCommand 更新用户信息命令
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
|
||||
func (s *UserApplicationServiceImpl) SendCode(ctx context.Context, cmd *commands.SendCodeCommand, clientIP, userAgent string) error {
|
||||
// 1. 检查频率限制
|
||||
if err := s.smsCodeService.CheckRateLimit(ctx, cmd.Phone, entities.SMSScene(cmd.Scene)); err != nil {
|
||||
if err := s.smsCodeService.CheckRateLimit(ctx, cmd.Phone, entities.SMSScene(cmd.Scene), clientIP, userAgent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := s.smsCodeService.SendCode(ctx, cmd.Phone, entities.SMSScene(cmd.Scene), clientIP, userAgent)
|
||||
err := s.smsCodeService.SendCode(ctx, cmd.Phone, entities.SMSScene(cmd.Scene), clientIP, userAgent, cmd.CaptchaVerifyParam)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -212,12 +212,6 @@ func (s *UserApplicationServiceImpl) LoginWithSMS(ctx context.Context, cmd *comm
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SendSMS 发送短信验证码
|
||||
// 业务流程:1. 发送短信验证码
|
||||
func (s *UserApplicationServiceImpl) SendSMS(ctx context.Context, cmd *commands.SendCodeCommand) error {
|
||||
return s.smsCodeService.SendCode(ctx, cmd.Phone, entities.SMSScene(cmd.Scene), "", "")
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
// 业务流程:1. 修改用户密码
|
||||
func (s *UserApplicationServiceImpl) ChangePassword(ctx context.Context, cmd *commands.ChangePasswordCommand) error {
|
||||
@@ -345,19 +339,19 @@ func (s *UserApplicationServiceImpl) ListUsers(ctx context.Context, query *queri
|
||||
EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress,
|
||||
CreatedAt: user.EnterpriseInfo.CreatedAt,
|
||||
}
|
||||
|
||||
|
||||
// 获取企业合同信息
|
||||
contracts, err := s.contractService.FindByUserID(ctx, user.ID)
|
||||
if err == nil && len(contracts) > 0 {
|
||||
contractItems := make([]*responses.ContractInfoItem, 0, len(contracts))
|
||||
for _, contract := range contracts {
|
||||
contractItems = append(contractItems, &responses.ContractInfoItem{
|
||||
ID: contract.ID,
|
||||
ContractName: contract.ContractName,
|
||||
ContractType: string(contract.ContractType),
|
||||
ID: contract.ID,
|
||||
ContractName: contract.ContractName,
|
||||
ContractType: string(contract.ContractType),
|
||||
ContractTypeName: contract.GetContractTypeName(),
|
||||
ContractFileURL: contract.ContractFileURL,
|
||||
CreatedAt: contract.CreatedAt,
|
||||
ContractFileURL: contract.ContractFileURL,
|
||||
CreatedAt: contract.CreatedAt,
|
||||
})
|
||||
}
|
||||
item.EnterpriseInfo.Contracts = contractItems
|
||||
@@ -417,19 +411,19 @@ func (s *UserApplicationServiceImpl) GetUserDetail(ctx context.Context, userID s
|
||||
EnterpriseAddress: user.EnterpriseInfo.EnterpriseAddress,
|
||||
CreatedAt: user.EnterpriseInfo.CreatedAt,
|
||||
}
|
||||
|
||||
|
||||
// 获取企业合同信息
|
||||
contracts, err := s.contractService.FindByUserID(ctx, user.ID)
|
||||
if err == nil && len(contracts) > 0 {
|
||||
contractItems := make([]*responses.ContractInfoItem, 0, len(contracts))
|
||||
for _, contract := range contracts {
|
||||
contractItems = append(contractItems, &responses.ContractInfoItem{
|
||||
ID: contract.ID,
|
||||
ContractName: contract.ContractName,
|
||||
ContractType: string(contract.ContractType),
|
||||
ID: contract.ID,
|
||||
ContractName: contract.ContractName,
|
||||
ContractType: string(contract.ContractType),
|
||||
ContractTypeName: contract.GetContractTypeName(),
|
||||
ContractFileURL: contract.ContractFileURL,
|
||||
CreatedAt: contract.CreatedAt,
|
||||
ContractFileURL: contract.ContractFileURL,
|
||||
CreatedAt: contract.CreatedAt,
|
||||
})
|
||||
}
|
||||
item.EnterpriseInfo.Contracts = contractItems
|
||||
|
||||
@@ -40,6 +40,7 @@ type Config struct {
|
||||
Xingwei XingweiConfig `mapstructure:"xingwei"`
|
||||
Jiguang JiguangConfig `mapstructure:"jiguang"`
|
||||
Shumai ShumaiConfig `mapstructure:"shumai"`
|
||||
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
||||
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||
}
|
||||
|
||||
@@ -212,6 +213,14 @@ type SMSConfig struct {
|
||||
ExpireTime time.Duration `mapstructure:"expire_time"`
|
||||
RateLimit SMSRateLimit `mapstructure:"rate_limit"`
|
||||
MockEnabled bool `mapstructure:"mock_enabled"` // 是否启用模拟短信服务
|
||||
// 签名验证配置
|
||||
SignatureEnabled bool `mapstructure:"signature_enabled"` // 是否启用签名验证
|
||||
SignatureSecret string `mapstructure:"signature_secret"` // 签名密钥
|
||||
// 滑块验证码配置
|
||||
CaptchaEnabled bool `mapstructure:"captcha_enabled"` // 是否启用滑块验证码
|
||||
CaptchaSecret string `mapstructure:"captcha_secret"` // 阿里云验证码密钥
|
||||
CaptchaEndpoint string `mapstructure:"captcha_endpoint"` // 阿里云验证码服务Endpoint
|
||||
SceneID string `mapstructure:"scene_id"` // 阿里云验证码场景ID
|
||||
}
|
||||
|
||||
// SMSRateLimit 短信限流配置
|
||||
@@ -322,11 +331,13 @@ type SignConfig struct {
|
||||
|
||||
// WalletConfig 钱包配置
|
||||
type WalletConfig struct {
|
||||
DefaultCreditLimit float64 `mapstructure:"default_credit_limit"`
|
||||
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
||||
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
|
||||
BalanceAlert BalanceAlertConfig `mapstructure:"balance_alert"`
|
||||
DefaultCreditLimit float64 `mapstructure:"default_credit_limit"`
|
||||
MinAmount string `mapstructure:"min_amount"` // 最低充值金额
|
||||
MaxAmount string `mapstructure:"max_amount"` // 最高充值金额
|
||||
RechargeBonusEnabled bool `mapstructure:"recharge_bonus_enabled"` // 是否启用充值赠送,关闭后仅展示商务洽谈提示
|
||||
ApiStoreRechargeTip string `mapstructure:"api_store_recharge_tip"` // API 商店充值提示文案(大额/批量需求联系商务)
|
||||
AliPayRechargeBonus []AliPayRechargeBonusRule `mapstructure:"alipay_recharge_bonus"`
|
||||
BalanceAlert BalanceAlertConfig `mapstructure:"balance_alert"`
|
||||
}
|
||||
|
||||
// BalanceAlertConfig 余额预警配置
|
||||
@@ -582,6 +593,33 @@ type ShumaiLevelFileConfig struct {
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// ShujubaoConfig 数据宝配置
|
||||
type ShujubaoConfig struct {
|
||||
URL string `mapstructure:"url"`
|
||||
AppSecret string `mapstructure:"app_secret"`
|
||||
SignMethod string `mapstructure:"sign_method"` // md5 或 hmac,默认 hmac
|
||||
Timeout time.Duration `mapstructure:"timeout"`
|
||||
|
||||
Logging ShujubaoLoggingConfig `mapstructure:"logging"`
|
||||
}
|
||||
|
||||
// ShujubaoLoggingConfig 数据宝日志配置
|
||||
type ShujubaoLoggingConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
LogDir string `mapstructure:"log_dir"`
|
||||
UseDaily bool `mapstructure:"use_daily"`
|
||||
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||
LevelConfigs map[string]ShujubaoLevelFileConfig `mapstructure:"level_configs"`
|
||||
}
|
||||
|
||||
// ShujubaoLevelFileConfig 数据宝级别文件配置
|
||||
type ShujubaoLevelFileConfig struct {
|
||||
MaxSize int `mapstructure:"max_size"`
|
||||
MaxBackups int `mapstructure:"max_backups"`
|
||||
MaxAge int `mapstructure:"max_age"`
|
||||
Compress bool `mapstructure:"compress"`
|
||||
}
|
||||
|
||||
// PDFGenConfig PDF生成服务配置
|
||||
type PDFGenConfig struct {
|
||||
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
|
||||
|
||||
@@ -37,10 +37,12 @@ import (
|
||||
product_repo "tyapi-server/internal/infrastructure/database/repositories/product"
|
||||
infra_events "tyapi-server/internal/infrastructure/events"
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
"tyapi-server/internal/infrastructure/external/captcha"
|
||||
"tyapi-server/internal/infrastructure/external/email"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/ocr"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/sms"
|
||||
"tyapi-server/internal/infrastructure/external/storage"
|
||||
@@ -237,6 +239,19 @@ func NewContainer() *Container {
|
||||
},
|
||||
// 短信服务
|
||||
sms.NewAliSMSService,
|
||||
// 验证码服务
|
||||
fx.Annotate(
|
||||
func(cfg *config.Config) *captcha.CaptchaService {
|
||||
return captcha.NewCaptchaService(captcha.CaptchaConfig{
|
||||
AccessKeyID: cfg.SMS.AccessKeyID,
|
||||
AccessKeySecret: cfg.SMS.AccessKeySecret,
|
||||
EndpointURL: cfg.SMS.CaptchaEndpoint,
|
||||
SceneID: cfg.SMS.SceneID,
|
||||
EncryptKey: cfg.SMS.CaptchaSecret, // 加密模式 ekey(Base64 编码的 32 字节)
|
||||
})
|
||||
},
|
||||
fx.ResultTags(`name:"captchaService"`),
|
||||
),
|
||||
// 邮件服务
|
||||
fx.Annotate(
|
||||
func(cfg *config.Config, logger *zap.Logger) *email.QQEmailService {
|
||||
@@ -375,6 +390,10 @@ func NewContainer() *Container {
|
||||
func(cfg *config.Config) (*shumai.ShumaiService, error) {
|
||||
return shumai.NewShumaiServiceWithConfig(cfg)
|
||||
},
|
||||
// ShujubaoService - 数据宝服务
|
||||
func(cfg *config.Config) (*shujubao.ShujubaoService, error) {
|
||||
return shujubao.NewShujubaoServiceWithConfig(cfg)
|
||||
},
|
||||
func(cfg *config.Config) *yushan.YushanService {
|
||||
return yushan.NewYushanService(
|
||||
cfg.Yushan.URL,
|
||||
@@ -641,6 +660,10 @@ func NewContainer() *Container {
|
||||
api_repo.NewGormApiCallRepository,
|
||||
fx.As(new(domain_api_repo.ApiCallRepository)),
|
||||
),
|
||||
fx.Annotate(
|
||||
api_repo.NewGormReportRepository,
|
||||
fx.As(new(domain_api_repo.ReportRepository)),
|
||||
),
|
||||
),
|
||||
|
||||
// 统计域仓储层
|
||||
@@ -665,7 +688,10 @@ func NewContainer() *Container {
|
||||
user_service.NewUserAggregateService,
|
||||
),
|
||||
user_service.NewUserAuthService,
|
||||
user_service.NewSMSCodeService,
|
||||
fx.Annotate(
|
||||
user_service.NewSMSCodeService,
|
||||
fx.ParamTags(``, ``, ``, `name:"captchaService"`),
|
||||
),
|
||||
user_service.NewContractAggregateService,
|
||||
product_service.NewProductManagementService,
|
||||
product_service.NewProductSubscriptionService,
|
||||
@@ -747,7 +773,8 @@ func NewContainer() *Container {
|
||||
api_services.NewApiUserAggregateService,
|
||||
),
|
||||
api_services.NewApiCallAggregateService,
|
||||
api_services.NewApiRequestService,
|
||||
// 使用带仓储注入的构造函数,支持企业报告记录持久化
|
||||
api_services.NewApiRequestServiceWithRepos,
|
||||
api_services.NewFormConfigService,
|
||||
),
|
||||
|
||||
@@ -880,6 +907,7 @@ func NewContainer() *Container {
|
||||
ocrService sharedOCR.OCRService,
|
||||
txManager *shared_database.TransactionManager,
|
||||
logger *zap.Logger,
|
||||
cfg *config.Config,
|
||||
) certification.CertificationApplicationService {
|
||||
return certification.NewCertificationApplicationService(
|
||||
aggregateService,
|
||||
@@ -897,6 +925,7 @@ func NewContainer() *Container {
|
||||
ocrService,
|
||||
txManager,
|
||||
logger,
|
||||
cfg,
|
||||
)
|
||||
},
|
||||
fx.As(new(certification.CertificationApplicationService)),
|
||||
@@ -1260,6 +1289,8 @@ func NewContainer() *Container {
|
||||
) *handlers.ComponentReportOrderHandler {
|
||||
return handlers.NewComponentReportOrderHandler(componentReportOrderService, purchaseOrderRepo, config, logger)
|
||||
},
|
||||
// 企业全景报告页面处理器
|
||||
handlers.NewQYGLReportHandler,
|
||||
// UI组件HTTP处理器
|
||||
func(
|
||||
uiComponentAppService product.UIComponentApplicationService,
|
||||
@@ -1269,12 +1300,19 @@ func NewContainer() *Container {
|
||||
) *handlers.UIComponentHandler {
|
||||
return handlers.NewUIComponentHandler(uiComponentAppService, responseBuilder, validator, logger)
|
||||
},
|
||||
// 验证码HTTP处理器
|
||||
fx.Annotate(
|
||||
handlers.NewCaptchaHandler,
|
||||
fx.ParamTags(`name:"captchaService"`, ``, ``, ``),
|
||||
),
|
||||
),
|
||||
|
||||
// 路由注册
|
||||
fx.Provide(
|
||||
// 用户路由
|
||||
routes.NewUserRoutes,
|
||||
// 验证码路由
|
||||
routes.NewCaptchaRoutes,
|
||||
// 认证路由
|
||||
routes.NewCertificationRoutes,
|
||||
// 财务路由
|
||||
@@ -1299,6 +1337,8 @@ func NewContainer() *Container {
|
||||
routes.NewStatisticsRoutes,
|
||||
// PDFG路由
|
||||
routes.NewPDFGRoutes,
|
||||
// 企业报告页面路由
|
||||
routes.NewQYGLReportRoutes,
|
||||
),
|
||||
|
||||
// 应用生命周期
|
||||
@@ -1401,6 +1441,7 @@ func RegisterMiddlewares(
|
||||
func RegisterRoutes(
|
||||
router *sharedhttp.GinRouter,
|
||||
userRoutes *routes.UserRoutes,
|
||||
captchaRoutes *routes.CaptchaRoutes,
|
||||
certificationRoutes *routes.CertificationRoutes,
|
||||
financeRoutes *routes.FinanceRoutes,
|
||||
productRoutes *routes.ProductRoutes,
|
||||
@@ -1413,6 +1454,7 @@ func RegisterRoutes(
|
||||
apiRoutes *routes.ApiRoutes,
|
||||
statisticsRoutes *routes.StatisticsRoutes,
|
||||
pdfgRoutes *routes.PDFGRoutes,
|
||||
qyglReportRoutes *routes.QYGLReportRoutes,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
adminAuth *middleware.AdminAuthMiddleware,
|
||||
cfg *config.Config,
|
||||
@@ -1425,6 +1467,7 @@ func RegisterRoutes(
|
||||
|
||||
// 所有域名路由路由
|
||||
userRoutes.Register(router)
|
||||
captchaRoutes.Register(router)
|
||||
certificationRoutes.Register(router)
|
||||
financeRoutes.Register(router)
|
||||
productRoutes.Register(router)
|
||||
@@ -1437,6 +1480,7 @@ func RegisterRoutes(
|
||||
announcementRoutes.Register(router)
|
||||
statisticsRoutes.Register(router)
|
||||
pdfgRoutes.Register(router)
|
||||
qyglReportRoutes.Register(router)
|
||||
|
||||
// 打印注册的路由信息
|
||||
router.PrintRoutes()
|
||||
|
||||
@@ -100,11 +100,76 @@ type JRZQDCBEReq struct {
|
||||
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
type JRZQACABReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
BankCard string `json:"bank_card" validate:"required,validBankCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
// shujubao
|
||||
type QYGL2ACDReq struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
LegalPerson string `json:"legal_person" validate:"required,min=1,validName"`
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
}
|
||||
type QYGLUY3SReq struct {
|
||||
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
|
||||
EntRegno string `json:"ent_reg_no" validate:"omitempty"`
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
}
|
||||
type QYGLJ1U9Req struct {
|
||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"required,validUSCI"`
|
||||
}
|
||||
type JRZQOCRYReq struct {
|
||||
PhotoData string `json:"photo_data" validate:"required,validBase64Image"`
|
||||
}
|
||||
type JRZQOCREReq struct {
|
||||
PhotoData string `json:"photo_data" validate:"omitempty,validBase64Image"`
|
||||
ImageUrl string `json:"image_url" validate:"omitempty,url"`
|
||||
}
|
||||
|
||||
type YYSYK9R4Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
}
|
||||
|
||||
type YYSY35TAReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type QCXG9F5CReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXG3M7ZReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
PlateColor string `json:"plate_color" validate:"omitempty"`
|
||||
}
|
||||
type QCXG3B8ZReq struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXGM7R9Req struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXGP1W3Req struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXG5U0ZReq struct {
|
||||
VinCode string `json:"vin_code" validate:"required"`
|
||||
}
|
||||
type QCXGU2K4Req struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
type QCXGY7F2Req struct {
|
||||
VinCode string `json:"vin_code" validate:"required"`
|
||||
VehicleName string `json:"vehicle_name" validate:"omitempty"`
|
||||
VehicleLocation string `json:"vehicle_location" validate:"required"`
|
||||
FirstRegistrationdate string `json:"first_registrationdate" validate:"required"`
|
||||
Color string `json:"color" validate:"omitempty"`
|
||||
}
|
||||
type QYGL6F2DReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
}
|
||||
@@ -265,9 +330,8 @@ type COMB86PMReq struct {
|
||||
}
|
||||
|
||||
type QCXG7A2BReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
}
|
||||
|
||||
type QCXG4896Req struct {
|
||||
PlateNo string `json:"plate_no" validate:"required"`
|
||||
AuthDate string `json:"auth_date" validate:"required,validAuthDate" encrypt:"false"`
|
||||
@@ -359,7 +423,7 @@ type COMENT01Req struct {
|
||||
}
|
||||
|
||||
type JRZQ09J8Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"omitempty,min=11,max=11,validMobileNo"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"`
|
||||
@@ -540,10 +604,18 @@ type YYSY6F2BReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type IVYZOCR1Req struct {
|
||||
PhotoData string `json:"photo_data" validate:"omitempty,validBase64Image"`
|
||||
ImageUrl string `json:"image_url" validate:"omitempty,url"`
|
||||
}
|
||||
type YYSY8B1CReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type QYGLJ0Q1Req struct {
|
||||
EntName string `json:"ent_name" validate:"omitempty,min=1,validEnterpriseName"`
|
||||
EntCode string `json:"ent_code" validate:"omitempty,validUSCI"`
|
||||
}
|
||||
type YYSY6D9AReq struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
@@ -895,7 +967,7 @@ type YYSYK8R3Req struct {
|
||||
|
||||
type YYSYF2T7Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
DateRange string `json:"date_range" validate:"required,validAuthDate" `
|
||||
DateRange string `json:"date_range" validate:"required,validDateRange"`
|
||||
}
|
||||
|
||||
type QYGL5S1IReq struct {
|
||||
@@ -930,15 +1002,17 @@ type IVYZP0T4Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
}
|
||||
|
||||
type IVYZF2T7Req struct {
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
DateRange string `json:"date_range" validate:"required,validAuthDate" `
|
||||
}
|
||||
|
||||
type IVYZX5QZReq struct {
|
||||
ReturnURL string `json:"return_url" validate:"required,validReturnURL"`
|
||||
}
|
||||
|
||||
type IVYZX5Q2Req struct {
|
||||
Token string `json:"token" validate:"required,validToken"`
|
||||
Token string `json:"token" validate:"required"`
|
||||
}
|
||||
|
||||
type JRZQ1P5GReq struct {
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"`
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ type PDFG01GZReq struct {
|
||||
Name string `json:"name" validate:"required,min=1,validName"`
|
||||
IDCard string `json:"id_card" validate:"required,validIDCard"`
|
||||
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
|
||||
AuthAuthorizeFileCode string `json:"auth_authorize_file_code" validate:"required"` // IVYZ5A9O需要
|
||||
Authorized string `json:"authorized" validate:"required,oneof=0 1"` // 授权标识,0或1
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func NewApiCall(accessId, requestParams, clientIp string) (*ApiCall, error) {
|
||||
AccessId: accessId,
|
||||
TransactionId: GenerateTransactionID(),
|
||||
ClientIp: clientIp,
|
||||
RequestParams: "",
|
||||
RequestParams: requestParams,
|
||||
Status: ApiCallStatusPending,
|
||||
StartAt: time.Now(),
|
||||
}, nil
|
||||
|
||||
35
internal/domains/api/entities/enterprise_report.go
Normal file
35
internal/domains/api/entities/enterprise_report.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
// Report 报告记录实体
|
||||
// 用于持久化存储各类报告(企业报告等),通过编号和类型区分
|
||||
type Report struct {
|
||||
// 报告编号,直接使用业务生成的 reportId 作为主键
|
||||
ReportID string `gorm:"primaryKey;type:varchar(64)" json:"report_id"`
|
||||
|
||||
// 报告类型,例如 enterprise(企业报告)、personal 等
|
||||
Type string `gorm:"type:varchar(32);not null;index" json:"type"`
|
||||
|
||||
// 调用来源API编码,例如 QYGLJ1U9
|
||||
ApiCode string `gorm:"type:varchar(32);not null;index" json:"api_code"`
|
||||
|
||||
// 企业名称和统一社会信用代码,便于后续检索
|
||||
EntName string `gorm:"type:varchar(255);index" json:"ent_name"`
|
||||
EntCode string `gorm:"type:varchar(64);index" json:"ent_code"`
|
||||
|
||||
// 原始请求参数(JSON字符串),用于审计和排错
|
||||
RequestParams string `gorm:"type:text" json:"request_params"`
|
||||
|
||||
// 报告完整JSON内容
|
||||
ReportData string `gorm:"type:text" json:"report_data"`
|
||||
|
||||
// 创建时间
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName 指定数据库表名
|
||||
func (Report) TableName() string {
|
||||
return "reports"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
)
|
||||
|
||||
// ReportRepository 报告记录仓储接口
|
||||
type ReportRepository interface {
|
||||
// Create 创建报告记录
|
||||
Create(ctx context.Context, report *entities.Report) error
|
||||
|
||||
// FindByReportID 根据报告编号查询记录
|
||||
FindByReportID(ctx context.Context, reportID string) (*entities.Report, error)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/application/api/commands"
|
||||
"tyapi-server/internal/config"
|
||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/domains/api/services/processors/comb"
|
||||
"tyapi-server/internal/domains/api/services/processors/dwbg"
|
||||
@@ -22,6 +23,7 @@ import (
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
@@ -49,10 +51,13 @@ type ApiRequestService struct {
|
||||
processorDeps *processors.ProcessorDependencies
|
||||
combService *comb.CombService
|
||||
config *config.Config
|
||||
|
||||
reportRepo api_repositories.ReportRepository
|
||||
}
|
||||
|
||||
func NewApiRequestService(
|
||||
westDexService *westdex.WestDexService,
|
||||
shujubaoService *shujubao.ShujubaoService,
|
||||
muziService *muzi.MuziService,
|
||||
yushanService *yushan.YushanService,
|
||||
tianYanChaService *tianyancha.TianYanChaService,
|
||||
@@ -64,26 +69,76 @@ func NewApiRequestService(
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
cfg *config.Config,
|
||||
) *ApiRequestService {
|
||||
return NewApiRequestServiceWithRepos(
|
||||
westDexService,
|
||||
shujubaoService,
|
||||
muziService,
|
||||
yushanService,
|
||||
tianYanChaService,
|
||||
alicloudService,
|
||||
zhichaService,
|
||||
xingweiService,
|
||||
jiguangService,
|
||||
shumaiService,
|
||||
validator,
|
||||
productManagementService,
|
||||
cfg,
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
// NewApiRequestServiceWithRepos 带自定义仓储的构造函数,便于扩展(例如企业报告记录)
|
||||
func NewApiRequestServiceWithRepos(
|
||||
westDexService *westdex.WestDexService,
|
||||
shujubaoService *shujubao.ShujubaoService,
|
||||
muziService *muzi.MuziService,
|
||||
yushanService *yushan.YushanService,
|
||||
tianYanChaService *tianyancha.TianYanChaService,
|
||||
alicloudService *alicloud.AlicloudService,
|
||||
zhichaService *zhicha.ZhichaService,
|
||||
xingweiService *xingwei.XingweiService,
|
||||
jiguangService *jiguang.JiguangService,
|
||||
shumaiService *shumai.ShumaiService,
|
||||
validator interfaces.RequestValidator,
|
||||
productManagementService *services.ProductManagementService,
|
||||
cfg *config.Config,
|
||||
reportRepo api_repositories.ReportRepository,
|
||||
) *ApiRequestService {
|
||||
// 创建组合包服务
|
||||
combService := comb.NewCombService(productManagementService)
|
||||
|
||||
// 创建处理器依赖容器
|
||||
processorDeps := processors.NewProcessorDependencies(westDexService, muziService, yushanService, tianYanChaService, alicloudService, zhichaService, xingweiService, jiguangService, shumaiService, validator, combService)
|
||||
processorDeps := processors.NewProcessorDependencies(
|
||||
westDexService,
|
||||
shujubaoService,
|
||||
muziService,
|
||||
yushanService,
|
||||
tianYanChaService,
|
||||
alicloudService,
|
||||
zhichaService,
|
||||
xingweiService,
|
||||
jiguangService,
|
||||
shumaiService,
|
||||
validator,
|
||||
combService,
|
||||
reportRepo,
|
||||
)
|
||||
|
||||
// 统一注册所有处理器
|
||||
registerAllProcessors(combService)
|
||||
|
||||
return &ApiRequestService{
|
||||
westDexService: westDexService,
|
||||
muziService: muziService,
|
||||
yushanService: yushanService,
|
||||
tianYanChaService: tianYanChaService,
|
||||
alicloudService: alicloudService,
|
||||
validator: validator,
|
||||
processorDeps: processorDeps,
|
||||
combService: combService,
|
||||
config: cfg,
|
||||
westDexService: westDexService,
|
||||
muziService: muziService,
|
||||
yushanService: yushanService,
|
||||
tianYanChaService: tianYanChaService,
|
||||
alicloudService: alicloudService,
|
||||
validator: validator,
|
||||
processorDeps: processorDeps,
|
||||
combService: combService,
|
||||
config: cfg,
|
||||
reportRepo: reportRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +176,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"JRZQ0A03": jrzq.ProcessJRZQ0A03Request,
|
||||
"JRZQ4AA8": jrzq.ProcessJRZQ4AA8Request,
|
||||
"JRZQDCBE": jrzq.ProcessJRZQDCBERequest,
|
||||
"JRZQACAB": jrzq.ProcessJRZQACABERequest, // 银行卡四要素
|
||||
"JRZQ09J8": jrzq.ProcessJRZQ09J8Request,
|
||||
"JRZQ1D09": jrzq.ProcessJRZQ1D09Request,
|
||||
"JRZQ3C7B": jrzq.ProcessJRZQ3C7BRequest,
|
||||
@@ -142,9 +198,12 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"JRZQ1W4X": jrzq.ProcessJRZQ1W4XRequest,
|
||||
"JRZQ3P01": jrzq.ProcessJRZQ3P01Request,
|
||||
"JRZQ3AG6": jrzq.ProcessJRZQ3AG6Request,
|
||||
"JRZQO6L7": jrzq.ProcessJRZQO6L7Request, // 全国自然人经济特征评分模型v3 简版
|
||||
"JRZQO7L1": jrzq.ProcessJRZQO7L1Request, // 全国自然人经济特征评分模型v4 详版
|
||||
"JRZQS7G0": jrzq.ProcessJRZQS7G0Request, // 社保综合评分V1
|
||||
"JRZQO6L7": jrzq.ProcessJRZQO6L7Request, // 全国自然人经济特征评分模型v3 简版
|
||||
"JRZQO7L1": jrzq.ProcessJRZQO7L1Request, // 全国自然人经济特征评分模型v4 详版
|
||||
"JRZQS7G0": jrzq.ProcessJRZQS7G0Request, // 社保综合评分V1
|
||||
"JRZQ1P5G": jrzq.ProcessJRZQ1P5GRequest, // 全国自然人借贷压力指数查询(2)
|
||||
"JRZQOCRE": jrzq.ProcessJRZQOCREERequest, // 银行卡OCR数卖
|
||||
"JRZQOCRY": jrzq.ProcessJRZQOCRYERequest, // 银行卡OCR数据宝
|
||||
|
||||
// QYGL系列处理器
|
||||
"QYGL8261": qygl.ProcessQYGL8261Request,
|
||||
@@ -174,6 +233,10 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QYGLNIO8": qygl.ProcessQYGLNIO8Request, //企业基本信息
|
||||
"QYGLP0HT": qygl.ProcessQYGLP0HTRequest, //股权穿透
|
||||
"QYGL5S1I": qygl.ProcessQYGL5S1IRequest, //企业司法涉诉I
|
||||
"QYGLJ1U9": qygl.ProcessQYGLJ1U9Request, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I)
|
||||
"QYGLJ0Q1": qygl.ProcessQYGLJ0Q1Request, //企业股权结构全景查询
|
||||
"QYGLUY3S": qygl.ProcessQYGLUY3SRequest, //企业经营状态全景查询
|
||||
"YYSY35TA": yysy.ProcessYYSY35TARequest, //运营商归属地数卖
|
||||
|
||||
// YYSY系列处理器
|
||||
"YYSYD50F": yysy.ProcessYYSYD50FRequest,
|
||||
@@ -202,6 +265,8 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"YYSYS9W1": yysy.ProcessYYSYS9W1Request, //手机携号转网查询
|
||||
"YYSYK8R3": yysy.ProcessYYSYK8R3Request, //手机空号检测查询
|
||||
"YYSYH6F3": yysy.ProcessYYSYH6F3Request, //运营商三要素即时版查询
|
||||
"YYSYK9R4": yysy.ProcessYYSYK9R4Request, //全网手机三要素验证1979周更新版
|
||||
"YYSYF2T7": yysy.ProcessYYSYF2T7Request, //手机二次放号检测查询
|
||||
|
||||
// IVYZ系列处理器
|
||||
"IVYZ0B03": ivyz.ProcessIVYZ0B03Request,
|
||||
@@ -242,6 +307,8 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"IVYZN2P8": ivyz.ProcessIVYZN2P8Request, //身份证实名认证政务版
|
||||
"IVYZX5QZ": ivyz.ProcessIVYZX5QZRequest, //活体检测
|
||||
"IVYZX5Q2": ivyz.ProcessIVYZX5Q2Request, //活体识别步骤二
|
||||
"IVYZOCR1": ivyz.ProcessIVYZOCR1Request, //身份证OCR
|
||||
"IVYZOCR2": ivyz.ProcessIVYZOCR2Request, //身份证OCR2数卖
|
||||
|
||||
// COMB系列处理器 - 只注册有自定义逻辑的组合包
|
||||
"COMB86PM": comb.ProcessCOMB86PMRequest, // 有自定义逻辑:重命名ApiCode
|
||||
@@ -254,19 +321,27 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
"QCXG8A3D": qcxg.ProcessQCXG8A3DRequest,
|
||||
"QCXG6B4E": qcxg.ProcessQCXG6B4ERequest,
|
||||
"QCXG4896": qcxg.ProcessQCXG4896Request,
|
||||
"QCXG5F3A": qcxg.ProcessQCXG5F3ARequest, // 极光个人车辆查询
|
||||
"QCXG4D2E": qcxg.ProcessQCXG4D2ERequest, // 极光名下车辆数量查询
|
||||
"QCXGJJ2A": qcxg.ProcessQCXGJJ2ARequest, // vin码查车辆信息(一对多)
|
||||
"QCXGGJ3A": qcxg.ProcessQCXGGJ3ARequest, // 车辆vin码查询号牌
|
||||
"QCXGYTS2": qcxg.ProcessQCXGYTS2Request, // 车辆二要素核验v2
|
||||
"QCXGP00W": qcxg.ProcessQCXGP00WRequest, // 车辆出险详版查询
|
||||
"QCXGGB2Q": qcxg.ProcessQCXGGB2QRequest, // 车辆二要素核验V1
|
||||
"QCXG4I1Z": qcxg.ProcessQCXG4I1ZRequest, // 车辆过户详版查询
|
||||
"QCXG1H7Y": qcxg.ProcessQCXG1H7YRequest, // 车辆过户简版查询
|
||||
"QCXG3Z3L": qcxg.ProcessQCXG3Z3LRequest, // 车辆维保详细版查询
|
||||
"QCXG3Y6B": qcxg.ProcessQCXG3Y6BRequest, // 车辆维保简版查询
|
||||
"QCXG2T6S": qcxg.ProcessQCXG2T6SRequest, // 车辆里程记录(品牌查询)
|
||||
"QCXG1U4U": qcxg.ProcessQCXG1U4URequest, // 车辆里程记录(混合查询)
|
||||
"QCXG5F3A": qcxg.ProcessQCXG5F3ARequest, // 极光个人车辆查询
|
||||
"QCXG4D2E": qcxg.ProcessQCXG4D2ERequest, // 极光名下车辆数量查询
|
||||
"QCXGJJ2A": qcxg.ProcessQCXGJJ2ARequest, // vin码查车辆信息(一对多)
|
||||
"QCXGGJ3A": qcxg.ProcessQCXGGJ3ARequest, // 车辆vin码查询号牌
|
||||
"QCXGYTS2": qcxg.ProcessQCXGYTS2Request, // 车辆二要素核验v2
|
||||
"QCXGP00W": qcxg.ProcessQCXGP00WRequest, // 车辆出险详版查询
|
||||
"QCXGGB2Q": qcxg.ProcessQCXGGB2QRequest, // 车辆二要素核验V1
|
||||
"QCXG4I1Z": qcxg.ProcessQCXG4I1ZRequest, // 车辆过户详版查询
|
||||
"QCXG1H7Y": qcxg.ProcessQCXG1H7YRequest, // 车辆过户简版查询
|
||||
"QCXG3Z3L": qcxg.ProcessQCXG3Z3LRequest, // 车辆维保详细版查询
|
||||
"QCXG3Y6B": qcxg.ProcessQCXG3Y6BRequest, // 车辆维保简版查询
|
||||
"QCXG2T6S": qcxg.ProcessQCXG2T6SRequest, // 车辆里程记录(品牌查询)
|
||||
"QCXG1U4U": qcxg.ProcessQCXG1U4URequest, //
|
||||
"QCXG9F5C": qcxg.ProcessQCXG9F5CERequest, //疑似营运车辆注册平台数 10386
|
||||
"QCXG3B8Z": qcxg.ProcessQCXG3B8ZRequest, //疑似运营车辆查询(月度里程)10268
|
||||
"QCXGP1W3": qcxg.ProcessQCXGP1W3Request, //疑似运营车辆查询(季度里程)10269
|
||||
"QCXGM7R9": qcxg.ProcessQCXGM7R9Request, //疑似运营车辆查询(半年度里程)10270
|
||||
"QCXGU2K4": qcxg.ProcessQCXGU2K4Request, //疑似运营车辆查询(年度里程)10271
|
||||
"QCXG5U0Z": qcxg.ProcessQCXG5U0ZRequest, // 车辆静态信息查询 10479
|
||||
"QCXGY7F2": qcxg.ProcessQCXGY7F2Request, // 二手车VIN估值 10443
|
||||
"QCXG3M7Z": qcxg.ProcessQCXG3M7ZRequest, //人车关系核验(ETC)10093 月更
|
||||
|
||||
// DWBG系列处理器 - 多维报告
|
||||
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,
|
||||
|
||||
@@ -176,6 +176,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"QCXG8A3D": &dto.QCXG8A3DReq{},
|
||||
"QCXG6B4E": &dto.QCXG6B4EReq{},
|
||||
"QYGL2B5C": &dto.QYGL2B5CReq{},
|
||||
"QYGLJ1U9": &dto.QYGLJ1U9Req{},
|
||||
"JRZQ2F8A": &dto.JRZQ2F8AReq{},
|
||||
"JRZQ1E7B": &dto.JRZQ1E7BReq{},
|
||||
"JRZQ3C9R": &dto.JRZQ3C9RReq{},
|
||||
@@ -244,6 +245,24 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
||||
"IVYZX5Q2": &dto.IVYZX5Q2Req{}, //活体识别步骤二
|
||||
"PDFG01GZ": &dto.PDFG01GZReq{}, //
|
||||
"QYGL5S1I": &dto.QYGL5S1IReq{}, //企业司法涉诉V2
|
||||
"JRZQACAB": &dto.JRZQACABReq{}, //银行卡四要素
|
||||
"QCXG9F5C": &dto.QCXG9F5CReq{}, //疑似营运车辆注册平台数 10386
|
||||
"QCXG3B8Z": &dto.QCXG3B8ZReq{}, //疑似运营车辆查询(月度里程)10268
|
||||
"QCXGP1W3": &dto.QCXGP1W3Req{}, //疑似运营车辆查询(季度里程)10269
|
||||
"QCXGM7R9": &dto.QCXGM7R9Req{}, //疑似运营车辆查询(半年度里程)10270
|
||||
"QCXGU2K4": &dto.QCXGU2K4Req{}, //疑似运营车辆查询(年度里程)10271
|
||||
"QCXG5U0Z": &dto.QCXG5U0ZReq{}, //车辆静态信息查询 10479
|
||||
"QCXGY7F2": &dto.QCXGY7F2Req{}, //二手车VIN估值 10443
|
||||
"YYSYK9R4": &dto.YYSYK9R4Req{}, //全网手机三要素验证1979周更新版
|
||||
"QCXG3M7Z": &dto.QCXG3M7ZReq{}, //人车关系核验(ETC)10093 月更
|
||||
"JRZQ1P5G": &dto.JRZQ1P5GReq{}, //全国自然人借贷压力指数查询(2)
|
||||
"IVYZOCR1": &dto.IVYZOCR1Req{}, //身份证OCR
|
||||
"IVYZOCR2": &dto.IVYZOCR1Req{}, //身份证OCR2数卖
|
||||
"QYGLJ0Q1": &dto.QYGLJ0Q1Req{}, //企业股权结构全景查询
|
||||
"QYGLUY3S": &dto.QYGLUY3SReq{}, //企业全量信息核验V2 可用
|
||||
"JRZQOCRE": &dto.JRZQOCREReq{}, //银行卡OCR数卖
|
||||
"JRZQOCRY": &dto.JRZQOCRYReq{}, //银行卡OCR数据宝
|
||||
"YYSY35TA": &dto.YYSY35TAReq{}, //运营商归属地数卖
|
||||
}
|
||||
|
||||
// 优先返回已配置的DTO
|
||||
@@ -351,6 +370,8 @@ func (s *FormConfigServiceImpl) parseValidationRules(validateTag string) string
|
||||
frontendRules = append(frontendRules, "日期格式")
|
||||
case rule == "validAuthDate":
|
||||
frontendRules = append(frontendRules, "授权日期格式")
|
||||
case rule == "validDateRange":
|
||||
frontendRules = append(frontendRules, "日期范围格式(YYYYMMDD-YYYYMMDD)")
|
||||
case rule == "validTimeRange":
|
||||
frontendRules = append(frontendRules, "时间范围格式")
|
||||
case rule == "validMobileType":
|
||||
@@ -388,6 +409,8 @@ func (s *FormConfigServiceImpl) getFieldType(fieldType reflect.Type, validation
|
||||
return "text" // time_range是HH:MM-HH:MM格式,使用文本输入
|
||||
} else if strings.Contains(validation, "授权日期格式") {
|
||||
return "text" // auth_date是YYYYMMDD-YYYYMMDD格式,使用文本输入
|
||||
} else if strings.Contains(validation, "日期范围格式") {
|
||||
return "text" // date_range 为 YYYYMMDD-YYYYMMDD,使用文本输入便于直接输入
|
||||
} else if strings.Contains(validation, "日期") {
|
||||
return "date"
|
||||
} else if strings.Contains(validation, "链接") {
|
||||
@@ -423,6 +446,7 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||
"ent_name": "企业名称",
|
||||
"legal_person": "法人姓名",
|
||||
"ent_code": "企业代码",
|
||||
"ent_reg_no": "企业注册号",
|
||||
"auth_date": "授权日期",
|
||||
"date_range": "日期范围",
|
||||
"time_range": "时间范围",
|
||||
@@ -444,7 +468,7 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||
"plate_type": "号牌类型",
|
||||
"vin_code": "车辆识别代号VIN码",
|
||||
"return_type": "返回类型",
|
||||
"photo_data": "人脸图片",
|
||||
"photo_data": "入参图片base64编码",
|
||||
"owner_type": "企业主类型",
|
||||
"type": "查询类型",
|
||||
"query_reason_id": "查询原因ID",
|
||||
@@ -456,9 +480,14 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||
"notice_model": "车辆型号",
|
||||
"vlphoto_data": "行驶证图片",
|
||||
"carplate_type": "车辆号牌类型",
|
||||
"image_url": "行驶证图片地址",
|
||||
"image_url": "入参图片地址",
|
||||
"reg_url": "车辆登记证图片地址",
|
||||
"token": "token采集及获取结果时所使用的凭证,有效期2个小时,在此时效内,应用侧可以发起采集请求(重复的采集所触发的结果会被忽略)和结果查询",
|
||||
"vehicle_name": "车型名称",
|
||||
"vehicle_location": "车辆所在地",
|
||||
"first_registrationdate": "首次登记日期",
|
||||
"color": "颜色",
|
||||
"plate_color": "车牌颜色",
|
||||
}
|
||||
|
||||
if label, exists := labelMap[jsonTag]; exists {
|
||||
@@ -480,6 +509,7 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso
|
||||
"ent_name": "示例企业有限公司",
|
||||
"legal_person": "王五",
|
||||
"ent_code": "91110000123456789X",
|
||||
"ent_reg_no": "110000000123456",
|
||||
"auth_date": "20240101-20241231",
|
||||
"date_range": "20240101-20241231",
|
||||
"time_range": "09:00-18:00",
|
||||
@@ -516,6 +546,11 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso
|
||||
"image_url": "https://example.com/images/driving_license.jpg",
|
||||
"reg_url": "https://example.com/images/vehicle_registration.jpg",
|
||||
"token": "0fc79b80371f45e2ac1c693ef9136b24",
|
||||
"vehicle_name": "车型名称,示例:凌派 2020款 锐·混动 1.5L 锐·舒适版",
|
||||
"vehicle_location": "车辆所在地,示例:北京",
|
||||
"first_registrationdate": "初登日期,示例:2020-05",
|
||||
"color": "示例:白色",
|
||||
"plate_color": "车牌颜色(0:蓝色,1:黄色,2:黑色,3:白色,4:渐变绿色,5:黄绿双拼色,6:蓝白渐变色,7:临时牌照,11:绿色,12:红色)默认标准车牌查蓝色,新能源车牌查绿色)",
|
||||
}
|
||||
|
||||
if example, exists := exampleMap[jsonTag]; exists {
|
||||
@@ -546,6 +581,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"ent_reg_no": "请输入企业注册号(统一社会信用代码)",
|
||||
"auth_date": "请输入授权日期范围(YYYYMMDD-YYYYMMDD)",
|
||||
"date_range": "请输入日期范围(YYYYMMDD-YYYYMMDD)",
|
||||
"time_range": "请输入时间范围(HH:MM-HH:MM)",
|
||||
@@ -567,7 +603,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
||||
"plate_type": "请选择号牌类型(01或02)",
|
||||
"vin_code": "请输入17位车辆识别代号VIN码",
|
||||
"return_type": "请选择返回类型",
|
||||
"photo_data": "请输入base64编码的人脸图片(支持JPG、BMP、PNG格式)",
|
||||
"photo_data": "请输入base64编码的入参图片(支持JPG、BMP、PNG格式)",
|
||||
"ownerType": "请选择企业主类型",
|
||||
"type": "请选择查询类型",
|
||||
"query_reason_id": "请选择查询原因ID",
|
||||
@@ -582,6 +618,11 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
||||
"image_url": "请输入行驶证图片地址",
|
||||
"reg_url": "请输入车辆登记证图片地址",
|
||||
"token": "请输入token",
|
||||
"vehicle_name": "请输入车型名称",
|
||||
"vehicle_location": "请输入车辆所在地",
|
||||
"first_registrationdate": "请输入首次登记日期,格式:YYYY-MM",
|
||||
"color": "请输入颜色",
|
||||
"plate_color": "请输入车牌颜色",
|
||||
}
|
||||
|
||||
if placeholder, exists := placeholderMap[jsonTag]; exists {
|
||||
@@ -614,6 +655,7 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"ent_reg_no": "请输入企业注册号(统一社会信用代码)",
|
||||
"auth_date": "请输入授权日期范围,格式:YYYYMMDD-YYYYMMDD,且日期范围必须包括今天",
|
||||
"date_range": "请输入日期范围,格式:YYYYMMDD-YYYYMMDD",
|
||||
"time_range": "请输入时间范围,格式:HH:MM-HH:MM",
|
||||
@@ -635,7 +677,7 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s
|
||||
"plate_type": "号牌类型:01-小型汽车;02-大型汽车(可选)",
|
||||
"vin_code": "请输入17位车辆识别代号VIN码(Vehicle Identification Number)",
|
||||
"return_type": "返回类型:1-专业和学校名称数据返回编码形式(默认);2-专业和学校名称数据返回中文名称",
|
||||
"photo_data": "人脸图片(必填):base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||
"photo_data": "入参图片:base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||
"owner_type": "企业主类型编码:1-法定代表人;2-主要人员;3-自然人股东;4-法定代表人及自然人股东;5-其他",
|
||||
"type": "查询类型:per-人员,ent-企业 ",
|
||||
"query_reason_id": "查询原因ID:1-授信审批;2-贷中管理;3-贷后管理;4-异议处理;5-担保查询;6-租赁资质审查;7-融资租赁审批;8-借贷撮合查询;9-保险审批;10-资质审核;11-风控审核;12-企业背调",
|
||||
@@ -647,9 +689,14 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s
|
||||
"notice_model": "车辆型号",
|
||||
"vlphoto_data": "行驶证图片:base64编码的图片数据,仅支持JPG、BMP、PNG三种格式",
|
||||
"carplate_type": "车辆号牌类型:01-大型汽车;02-小型汽车;03-使馆汽车;04-领馆汽车;05-境外汽车;06-外籍汽车;07-普通摩托车;08-轻便摩托车;09-使馆摩托车;10-领馆摩托车;11-境外摩托车;12-外籍摩托车;13-低速车;14-拖拉机;15-挂车;16-教练汽车;17-教练摩托车;20-临时入境汽车;21-临时入境摩托车;22-临时行驶车;23-警用汽车;24-警用摩托;51-新能源大型车;52-新能源小型车",
|
||||
"image_url": "行驶证图片地址(必填):请提供行驶证的图片URL地址",
|
||||
"image_url": "入参图片url地址",
|
||||
"reg_url": "车辆登记证图片地址(非必填):请提供车辆登记证的图片URL地址",
|
||||
"token": "token采集及获取结果时所使用的凭证,有效期2个小时,在此时效内,应用侧可以发起采集请求(重复的采集所触发的结果会被忽略)和结果查询",
|
||||
"vehicle_name": "车型名称,示例:凌派 2020款 锐·混动 1.5L 锐·舒适版",
|
||||
"vehicle_location": "车辆所在地",
|
||||
"first_registrationdate": "首次登记日期,格式:YYYY-MM",
|
||||
"color": "颜色",
|
||||
"plate_color": "车牌颜色",
|
||||
}
|
||||
|
||||
if desc, exists := descMap[jsonTag]; exists {
|
||||
|
||||
@@ -48,6 +48,7 @@ type baseProductData struct {
|
||||
RiskWarning riskWarning `json:"riskWarning"`
|
||||
StandLiveInfo standLiveInfo `json:"standLiveInfo"`
|
||||
VerifyRule string `json:"verifyRule"`
|
||||
MultCourtInfo multCourtInfo `json:"multCourtInfo"`
|
||||
}
|
||||
|
||||
// baseInfo 存放被查询人的基础身份信息
|
||||
@@ -171,6 +172,34 @@ type standLiveInfo struct {
|
||||
FinalAuthResult string `json:"finalAuthResult"`
|
||||
}
|
||||
|
||||
// multCourtInfo 司法风险核验产品,统一承载涉案/执行/失信/限高四类公告
|
||||
type multCourtInfo struct {
|
||||
LegalCasesFlag int `json:"legalCasesFlag"`
|
||||
LegalCases []multCaseItem `json:"legalCases"`
|
||||
ExecutionCasesFlag int `json:"executionCasesFlag"`
|
||||
ExecutionCases []multCaseItem `json:"executionCases"`
|
||||
DisinCasesFlag int `json:"disinCasesFlag"`
|
||||
DisinCases []multCaseItem `json:"disinCases"`
|
||||
LimitCasesFlag int `json:"limitCasesFlag"`
|
||||
LimitCases []multCaseItem `json:"limitCases"`
|
||||
}
|
||||
|
||||
// multCaseItem 司法各类公告的通用记录结构
|
||||
type multCaseItem struct {
|
||||
CaseNumber string `json:"caseNumber"`
|
||||
CaseType string `json:"caseType"`
|
||||
Court string `json:"court"`
|
||||
LitigantType string `json:"litigantType"`
|
||||
FilingTime string `json:"filingTime"`
|
||||
DisposalTime string `json:"disposalTime"`
|
||||
CaseStatus string `json:"caseStatus"`
|
||||
ExecutionAmount string `json:"executionAmount"`
|
||||
RepaidAmount string `json:"repaidAmount"`
|
||||
CaseReason string `json:"caseReason"`
|
||||
DisposalMethod string `json:"disposalMethod"`
|
||||
JudgmentResult string `json:"judgmentResult"`
|
||||
}
|
||||
|
||||
// --- FLXG7E8F ---
|
||||
|
||||
// judicialProductData 对应 FLXG7E8F 司法产品数据
|
||||
@@ -702,52 +731,60 @@ func buildBasicInfo(ctx context.Context, sourceCtx *sourceContext) reportBasicIn
|
||||
Details: carrierDetails,
|
||||
})
|
||||
|
||||
// 兼容处理:安全访问JudicialData
|
||||
var stat *lawsuitStat
|
||||
if sourceCtx != nil && sourceCtx.JudicialData != nil {
|
||||
stat = &sourceCtx.JudicialData.JudicialData.LawsuitStat
|
||||
}
|
||||
|
||||
totalCaseCount := 0
|
||||
totalCriminal := 0
|
||||
// 兼容处理:从DWBG8B4D.multCourtInfo获取司法记录条数,并按案件类型拆分
|
||||
totalExecution := 0
|
||||
if stat != nil {
|
||||
// 兼容处理:安全访问Cases数组
|
||||
totalCriminal = safeLen(stat.Criminal.Cases)
|
||||
totalCaseCount = totalCriminal + safeLen(stat.Civil.Cases) + safeLen(stat.Administrative.Cases) + safeLen(stat.Preservation.Cases) + safeLen(stat.Bankrupt.Cases)
|
||||
totalExecution = safeLen(stat.Implement.Cases)
|
||||
}
|
||||
|
||||
totalDishonest := 0
|
||||
totalRestriction := 0
|
||||
if sourceCtx != nil && sourceCtx.JudicialData != nil {
|
||||
totalDishonest = safeLen(sourceCtx.JudicialData.JudicialData.BreachCaseList)
|
||||
totalRestriction = safeLen(sourceCtx.JudicialData.JudicialData.ConsumptionRestrictionList)
|
||||
criminalCount := 0
|
||||
civilCount := 0
|
||||
administrativeCount := 0
|
||||
preservationCount := 0
|
||||
bankruptCount := 0
|
||||
|
||||
if sourceCtx != nil && sourceCtx.BaseData != nil {
|
||||
mc := sourceCtx.BaseData.MultCourtInfo
|
||||
totalExecution = safeLen(mc.ExecutionCases)
|
||||
totalDishonest = safeLen(mc.DisinCases)
|
||||
totalRestriction = safeLen(mc.LimitCases)
|
||||
|
||||
for _, c := range mc.LegalCases {
|
||||
switch strings.TrimSpace(c.CaseType) {
|
||||
case "刑事案件":
|
||||
criminalCount++
|
||||
case "民事案件":
|
||||
civilCount++
|
||||
case "行政案件":
|
||||
administrativeCount++
|
||||
case "保全审查":
|
||||
preservationCount++
|
||||
case "破产清算":
|
||||
bankruptCount++
|
||||
default:
|
||||
// 其它类型暂不单独展示,只参与是否有司法记录的判断
|
||||
civilCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if totalCaseCount > 0 || totalExecution > 0 || totalDishonest > 0 || totalRestriction > 0 {
|
||||
detailParts := make([]string, 0, 5)
|
||||
if stat != nil {
|
||||
addCaseDetail := func(label string, count int) {
|
||||
if count > 0 {
|
||||
detailParts = append(detailParts, fmt.Sprintf("%s%d条", label, count))
|
||||
}
|
||||
totalLegal := criminalCount + civilCount + administrativeCount + preservationCount + bankruptCount
|
||||
|
||||
if totalLegal > 0 || totalExecution > 0 || totalDishonest > 0 || totalRestriction > 0 {
|
||||
detailParts := make([]string, 0, 8)
|
||||
addCaseDetail := func(label string, count int) {
|
||||
if count > 0 {
|
||||
detailParts = append(detailParts, fmt.Sprintf("%s%d条", label, count))
|
||||
}
|
||||
addCaseDetail("刑事案件", safeLen(stat.Criminal.Cases))
|
||||
addCaseDetail("民事案件", safeLen(stat.Civil.Cases))
|
||||
addCaseDetail("行政案件", safeLen(stat.Administrative.Cases))
|
||||
addCaseDetail("非诉保全审查案件", safeLen(stat.Preservation.Cases))
|
||||
addCaseDetail("强制清算与破产案件", safeLen(stat.Bankrupt.Cases))
|
||||
}
|
||||
if totalExecution > 0 {
|
||||
detailParts = append(detailParts, fmt.Sprintf("执行案件%d条", totalExecution))
|
||||
}
|
||||
if totalDishonest > 0 {
|
||||
detailParts = append(detailParts, fmt.Sprintf("失信案件%d条", totalDishonest))
|
||||
}
|
||||
if totalRestriction > 0 {
|
||||
detailParts = append(detailParts, fmt.Sprintf("限高案件%d条", totalRestriction))
|
||||
}
|
||||
|
||||
addCaseDetail("刑事案件", criminalCount)
|
||||
addCaseDetail("民事案件", civilCount)
|
||||
addCaseDetail("行政案件", administrativeCount)
|
||||
addCaseDetail("保全审查案件", preservationCount)
|
||||
addCaseDetail("强制清算与破产案件", bankruptCount)
|
||||
addCaseDetail("执行案件", totalExecution)
|
||||
addCaseDetail("失信案件", totalDishonest)
|
||||
addCaseDetail("限高案件", totalRestriction)
|
||||
|
||||
details := buildCaseDetails(detailParts)
|
||||
verifications = append(verifications, verificationItem{
|
||||
Item: "法院信息",
|
||||
@@ -821,50 +858,78 @@ func buildRiskIdentification(ctx context.Context, sourceCtx *sourceContext) risk
|
||||
},
|
||||
}
|
||||
|
||||
// 兼容处理:安全访问JudicialData
|
||||
if sourceCtx == nil || sourceCtx.JudicialData == nil {
|
||||
log.Debug("JudicialData为空,返回空的风险识别数据",
|
||||
// 兼容处理:从DWBG8B4D.multCourtInfo获取司法信息
|
||||
if sourceCtx == nil || sourceCtx.BaseData == nil {
|
||||
log.Debug("BaseData为空,返回空的风险识别数据",
|
||||
zap.String("api_code", "COMBHZY2"),
|
||||
)
|
||||
return identification
|
||||
}
|
||||
|
||||
stat := sourceCtx.JudicialData.JudicialData.LawsuitStat
|
||||
mc := sourceCtx.BaseData.MultCourtInfo
|
||||
baseName := ""
|
||||
baseID := ""
|
||||
if sourceCtx.BaseData != nil {
|
||||
baseName = sourceCtx.BaseData.BaseInfo.Name
|
||||
baseID = sourceCtx.BaseData.BaseInfo.IdCard
|
||||
}
|
||||
baseName = sourceCtx.BaseData.BaseInfo.Name
|
||||
baseID = sourceCtx.BaseData.BaseInfo.IdCard
|
||||
|
||||
// 兼容处理:安全访问Cases数组
|
||||
// 涉案公告列表:直接使用multCourtInfo.legalCases
|
||||
caseRecords := make([]caseAnnouncementRecord, 0)
|
||||
if stat.Civil.Cases != nil {
|
||||
caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Civil.Cases, "民事案件")...)
|
||||
}
|
||||
if stat.Criminal.Cases != nil {
|
||||
caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Criminal.Cases, "刑事案件")...)
|
||||
}
|
||||
if stat.Administrative.Cases != nil {
|
||||
caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Administrative.Cases, "行政案件")...)
|
||||
}
|
||||
if stat.Preservation.Cases != nil {
|
||||
caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Preservation.Cases, "非诉保全审查")...)
|
||||
}
|
||||
if stat.Bankrupt.Cases != nil {
|
||||
caseRecords = append(caseRecords, convertCaseAnnouncements(stat.Bankrupt.Cases, "强制清算与破产")...)
|
||||
for _, c := range mc.LegalCases {
|
||||
record := caseAnnouncementRecord{
|
||||
CaseNumber: defaultIfEmpty(c.CaseNumber, "-"),
|
||||
CaseType: defaultIfEmpty(c.CaseType, "-"),
|
||||
FilingDate: defaultIfEmpty(c.FilingTime, ""),
|
||||
Authority: defaultIfEmpty(c.Court, ""),
|
||||
}
|
||||
caseRecords = append(caseRecords, record)
|
||||
}
|
||||
identification.CaseAnnouncements.Records = caseRecords
|
||||
|
||||
if stat.Implement.Cases != nil {
|
||||
identification.EnforcementAnnouncements.Records = convertEnforcementAnnouncements(stat.Implement.Cases)
|
||||
// 执行公告列表:multCourtInfo.executionCases
|
||||
enfRecords := make([]enforcementAnnouncementRecord, 0, len(mc.ExecutionCases))
|
||||
for _, c := range mc.ExecutionCases {
|
||||
amountStr := strings.TrimSpace(c.ExecutionAmount)
|
||||
targetAmount := "-"
|
||||
if amountStr != "" && amountStr != "-" {
|
||||
targetAmount = formatCurrencyYuan(parseFloatSafe(amountStr))
|
||||
}
|
||||
record := enforcementAnnouncementRecord{
|
||||
CaseNumber: defaultIfEmpty(c.CaseNumber, "-"),
|
||||
TargetAmount: targetAmount,
|
||||
FilingDate: defaultIfEmpty(c.FilingTime, ""),
|
||||
Court: defaultIfEmpty(c.Court, ""),
|
||||
Status: defaultIfEmpty(c.CaseStatus, "-"),
|
||||
}
|
||||
enfRecords = append(enfRecords, record)
|
||||
}
|
||||
if sourceCtx.JudicialData.JudicialData.BreachCaseList != nil {
|
||||
identification.DishonestAnnouncements.Records = convertDishonestAnnouncements(sourceCtx.JudicialData.JudicialData.BreachCaseList, baseName, baseID)
|
||||
identification.EnforcementAnnouncements.Records = enfRecords
|
||||
|
||||
// 失信公告列表:multCourtInfo.disinCases
|
||||
dishonestRecords := make([]dishonestAnnouncementRecord, 0, len(mc.DisinCases))
|
||||
for _, item := range mc.DisinCases {
|
||||
record := dishonestAnnouncementRecord{
|
||||
DishonestPerson: defaultIfEmpty(baseName, "-"),
|
||||
IdCard: defaultIfEmpty(baseID, "-"),
|
||||
Court: defaultIfEmpty(item.Court, ""),
|
||||
FilingDate: defaultIfEmpty(item.FilingTime, item.DisposalTime),
|
||||
PerformanceStatus: defaultIfEmpty(item.JudgmentResult, defaultIfEmpty(item.CaseStatus, "-")),
|
||||
}
|
||||
dishonestRecords = append(dishonestRecords, record)
|
||||
}
|
||||
if sourceCtx.JudicialData.JudicialData.ConsumptionRestrictionList != nil {
|
||||
identification.HighConsumptionRestrictionAnn.Records = convertConsumptionRestrictions(sourceCtx.JudicialData.JudicialData.ConsumptionRestrictionList, baseName, baseID)
|
||||
identification.DishonestAnnouncements.Records = dishonestRecords
|
||||
|
||||
// 限高公告列表:multCourtInfo.limitCases
|
||||
limitRecords := make([]highRestrictionAnnouncementRecord, 0, len(mc.LimitCases))
|
||||
for _, item := range mc.LimitCases {
|
||||
record := highRestrictionAnnouncementRecord{
|
||||
RestrictedPerson: defaultIfEmpty(baseName, "-"),
|
||||
IdCard: defaultIfEmpty(baseID, "-"),
|
||||
Court: defaultIfEmpty(item.Court, ""),
|
||||
StartDate: defaultIfEmpty(item.FilingTime, item.DisposalTime),
|
||||
Measure: "限制高消费",
|
||||
}
|
||||
limitRecords = append(limitRecords, record)
|
||||
}
|
||||
identification.HighConsumptionRestrictionAnn.Records = limitRecords
|
||||
|
||||
return identification
|
||||
}
|
||||
@@ -1299,13 +1364,13 @@ func gatherOtherRiskDetails(sourceCtx *sourceContext) string {
|
||||
if risk.VeryFrequentRentalApplications > 0 {
|
||||
hits = append(hits, "租赁机构申请次数极多")
|
||||
}
|
||||
// 兼容处理:安全访问JudicialData
|
||||
if sourceCtx != nil && sourceCtx.JudicialData != nil {
|
||||
stat := sourceCtx.JudicialData.JudicialData.LawsuitStat
|
||||
totalCase := safeLen(stat.Civil.Cases) + safeLen(stat.Criminal.Cases) + safeLen(stat.Administrative.Cases) + safeLen(stat.Preservation.Cases) + safeLen(stat.Bankrupt.Cases)
|
||||
totalExecution := safeLen(stat.Implement.Cases)
|
||||
totalDishonest := safeLen(sourceCtx.JudicialData.JudicialData.BreachCaseList)
|
||||
totalRestriction := safeLen(sourceCtx.JudicialData.JudicialData.ConsumptionRestrictionList)
|
||||
// 兼容处理:根据DWBG8B4D.multCourtInfo判断是否存在司法记录
|
||||
if sourceCtx != nil && sourceCtx.BaseData != nil {
|
||||
mc := sourceCtx.BaseData.MultCourtInfo
|
||||
totalCase := safeLen(mc.LegalCases)
|
||||
totalExecution := safeLen(mc.ExecutionCases)
|
||||
totalDishonest := safeLen(mc.DisinCases)
|
||||
totalRestriction := safeLen(mc.LimitCases)
|
||||
if totalCase > 0 || totalExecution > 0 || totalDishonest > 0 || totalRestriction > 0 {
|
||||
hits = append(hits, "存在司法风险记录")
|
||||
}
|
||||
@@ -1393,11 +1458,10 @@ func buildRuleHitBullet(summary reportSummary) (string, bool, bool) {
|
||||
}
|
||||
|
||||
func buildJudicialBullet(ctx *sourceContext) (string, bool, bool) {
|
||||
if ctx.JudicialData == nil {
|
||||
if ctx == nil || ctx.BaseData == nil {
|
||||
return "", false, false
|
||||
}
|
||||
|
||||
stat := ctx.JudicialData.JudicialData.LawsuitStat
|
||||
mc := ctx.BaseData.MultCourtInfo
|
||||
parts := make([]string, 0, 6)
|
||||
|
||||
addPart := func(label string, count int) {
|
||||
@@ -1406,12 +1470,10 @@ func buildJudicialBullet(ctx *sourceContext) (string, bool, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
addPart("刑事案件", len(stat.Criminal.Cases))
|
||||
addPart("民事案件", len(stat.Civil.Cases))
|
||||
addPart("行政案件", len(stat.Administrative.Cases))
|
||||
addPart("执行案件", len(stat.Implement.Cases))
|
||||
addPart("失信记录", len(ctx.JudicialData.JudicialData.BreachCaseList))
|
||||
addPart("限高记录", len(ctx.JudicialData.JudicialData.ConsumptionRestrictionList))
|
||||
addPart("涉案公告", len(mc.LegalCases))
|
||||
addPart("执行案件", len(mc.ExecutionCases))
|
||||
addPart("失信记录", len(mc.DisinCases))
|
||||
addPart("限高记录", len(mc.LimitCases))
|
||||
|
||||
if len(parts) == 0 {
|
||||
return "", false, false
|
||||
|
||||
@@ -2,13 +2,16 @@ package processors
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/application/api/commands"
|
||||
"tyapi-server/internal/domains/api/repositories"
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
"tyapi-server/internal/infrastructure/external/jiguang"
|
||||
"tyapi-server/internal/infrastructure/external/muzi"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/infrastructure/external/tianyancha"
|
||||
"tyapi-server/internal/infrastructure/external/westdex"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/yushan"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
@@ -28,6 +31,7 @@ type CallContext struct {
|
||||
// ProcessorDependencies 处理器依赖容器
|
||||
type ProcessorDependencies struct {
|
||||
WestDexService *westdex.WestDexService
|
||||
ShujubaoService *shujubao.ShujubaoService
|
||||
MuziService *muzi.MuziService
|
||||
YushanService *yushan.YushanService
|
||||
TianYanChaService *tianyancha.TianYanChaService
|
||||
@@ -40,11 +44,15 @@ type ProcessorDependencies struct {
|
||||
CombService CombServiceInterface // Changed to interface to break import cycle
|
||||
Options *commands.ApiCallOptions // 添加Options支持
|
||||
CallContext *CallContext // 添加CallApi调用上下文
|
||||
|
||||
// 企业报告记录仓储,用于持久化 QYGLJ1U9 生成的企业报告
|
||||
ReportRepo repositories.ReportRepository
|
||||
}
|
||||
|
||||
// NewProcessorDependencies 创建处理器依赖容器
|
||||
func NewProcessorDependencies(
|
||||
westDexService *westdex.WestDexService,
|
||||
shujubaoService *shujubao.ShujubaoService,
|
||||
muziService *muzi.MuziService,
|
||||
yushanService *yushan.YushanService,
|
||||
tianYanChaService *tianyancha.TianYanChaService,
|
||||
@@ -55,9 +63,11 @@ func NewProcessorDependencies(
|
||||
shumaiService *shumai.ShumaiService,
|
||||
validator interfaces.RequestValidator,
|
||||
combService CombServiceInterface, // Changed to interface
|
||||
reportRepo repositories.ReportRepository,
|
||||
) *ProcessorDependencies {
|
||||
return &ProcessorDependencies{
|
||||
WestDexService: westDexService,
|
||||
ShujubaoService: shujubaoService,
|
||||
MuziService: muziService,
|
||||
YushanService: yushanService,
|
||||
TianYanChaService: tianYanChaService,
|
||||
@@ -70,6 +80,7 @@ func NewProcessorDependencies(
|
||||
CombService: combService,
|
||||
Options: nil, // 初始化为nil,在调用时设置
|
||||
CallContext: nil, // 初始化为nil,在调用时设置
|
||||
ReportRepo: reportRepo,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ func ProcessDWBG6A2CRequest(ctx context.Context, params []byte, deps *processors
|
||||
if respMap, ok := respData.(map[string]interface{}); ok {
|
||||
delete(respMap, "reportUrl")
|
||||
delete(respMap, "multCourtInfo")
|
||||
delete(respMap, "judiciaRiskInfos")
|
||||
// delete(respMap, "judiciaRiskInfos")
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ func ProcessFLXG0V4BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" || paramsDto.IDCard == "320682198910134998"{
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||
|
||||
@@ -20,7 +20,7 @@ func ProcessFLXG5A3BRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"{
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"|| paramsDto.IDCard == "320682198910134998"{
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
|
||||
@@ -20,7 +20,7 @@ func ProcessFLXG7E8FRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"|| paramsDto.IDCard == "320682198910134998" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
|
||||
@@ -20,7 +20,7 @@ func ProcessFLXGCA3DRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550" {
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"|| paramsDto.IDCard == "320682198910134998" {
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedName, err := deps.WestDexService.Encrypt(paramsDto.Name)
|
||||
|
||||
@@ -25,7 +25,7 @@ func ProcessFLXGDEA9Request(ctx context.Context, params []byte, deps *processors
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"{
|
||||
if paramsDto.IDCard == "350681198611130611" || paramsDto.IDCard == "622301200006250550"|| paramsDto.IDCard == "320682198910134998"{
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("查询为空"))
|
||||
}
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessIVYZOCR1Request IVYZOCR1 身份证OCR API 处理方法(使用数据宝服务示例)
|
||||
func ProcessIVYZOCR1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZOCR1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "8782f2a32463f75b53096323461df735",
|
||||
"imageId": paramsDto.PhotoData,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/trade/user/1985"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package ivyz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessIVYZOCR2Request IVYZOCR2 OCR识别API处理方法数卖
|
||||
func ProcessIVYZOCR2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.IVYZOCR1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.PhotoData == "" && paramsDto.ImageUrl == "" {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("photo_data or image_url is required"))
|
||||
}
|
||||
|
||||
// 2选1:有值的用对应 key,空则用另一个
|
||||
reqFormData := make(map[string]interface{})
|
||||
if paramsDto.PhotoData != "" {
|
||||
reqFormData["image"] = paramsDto.PhotoData
|
||||
} else {
|
||||
reqFormData["url"] = paramsDto.ImageUrl
|
||||
}
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/idcard/ocr" // 接口路径,根据数脉文档填写(如 v4/xxx)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrNotFound) {
|
||||
// 查无记录情况
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessIVYZP2Q6Request IVYZP2Q6 API处理方法 - 身份认证二要素
|
||||
@@ -21,37 +21,64 @@ func ProcessIVYZP2Q6Request(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 加密姓名
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
reqFormData := map[string]interface{}{
|
||||
"idcard": paramsDto.IDCard,
|
||||
"name": paramsDto.Name,
|
||||
}
|
||||
|
||||
// 加密身份证号
|
||||
encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/id_card/check" // 接口路径,根据数脉文档填写(如 v4/xxx)
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idCard": encryptedIDCard,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI011", reqData)
|
||||
// 先尝试使用政务接口(app_id2 和 app_secret2)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, true)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
// 使用实时接口(app_id 和 app_secret)重试
|
||||
respBytes, err = deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData, false)
|
||||
// 如果重试后仍然失败,返回错误
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrNotFound) {
|
||||
// 查无记录情况
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
// 数据源返回 result(0-一致/1-不一致/2-无记录),映射为 state(1-匹配/2-不匹配/3-异常情况)
|
||||
var dsResp struct {
|
||||
Result int `json:"result"` // 0-一致 1-不一致 2-无记录(预留)
|
||||
}
|
||||
if err := json.Unmarshal(respBytes, &dsResp); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
state := resultToState(dsResp.Result)
|
||||
|
||||
out := map[string]interface{}{
|
||||
"errMsg": "",
|
||||
"state": state,
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// resultToState 将数据源 result 映射为接口 state:1-匹配 2-不匹配 3-异常情况
|
||||
func resultToState(result int) int {
|
||||
switch result {
|
||||
case 0: // 一致 → 匹配
|
||||
return 1
|
||||
case 1: // 不一致 → 不匹配
|
||||
return 2
|
||||
case 2: // 无记录(预留) → 异常情况
|
||||
return 3
|
||||
default:
|
||||
return 3 // 未知/异常 → 异常情况
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
"tyapi-server/internal/shared/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// ProcessIVYZX5Q2Request IVYZX5Q2 活体识别步骤二API处理方法
|
||||
@@ -44,5 +47,17 @@ func ProcessIVYZX5Q2Request(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
}
|
||||
|
||||
// result==2 时手动抛出错误(不通过/无记录,不返回正常响应)
|
||||
var body struct {
|
||||
Result int `json:"result"`
|
||||
}
|
||||
if err := json.Unmarshal(respBytes, &body); err == nil && body.Result == 2 {
|
||||
log := logger.GetGlobalLogger()
|
||||
log.Warn("IVYZX5Q2 活体检测 result=2 无记录或不通过,返回错误",
|
||||
zap.Int("result", body.Result),
|
||||
zap.ByteString("response", respBytes))
|
||||
return nil, errors.Join(processors.ErrNotFound, errors.New("活体检测 result=2 无记录或不通过"))
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func ProcessIVYZX5QZRequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
reqFormData := map[string]interface{}{
|
||||
"return_url": paramsDto.ReturnURL,
|
||||
"returnUrl": paramsDto.ReturnURL,
|
||||
}
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
)
|
||||
|
||||
// ProcessJRZQ1P5GRequest JRZQ1P5G 全国自然人借贷压力指数查询(2) - xingwei service
|
||||
func ProcessJRZQ1P5GRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQ1P5GReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"name": paramsDto.Name,
|
||||
"idCardNum": paramsDto.IDCard,
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
"authAuthorizeFileCode": paramsDto.AuthAuthorizeFileCode,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1068350101704863744"
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, xingwei.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessJRZQACABERequest JRZQACAB 银行卡四要素 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessJRZQACABERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQACABReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "7eb69f73a855e41875e22f139b934c3c",
|
||||
"name": paramsDto.Name,
|
||||
"idcard": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
"acc_no": paramsDto.BankCard,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/9442"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -58,6 +58,5 @@ func ProcessJRZQDCBERequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
@@ -46,8 +46,6 @@ func ProcessJRZQO6L7Request(ctx context.Context, params []byte, deps *processors
|
||||
"city": null,
|
||||
}
|
||||
|
||||
// 使用 WithSkipCode201Check 不跳过 201 错误检查,当 Code == "201" 时返回错误
|
||||
// ctx = zhicha.WithSkipCode201Check(ctx)
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI081", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessJRZQOCREERequest JRZQOCRE 银行卡OCR API 数卖服务示例
|
||||
func ProcessJRZQOCREERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQOCREReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
if paramsDto.PhotoData == "" && paramsDto.ImageUrl == "" {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("photo_data or image_url is required"))
|
||||
}
|
||||
|
||||
// 2选1:有值的用对应 key,空则用另一个
|
||||
reqFormData := make(map[string]interface{})
|
||||
if paramsDto.PhotoData != "" {
|
||||
reqFormData["image"] = paramsDto.PhotoData
|
||||
} else {
|
||||
reqFormData["url"] = paramsDto.ImageUrl
|
||||
}
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v2/bankcard/ocr" // 接口路径,根据数脉文档填写(如 v4/xxx)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package jrzq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessJRZQOCRYERequest JRZQOCRY 银行卡OCR API 处理方法(使用数据宝服务示例)
|
||||
func ProcessJRZQOCRYERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.JRZQOCRYReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "3ee8e7a7a71870db2c0bf98e7e6b8b5c",
|
||||
"imageId": paramsDto.PhotoData,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/trade/user/1986"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package pdfg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -37,18 +39,19 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
||||
zap.String("id_card", paramsDto.IDCard),
|
||||
zap.String("mobile_no", paramsDto.MobileNo),
|
||||
zap.String("authorized", paramsDto.Authorized),
|
||||
zap.String("auth_authorize_file_code", paramsDto.AuthAuthorizeFileCode),
|
||||
)
|
||||
|
||||
// 从context获取config(如果存在)
|
||||
var cacheTTL time.Duration = 24 * time.Hour
|
||||
var cacheDir string
|
||||
var apiDomain string
|
||||
if cfg, ok := ctx.Value("config").(*config.Config); ok && cfg != nil {
|
||||
cacheTTL = cfg.PDFGen.Cache.TTL
|
||||
if cacheTTL == 0 {
|
||||
cacheTTL = 24 * time.Hour
|
||||
}
|
||||
cacheDir = cfg.PDFGen.Cache.CacheDir
|
||||
apiDomain = cfg.API.Domain
|
||||
}
|
||||
|
||||
// 获取最大缓存大小
|
||||
@@ -93,36 +96,8 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
||||
pdfGenService = pdfgen.NewPDFGenService(defaultCfg, zapLogger)
|
||||
}
|
||||
|
||||
// 检查缓存(基于姓名+身份证)
|
||||
_, hit, createdAt, err := cacheManager.Get(paramsDto.Name, paramsDto.IDCard)
|
||||
if err != nil {
|
||||
zapLogger.Warn("检查缓存失败,继续生成PDF", zap.Error(err))
|
||||
} else if hit {
|
||||
// 计算缓存键,作为报告ID(可持久化)
|
||||
reportID := cacheManager.GetCacheKey(paramsDto.Name, paramsDto.IDCard)
|
||||
|
||||
// 缓存命中,模拟慢几秒
|
||||
zapLogger.Info("PDF缓存命中,返回缓存文件",
|
||||
zap.String("name", paramsDto.Name),
|
||||
zap.String("id_card", paramsDto.IDCard),
|
||||
zap.String("report_id", reportID),
|
||||
zap.Time("created_at", createdAt),
|
||||
)
|
||||
// 模拟慢几秒(2-4秒)
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 生成下载链接(基于报告ID)
|
||||
downloadURL := generateDownloadURL(reportID)
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"download_url": downloadURL,
|
||||
"report_id": reportID,
|
||||
"cached": true,
|
||||
"created_at": createdAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// 缓存未命中,需要生成PDF
|
||||
zapLogger.Info("PDF缓存未命中,开始生成PDF",
|
||||
// 直接生成PDF,不检查缓存(每次都重新生成)
|
||||
zapLogger.Info("开始生成PDF",
|
||||
zap.String("name", paramsDto.Name),
|
||||
zap.String("id_card", paramsDto.IDCard),
|
||||
)
|
||||
@@ -155,9 +130,6 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
||||
reportNumber = generateReportNumber()
|
||||
}
|
||||
|
||||
// 计算报告ID(与缓存键一致,便于通过ID直接下载)
|
||||
reportID := cacheManager.GetCacheKey(paramsDto.Name, paramsDto.IDCard)
|
||||
|
||||
// 构建PDF生成请求
|
||||
pdfReq := &pdfgen.GeneratePDFRequest{
|
||||
Data: formattedData,
|
||||
@@ -180,14 +152,18 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("生成PDF失败: %w", err))
|
||||
}
|
||||
|
||||
// 保存到缓存(基于姓名+身份证)
|
||||
if err := cacheManager.Set(paramsDto.Name, paramsDto.IDCard, pdfResp.PDFBytes); err != nil {
|
||||
// 生成报告ID(每次请求都生成唯一的ID)
|
||||
reportID := generateReportID()
|
||||
|
||||
// 保存到缓存(基于报告ID,文件名包含时间戳确保唯一性)
|
||||
if err := cacheManager.SetByReportID(reportID, pdfResp.PDFBytes); err != nil {
|
||||
zapLogger.Warn("保存PDF到缓存失败", zap.Error(err))
|
||||
// 不影响返回结果,只记录警告
|
||||
}
|
||||
|
||||
// 生成下载链接(基于报告ID)
|
||||
downloadURL := generateDownloadURL(reportID)
|
||||
downloadURL := generateDownloadURL(apiDomain, reportID)
|
||||
expiresAt := time.Now().Add(cacheTTL)
|
||||
|
||||
zapLogger.Info("PDF生成成功",
|
||||
zap.String("name", paramsDto.Name),
|
||||
@@ -201,7 +177,8 @@ func ProcessPDFG01GZRequest(ctx context.Context, params []byte, deps *processors
|
||||
"download_url": downloadURL,
|
||||
"report_id": reportID,
|
||||
"report_number": reportNumber,
|
||||
"cached": false,
|
||||
"expires_at": expiresAt.Format(time.RFC3339),
|
||||
"ttl_seconds": int(cacheTTL.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -219,35 +196,29 @@ func collectAPIData(ctx context.Context, params dto.PDFG01GZReq, deps *processor
|
||||
|
||||
results := make(chan processorResult, 5)
|
||||
|
||||
// 调用IVYZ5A9O - 需要: name, id_card, auth_authorize_file_code
|
||||
// 调用JRZQ0L85 - 需要: name, id_card, mobile_no
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logger.Error("调用IVYZ5A9O处理器时发生panic",
|
||||
logger.Error("调用JRZQ0L85处理器时发生panic",
|
||||
zap.Any("panic", r),
|
||||
)
|
||||
results <- processorResult{"IVYZ5A9O", nil, fmt.Errorf("处理器panic: %v", r)}
|
||||
results <- processorResult{"JRZQ0L85", nil, fmt.Errorf("处理器panic: %v", r)}
|
||||
}
|
||||
}()
|
||||
// 检查必需字段
|
||||
if params.AuthAuthorizeFileCode == "" {
|
||||
logger.Warn("IVYZ5A9O缺少auth_authorize_file_code字段,跳过调用")
|
||||
results <- processorResult{"IVYZ5A9O", nil, fmt.Errorf("缺少必需字段: auth_authorize_file_code")}
|
||||
return
|
||||
jrzq0l85Params := map[string]interface{}{
|
||||
"name": params.Name,
|
||||
"id_card": params.IDCard,
|
||||
"mobile_no": params.MobileNo,
|
||||
}
|
||||
ivyzParams := map[string]interface{}{
|
||||
"name": params.Name,
|
||||
"id_card": params.IDCard,
|
||||
"auth_authorize_file_code": params.AuthAuthorizeFileCode,
|
||||
}
|
||||
paramsBytes, err := json.Marshal(ivyzParams)
|
||||
paramsBytes, err := json.Marshal(jrzq0l85Params)
|
||||
if err != nil {
|
||||
logger.Warn("序列化IVYZ5A9O参数失败", zap.Error(err))
|
||||
results <- processorResult{"IVYZ5A9O", nil, err}
|
||||
logger.Warn("序列化JRZQ0L85参数失败", zap.Error(err))
|
||||
results <- processorResult{"JRZQ0L85", nil, err}
|
||||
return
|
||||
}
|
||||
data, err := callProcessor(ctx, "IVYZ5A9O", paramsBytes, deps)
|
||||
results <- processorResult{"IVYZ5A9O", data, err}
|
||||
data, err := callProcessor(ctx, "JRZQ0L85", paramsBytes, deps)
|
||||
results <- processorResult{"JRZQ0L85", data, err}
|
||||
}()
|
||||
|
||||
// 调用JRZQ8A2D - 需要: name, id_card, mobile_no, authorized
|
||||
@@ -421,17 +392,17 @@ func formatDataForPDF(apiData map[string]interface{}, params dto.PDFG01GZReq, lo
|
||||
},
|
||||
})
|
||||
|
||||
// 2. IVYZ5A9O - 自然人综合风险智能评估模型
|
||||
if data, ok := apiData["IVYZ5A9O"]; ok && data != nil {
|
||||
// 2. JRZQ0L85 - 自然人综合风险智能评估模型(替代原IVYZ5A9O)
|
||||
if data, ok := apiData["JRZQ0L85"]; ok && data != nil {
|
||||
result = append(result, map[string]interface{}{
|
||||
"apiID": "IVYZ5A9O",
|
||||
"apiID": "JRZQ0L85",
|
||||
"data": data,
|
||||
})
|
||||
} else {
|
||||
// 子处理器失败或无数据时,返回空对象 {}
|
||||
logger.Debug("IVYZ5A9O数据缺失,使用空对象")
|
||||
logger.Debug("JRZQ0L85数据缺失,使用空对象")
|
||||
result = append(result, map[string]interface{}{
|
||||
"apiID": "IVYZ5A9O",
|
||||
"apiID": "JRZQ0L85",
|
||||
"data": map[string]interface{}{},
|
||||
})
|
||||
}
|
||||
@@ -500,10 +471,29 @@ func generateReportNumber() string {
|
||||
return fmt.Sprintf("RPT%s", time.Now().Format("20060102150405"))
|
||||
}
|
||||
|
||||
// generateDownloadURL 生成下载链接(基于报告ID/缓存键)
|
||||
func generateDownloadURL(reportID string) string {
|
||||
// 这里应该生成实际的下载URL
|
||||
// 暂时返回一个占位符,实际应该根据服务器配置生成
|
||||
return fmt.Sprintf("/api/v1/pdfg/download?id=%s", reportID)
|
||||
// generateReportID 生成对外可见的报告ID
|
||||
// 每次请求都生成唯一的ID,格式:{report_number}-{随机字符串}
|
||||
// 注意:不再包含cacheKey,因为每次请求都会重新生成,不需要通过ID定位缓存文件
|
||||
func generateReportID() string {
|
||||
reportNumber := generateReportNumber()
|
||||
// 生成8字节随机字符串,确保每次请求ID都不同
|
||||
randomBytes := make([]byte, 8)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
// 如果随机数生成失败,使用纳秒时间戳作为后备
|
||||
randomBytes = []byte(fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||
}
|
||||
randomStr := hex.EncodeToString(randomBytes)
|
||||
return fmt.Sprintf("%s-%s", reportNumber, randomStr)
|
||||
}
|
||||
|
||||
// generateDownloadURL 生成下载链接(基于报告ID/缓存键)
|
||||
// apiDomain: 外部可访问的API域名,如 api.tianyuanapi.com
|
||||
func generateDownloadURL(apiDomain, reportID string) string {
|
||||
if apiDomain == "" {
|
||||
// 兜底:保留相对路径,方便本地/测试环境使用
|
||||
return fmt.Sprintf("/api/v1/pdfg/download?id=%s", reportID)
|
||||
}
|
||||
// 生成完整链接,例如:https://api.tianyuanapi.com/api/v1/pdfg/download?id=xxx
|
||||
return fmt.Sprintf("https://%s/api/v1/pdfg/download?id=%s", apiDomain, reportID)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXG3B8ZRequest QCXG3B8Z 疑似运营车辆查询(月度里程)10268 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXG3B8ZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG3B8ZReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "c94605174cfe29bb2a62e2600b7d1596",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10268"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXG3M7ZRequest QCXG3M7Z 人车关系核验(ETC)10093 月更 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXG3M7ZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG3M7ZReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "a2f32fc54b44ebc85b97a2aaff1734ec",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
"name": paramsDto.Name,
|
||||
"plateColor": paramsDto.PlateColor,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10093"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXG5U0ZRequest QCXG5U0Z 车辆静态信息查询 10479 API 处理方法(使用数据宝服务)
|
||||
func ProcessQCXG5U0ZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG5U0ZReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "7c8122677476dd2621f574976f1a9fde",
|
||||
"vinList": paramsDto.VinCode,
|
||||
}
|
||||
|
||||
apiPath := "/communication/personal/10479"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func ProcessQCXG7A2BRequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"cardNo": paramsDto.IDCard,
|
||||
"cardNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
respBytes, err := deps.YushanService.CallAPI(ctx, "CAR061", reqData)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXG9F5CERequest QCXG9F5C 疑似营运车辆注册平台数 10386 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXG9F5CERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXG9F5CReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "27ab7048dda23d9a56178a2e5d4300ec",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10386"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXGM7R9Request QCXGM7R9 疑似运营车辆查询(半年度里程)10270 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXGM7R9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGM7R9Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "fc335ea4308add7454ac0858b08bef72",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10270"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXGP1W3Request QCXGP1W3 疑似运营车辆查询(季度里程)10269 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXGP1W3Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGP1W3Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "ecd6f3485322b0c706fc1dce330fe26e",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10269"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXGU2K4Request QCXGU2K4 疑似运营车辆查询(年度里程)10271 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQCXGU2K4Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGU2K4Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "8c02f9c755b37b5a1bd39fc6ac9569d6",
|
||||
"carNo": paramsDto.PlateNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10271"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package qcxg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQCXGY7F2Request QCXGY7F2 二手车VIN估值 10443 API 处理方法(使用数据宝服务)
|
||||
func ProcessQCXGY7F2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QCXGY7F2Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "463cea654a0a99d5d04c62f98ac882c0",
|
||||
"vin": paramsDto.VinCode,
|
||||
"model_name": paramsDto.VehicleName,
|
||||
"Vehicle_location": paramsDto.VehicleLocation,
|
||||
"firstRegistrationDate": paramsDto.FirstRegistrationdate,
|
||||
"color": paramsDto.Color,
|
||||
}
|
||||
|
||||
apiPath := "/government/traffic/10443"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,19 +22,22 @@ func ProcessQYGL5S1IRequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedEntName, err := deps.ZhichaService.Encrypt(paramsDto.EntName)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
encryptedEntCode, err := deps.ZhichaService.Encrypt(paramsDto.EntCode)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
encryptedEntName, err := deps.ZhichaService.Encrypt(paramsDto.EntName)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
// 按企业名称时传 enterpriseNo(加密名),按统一信用代码时传 enterpriseName(加密代码)
|
||||
reqData := map[string]interface{}{}
|
||||
if paramsDto.EntName != "" {
|
||||
reqData["enterpriseName"] = encryptedEntName
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"enterpriseNo": encryptedEntCode,
|
||||
"enterpriseName": encryptedEntName,
|
||||
if paramsDto.EntCode != "" {
|
||||
reqData["enterpriseNo"] = encryptedEntCode
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI088", reqData)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQYGLJ0Q1Request QYGLJ0Q1 企业股权结构全景查询 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQYGLJ0Q1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGLJ0Q1Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 二选一:企业名称(entName) 与 统一社会信用代码(creditCode) 必须且仅能传其一
|
||||
hasEntName := paramsDto.EntName != ""
|
||||
hasEntCode := paramsDto.EntCode != ""
|
||||
if hasEntName == hasEntCode { // 两个都填或两个都未填
|
||||
return nil, errors.Join(processors.ErrInvalidParam, errors.New("ent_name 与 ent_code 二选一,必须且仅能传其中一个"))
|
||||
}
|
||||
|
||||
// 构建数据宝入参(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "adac456f7b4ced764b606c8b07fed4d3",
|
||||
}
|
||||
if hasEntName {
|
||||
reqParams["entName"] = paramsDto.EntName
|
||||
} else {
|
||||
reqParams["creditCode"] = paramsDto.EntCode
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10216"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 解析响应中的 JSON 字符串(使用 qyglb4c0 中的 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(parsedResp)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
)
|
||||
|
||||
// ProcessQYGLJ1U9Request 企业全景报告处理器:并发调用企业全量(QYGLUY3S)、股权全景(QYGLJ0Q1)、司法涉诉(QYGL5S1I),
|
||||
// 然后复用 qyglj1u9_processor_build.go 中的 buildReport / map* 逻辑生成企业报告结构
|
||||
func ProcessQYGLJ1U9Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
// 复用 QYGLUY3S 的入参结构:企业名称/注册号/统一社会信用代码
|
||||
var p dto.QYGLJ1U9Req
|
||||
if err := json.Unmarshal(params, &p); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
if err := deps.Validator.ValidateStruct(p); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 并发调用三个已有处理器
|
||||
type apiResult struct {
|
||||
key string
|
||||
data map[string]interface{}
|
||||
err error
|
||||
}
|
||||
resultsCh := make(chan apiResult, 3)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
call := func(key string, req interface{}, fn func(context.Context, []byte, *processors.ProcessorDependencies) ([]byte, error)) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
b, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
resultsCh <- apiResult{key: key, err: err}
|
||||
return
|
||||
}
|
||||
resp, err := fn(ctx, b, deps)
|
||||
if err != nil {
|
||||
resultsCh <- apiResult{key: key, err: err}
|
||||
return
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(resp, &m); err != nil {
|
||||
resultsCh <- apiResult{key: key, err: err}
|
||||
return
|
||||
}
|
||||
resultsCh <- apiResult{key: key, data: m}
|
||||
}()
|
||||
}
|
||||
|
||||
// 企业全量信息核验V2(QYGLUY3S)
|
||||
call("jiguangFull", map[string]interface{}{
|
||||
"ent_name": p.EntName,
|
||||
"ent_code": p.EntCode,
|
||||
}, ProcessQYGLUY3SRequest)
|
||||
|
||||
// 企业股权结构全景(QYGLJ0Q1)
|
||||
call("equityPanorama", map[string]interface{}{
|
||||
"ent_name": p.EntName,
|
||||
}, ProcessQYGLJ0Q1Request)
|
||||
|
||||
// 企业司法涉诉V2(QYGL5S1I)
|
||||
call("judicialCertFull", map[string]interface{}{
|
||||
"ent_name": p.EntName,
|
||||
"ent_code": p.EntCode,
|
||||
}, ProcessQYGL5S1IRequest)
|
||||
|
||||
wg.Wait()
|
||||
close(resultsCh)
|
||||
|
||||
var jiguang, judicial, equity map[string]interface{}
|
||||
for r := range resultsCh {
|
||||
if r.err != nil {
|
||||
// 任一关键数据源异常,则返回系统错误(也可以根据需求做降级)
|
||||
return nil, errors.Join(processors.ErrSystem, fmt.Errorf("%s 调用失败: %w", r.key, r.err))
|
||||
}
|
||||
switch r.key {
|
||||
case "jiguangFull":
|
||||
jiguang = r.data
|
||||
case "judicialCertFull":
|
||||
judicial = r.data
|
||||
case "equityPanorama":
|
||||
equity = r.data
|
||||
}
|
||||
}
|
||||
if jiguang == nil {
|
||||
jiguang = map[string]interface{}{}
|
||||
}
|
||||
if judicial == nil {
|
||||
judicial = map[string]interface{}{}
|
||||
}
|
||||
if equity == nil {
|
||||
equity = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// 复用构建逻辑生成企业报告结构
|
||||
report := buildReport(jiguang, judicial, equity)
|
||||
|
||||
// 为报告生成唯一编号并缓存,供后续通过编号查看
|
||||
reportID := saveQYGLReport(report)
|
||||
report["reportId"] = reportID
|
||||
|
||||
// 持久化企业报告记录到数据库(忽略持久化失败,不影响接口主流程)
|
||||
if deps.ReportRepo != nil {
|
||||
reqJSON, _ := json.Marshal(p)
|
||||
reportJSON, _ := json.Marshal(report)
|
||||
_ = deps.ReportRepo.Create(ctx, &entities.Report{
|
||||
ReportID: reportID,
|
||||
Type: "enterprise",
|
||||
ApiCode: "QYGLJ1U9",
|
||||
EntName: p.EntName,
|
||||
EntCode: p.EntCode,
|
||||
RequestParams: string(reqJSON),
|
||||
ReportData: string(reportJSON),
|
||||
})
|
||||
}
|
||||
// 为报告补充前端查看链接,供调用方直接跳转到企业报告页面(通过编号访问)
|
||||
report["reportUrl"] = buildQYGLReportURLByID(reportID)
|
||||
|
||||
out, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// 内存中的企业报告缓存(简单实现,进程重启后清空)
|
||||
var qyglReportStore = struct {
|
||||
sync.RWMutex
|
||||
data map[string]map[string]interface{}
|
||||
}{
|
||||
data: make(map[string]map[string]interface{}),
|
||||
}
|
||||
|
||||
// saveQYGLReport 保存报告并返回生成的编号
|
||||
func saveQYGLReport(report map[string]interface{}) string {
|
||||
id := generateQYGLReportID()
|
||||
qyglReportStore.Lock()
|
||||
qyglReportStore.data[id] = report
|
||||
qyglReportStore.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
// GetQYGLReport 根据编号获取报告(供页面渲染使用)
|
||||
func GetQYGLReport(id string) (map[string]interface{}, bool) {
|
||||
qyglReportStore.RLock()
|
||||
defer qyglReportStore.RUnlock()
|
||||
r, ok := qyglReportStore.data[id]
|
||||
return r, ok
|
||||
}
|
||||
|
||||
// generateQYGLReportID 生成短编号
|
||||
func generateQYGLReportID() string {
|
||||
b := make([]byte, 8)
|
||||
if _, err := rand.Read(b); err == nil {
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
// 随机数失败时退化为时间戳
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// buildQYGLReportURLByID 构造企业报告前端查看链接(通过编号查看)
|
||||
func buildQYGLReportURLByID(id string) string {
|
||||
return "https://api.tianyuanapi.com/reports/qygl/" + url.PathEscape(id)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
sharedvalidator "tyapi-server/internal/shared/validator"
|
||||
)
|
||||
|
||||
// TestQYGLJ1U9Req_ValidateParams 仅验证 QYGLJ1U9 入参的校验规则(特别是 validUSCI)。
|
||||
func TestQYGLJ1U9Req_ValidateParams(t *testing.T) {
|
||||
// 使用全局业务校验器
|
||||
bv := sharedvalidator.NewBusinessValidator()
|
||||
|
||||
t.Run("invalid_usci_should_fail", func(t *testing.T) {
|
||||
req := dto.QYGLJ1U9Req{
|
||||
EntName: "测试企业有限公司",
|
||||
EntCode: "123", // 明显不符合 validUSCI
|
||||
}
|
||||
if err := bv.ValidateStruct(req); err == nil {
|
||||
t.Fatalf("expected validation error for invalid ent_code, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("valid_usci_should_pass", func(t *testing.T) {
|
||||
req := dto.QYGLJ1U9Req{
|
||||
EntName: "杭州娃哈哈集团有限公司",
|
||||
EntCode: "91330000142916567N", // 符合 validUSCI 正则的示例
|
||||
}
|
||||
if err := bv.ValidateStruct(req); err != nil {
|
||||
t.Fatalf("expected no validation error for valid ent_code, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package qygl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessQYGLUY3SRequest QYGLUY3S 企业全量信息核验V2 可用 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessQYGLUY3SRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.QYGLUY3SReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "5131227a847c06c111f624a22ebacc06",
|
||||
"entName": paramsDto.EntName,
|
||||
"regno": paramsDto.EntRegno,
|
||||
"creditcode": paramsDto.EntCode,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/10195"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// 解析响应中的 JSON 字符串(使用 qyglb4c0 中的 RecursiveParse)
|
||||
parsedResp, err := RecursiveParse(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(parsedResp)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package yysy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessYYSY35TARequest YYSY35TA API 运营商归属地数卖处理方法数脉
|
||||
func ProcessYYSY35TARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSY35TAReq
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/phone/number" // 接口路径,根据数脉文档填写(如 v4/xxx)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrNotFound) {
|
||||
// 查无记录情况
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessYYSY3E7FRequest YYSY3E7F API处理方法 - 空号检测
|
||||
@@ -20,30 +20,25 @@ func ProcessYYSY3E7FRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"phone": encryptedMobileNo,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI010", reqData)
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/mobile_empty/check" // 接口路径,根据数脉文档填写(
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
|
||||
@@ -4,12 +4,21 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// shumaiMobileTransferResp 数脉 /v4/mobile-transfer/query 返回结构
|
||||
type shumaiMobileTransferResp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Channel string `json:"channel"` // 移动/电信/联通
|
||||
Status int `json:"status"` // 0-在网 1-不在网
|
||||
Desc string `json:"desc"` // 不在网原因(status=1时有效)
|
||||
}
|
||||
|
||||
// ProcessYYSY6D9ARequest YYSY6D9A API处理方法 - 全网手机号状态验证A
|
||||
func ProcessYYSY6D9ARequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSY6D9AReq
|
||||
@@ -21,29 +30,72 @@ func ProcessYYSY6D9ARequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"phone": encryptedMobileNo,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI030", reqData)
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/mobile-transfer/query"
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
if errors.Is(err, shumai.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
mapped, err := mapShumaiToYYSY6D9A(respBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
return json.Marshal(mapped)
|
||||
}
|
||||
|
||||
// mapShumaiToYYSY6D9A 将数脉 mobile-transfer 响应映射为最终 state/operators 格式
|
||||
// state: 1-正常 2-不在网(空号) 3-无短信能力 4-欠费 5-长时间关机 6-关机 7-通话中 -1-查询失败
|
||||
// operators: 1-移动 2-联通 3-电信
|
||||
func mapShumaiToYYSY6D9A(dataBytes []byte) (map[string]string, error) {
|
||||
var r shumaiMobileTransferResp
|
||||
if err := json.Unmarshal(dataBytes, &r); err != nil {
|
||||
return map[string]string{"state": "-1", "operators": ""}, nil // 解析失败视为查询失败
|
||||
}
|
||||
|
||||
operators := ispNameToCode(strings.TrimSpace(r.Channel))
|
||||
state := statusDescToState(r.Status, r.Desc)
|
||||
|
||||
return map[string]string{
|
||||
"state": state,
|
||||
"operators": operators,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// statusDescToState status: 0-在网 1-不在网;desc 为不在网原因
|
||||
func statusDescToState(status int, desc string) string {
|
||||
if status == 0 {
|
||||
return "1" // 正常
|
||||
}
|
||||
// status == 1 不在网,根据 desc 推断 state
|
||||
d := strings.TrimSpace(desc)
|
||||
if strings.Contains(d, "销号") || strings.Contains(d, "空号") {
|
||||
return "2" // 不在网(空号)
|
||||
}
|
||||
if strings.Contains(d, "无短信") || strings.Contains(d, "在网不可用") {
|
||||
return "3" // 无短信能力
|
||||
}
|
||||
if strings.Contains(d, "欠费") {
|
||||
return "4" // 欠费
|
||||
}
|
||||
if strings.Contains(d, "长时间关机") {
|
||||
return "5" // 长时间关机
|
||||
}
|
||||
if strings.Contains(d, "关机") {
|
||||
return "6" // 关机
|
||||
}
|
||||
if strings.Contains(d, "通话中") {
|
||||
return "7" // 通话中
|
||||
}
|
||||
return "2" // 不在网但未明确原因,默认空号
|
||||
}
|
||||
|
||||
@@ -4,12 +4,35 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/xingwei"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// shumaiMobileTransferResp 数脉 /v4/mobile-transfer/query 携号转网返回结构
|
||||
type shumaiMobileTransfer7d3eResp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Mobile string `json:"mobile"`
|
||||
Area *string `json:"area"`
|
||||
IspType string `json:"ispType"` // 转网前运营商
|
||||
NewIspType string `json:"newIspType"` // 转网后运营商
|
||||
}
|
||||
|
||||
// yysy7d3eResp 携号转网查询对外响应结构
|
||||
type yysy7d3eResp struct {
|
||||
BatchNo string `json:"batchNo"`
|
||||
QueryResult []yysy7d3eQueryItem `json:"queryResult"`
|
||||
}
|
||||
|
||||
type yysy7d3eQueryItem struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Result string `json:"result"` // 0:否 1:是
|
||||
After string `json:"after"` // 转网后:-1未知 1移动 2联通 3电信 4广电
|
||||
Before string `json:"before"` // 转网前:同上
|
||||
}
|
||||
|
||||
// ProcessYYSY7D3ERequest YYSY7D3E API处理方法 - 携号转网查询
|
||||
func ProcessYYSY7D3ERequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSY7D3EReq
|
||||
@@ -21,25 +44,70 @@ func ProcessYYSY7D3ERequest(ctx context.Context, params []byte, deps *processors
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建请求数据,将项目规范的字段名转换为 XingweiService 需要的字段名
|
||||
reqData := map[string]interface{}{
|
||||
"phoneNumber": paramsDto.MobileNo,
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
// 调用行为数据API,使用指定的project_id
|
||||
projectID := "CDJ-1100244706893164544"
|
||||
respBytes, err := deps.XingweiService.CallAPI(ctx, projectID, reqData)
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v4/mobile-transfer/query"
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, xingwei.ErrNotFound) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
} else if errors.Is(err, xingwei.ErrDatasource) {
|
||||
if errors.Is(err, shumai.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, xingwei.ErrSystem) {
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
mapped, err := mapShumaiToYYSY7D3E(respBytes)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return json.Marshal(mapped)
|
||||
}
|
||||
|
||||
// mapShumaiToYYSY7D3E 将数脉携号转网响应映射为 batchNo + queryResult 格式
|
||||
func mapShumaiToYYSY7D3E(dataBytes []byte) (*yysy7d3eResp, error) {
|
||||
var r shumaiMobileTransfer7d3eResp
|
||||
if err := json.Unmarshal(dataBytes, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
before := ispNameToCodeTransfer(strings.TrimSpace(r.IspType))
|
||||
after := ispNameToCodeTransfer(strings.TrimSpace(r.NewIspType))
|
||||
result := "0"
|
||||
if r.IspType != "" && r.NewIspType != "" && strings.TrimSpace(r.IspType) != strings.TrimSpace(r.NewIspType) {
|
||||
result = "1" // 转网前与转网后不同即为携号转网
|
||||
}
|
||||
|
||||
out := &yysy7d3eResp{
|
||||
BatchNo: r.OrderNo,
|
||||
QueryResult: []yysy7d3eQueryItem{
|
||||
{
|
||||
Mobile: r.Mobile,
|
||||
Result: result,
|
||||
After: after,
|
||||
Before: before,
|
||||
},
|
||||
},
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ispNameToCodeTransfer 运营商名称转编码:-1未知 1移动 2联通 3电信 4广电
|
||||
func ispNameToCodeTransfer(name string) string {
|
||||
switch name {
|
||||
case "移动":
|
||||
return "1"
|
||||
case "联通":
|
||||
return "2"
|
||||
case "电信":
|
||||
return "3"
|
||||
case "广电":
|
||||
return "4"
|
||||
default:
|
||||
return "-1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessYYSY8B1CRequest YYSY8B1C API处理方法 - 手机在网时长
|
||||
@@ -20,30 +20,81 @@ func ProcessYYSY8B1CRequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"phone": encryptedMobileNo,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI003", reqData)
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
apiPath := "/v2/mobile_online/check" // 接口路径,根据数脉文档填写(如 v4/xxx)
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
if errors.Is(err, shumai.ErrDatasource) {
|
||||
// 数据源错误
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
// 系统错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
// 其他未知错误
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
var shumaiResp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Channel string `json:"channel"` // cmcc/cucc/ctcc/gdcc
|
||||
Time string `json:"time"` // [0,3),[3,6),[6,12),[12,24),[24,-1)
|
||||
}
|
||||
if err := json.Unmarshal(respBytes, &shumaiResp); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
// 映射 channel -> operators
|
||||
operators := channelToOperators(shumaiResp.Channel)
|
||||
// 映射 time 区间 -> inTime
|
||||
inTime := timeIntervalToInTime(shumaiResp.Time)
|
||||
|
||||
out := map[string]string{
|
||||
"inTime": inTime,
|
||||
"operators": operators,
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// channelToOperators 运营商编码转名称:cmcc-移动 cucc-联通 ctcc-电信 gdcc-广电
|
||||
func channelToOperators(channel string) string {
|
||||
switch channel {
|
||||
case "cmcc":
|
||||
return "移动"
|
||||
case "cucc":
|
||||
return "联通"
|
||||
case "ctcc":
|
||||
return "电信"
|
||||
case "gdcc":
|
||||
return "广电"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// timeIntervalToInTime 在网时间区间转 inTime 值
|
||||
// [0,3)->0, [3,6)->3, [6,12)->6, [12,24)->12, [24,-1)->24
|
||||
// 空或异常->99, 查无记录->-1(此处按空/未知处理为99)
|
||||
func timeIntervalToInTime(timeInterval string) string {
|
||||
switch timeInterval {
|
||||
case "[0,3)":
|
||||
return "0"
|
||||
case "[3,6)":
|
||||
return "3"
|
||||
case "[6,12)":
|
||||
return "6"
|
||||
case "[12,24)":
|
||||
return "12"
|
||||
case "[24,-1)":
|
||||
return "24"
|
||||
case "":
|
||||
return "-1"
|
||||
default:
|
||||
return "99"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,14 @@ func ProcessYYSY9E4ARequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容上游有时返回 JSON 字符串的情况:如果是字符串则尝试再反序列化一次
|
||||
if str, ok := respData.(string); ok && str != "" {
|
||||
var parsed interface{}
|
||||
if err := json.Unmarshal([]byte(str), &parsed); err == nil {
|
||||
respData = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
if err != nil {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/zhicha"
|
||||
"tyapi-server/internal/infrastructure/external/shumai"
|
||||
)
|
||||
|
||||
// ProcessYYSY9F1BYequest YYSY9F1B API处理方法 - 手机二要素验证
|
||||
@@ -21,37 +21,94 @@ func ProcessYYSY9F1BYequest(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
"name": paramsDto.Name,
|
||||
}
|
||||
|
||||
encryptedMobileNo, err := deps.ZhichaService.Encrypt(paramsDto.MobileNo)
|
||||
// 3m8s 运营商二要素:order_no, fee, result(0-一致 1-不一致)。失败则直接返回,不再调携号转网接口。
|
||||
apiPath := "/v4/mobile_two/check"
|
||||
respBytes, err := deps.ShumaiService.CallAPIForm(ctx, apiPath, reqFormData)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"phone": encryptedMobileNo,
|
||||
"authorized": paramsDto.Authorized,
|
||||
}
|
||||
|
||||
respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI040", reqData)
|
||||
if err != nil {
|
||||
if errors.Is(err, zhicha.ErrDatasource) {
|
||||
if errors.Is(err, shumai.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将响应数据转换为JSON字节
|
||||
respBytes, err := json.Marshal(respData)
|
||||
// s9w1 手机携号转网(仅在上方二要素成功后再调)
|
||||
apiPath2 := "/v4/mobile-transfer/query"
|
||||
respBytes2, err := deps.ShumaiService.CallAPIForm(ctx, apiPath2, reqFormData)
|
||||
if err != nil {
|
||||
if errors.Is(err, shumai.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
} else if errors.Is(err, shumai.ErrSystem) {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
} else {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
}
|
||||
|
||||
var twoFactorResp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Fee int `json:"fee"`
|
||||
Result int `json:"result"` // 0-一致 1-不一致
|
||||
}
|
||||
if err := json.Unmarshal(respBytes, &twoFactorResp); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
var transferResp struct {
|
||||
OrderNo string `json:"order_no"`
|
||||
Mobile string `json:"mobile"`
|
||||
Area *string `json:"area"`
|
||||
IspType string `json:"ispType"` // 转网前运营商
|
||||
NewIspType string `json:"newIspType"` // 转网后运营商
|
||||
}
|
||||
if err := json.Unmarshal(respBytes2, &transferResp); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
// state: 1-一致 2-不一致 3-异常
|
||||
state := "3"
|
||||
switch twoFactorResp.Result {
|
||||
case 0:
|
||||
state = "1"
|
||||
case 1:
|
||||
state = "2"
|
||||
}
|
||||
|
||||
operator := ispNameToCode(transferResp.IspType)
|
||||
operatorReal := ispNameToCode(transferResp.NewIspType)
|
||||
|
||||
// is_xhzw: 0-否 1-是(转网前与转网后运营商不同即为携号转网)
|
||||
isXhzw := "0"
|
||||
if transferResp.IspType != "" && transferResp.NewIspType != "" && transferResp.IspType != transferResp.NewIspType {
|
||||
isXhzw = "1"
|
||||
}
|
||||
|
||||
out := map[string]string{
|
||||
"operator_real": operatorReal,
|
||||
"state": state,
|
||||
"is_xhzw": isXhzw,
|
||||
"operator": operator,
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// ispNameToCode 运营商名称转编码:1-移动 2-联通 3-电信
|
||||
func ispNameToCode(name string) string {
|
||||
switch name {
|
||||
case "移动":
|
||||
return "1"
|
||||
case "联通":
|
||||
return "2"
|
||||
case "电信":
|
||||
return "3"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
@@ -20,9 +21,12 @@ func ProcessYYSYF2T7Request(ctx context.Context, params []byte, deps *processors
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
// 从入参 date_range(YYYYMMDD-YYYYMMDD)提取右区间作为 date
|
||||
parts := strings.SplitN(paramsDto.DateRange, "-", 2)
|
||||
dateEnd := strings.TrimSpace(parts[1]) // 校验已保证格式正确,取结束日期
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
"date": paramsDto.DateRange,
|
||||
"date": dateEnd,
|
||||
}
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
|
||||
@@ -48,11 +48,11 @@ func ProcessYYSYF7DBRequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
// 组装日期:开始日期 + 当前日期(YYYYMMDD-YYYYMMDD)
|
||||
today := time.Now().Format("20060102")
|
||||
dateRange := paramsDto.StartDate + "-" + today
|
||||
// dateRange := startDateYyyymmdd + "-" + today
|
||||
|
||||
reqFormData := map[string]interface{}{
|
||||
"mobile": paramsDto.MobileNo,
|
||||
"date": dateRange,
|
||||
"date": today,
|
||||
}
|
||||
|
||||
// 以表单方式调用数脉 API;参数在 CallAPIForm 内转为 application/x-www-form-urlencoded
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package yysy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/infrastructure/external/shujubao"
|
||||
)
|
||||
|
||||
// ProcessYYSYK9R4Request JRZQACAB 全网手机三要素验证1979周更新版 API 处理方法(使用数据宝服务示例)
|
||||
func ProcessYYSYK9R4Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var paramsDto dto.YYSYK9R4Req
|
||||
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 构建数据宝入参:姓名、身份证、手机号、银行卡号(sign 外的业务参数可按需 AES 加密后作为 bodyData)
|
||||
reqParams := map[string]interface{}{
|
||||
"key": "c115708d915451da8f34a23e144dda6b",
|
||||
"name": paramsDto.Name,
|
||||
"idcard": paramsDto.IDCard,
|
||||
"mobile": paramsDto.MobileNo,
|
||||
}
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 personal/197
|
||||
apiPath := "/communication/personal/1979"
|
||||
data, err := deps.ShujubaoService.CallAPI(ctx, apiPath, reqParams)
|
||||
if err != nil {
|
||||
if errors.Is(err, shujubao.ErrDatasource) {
|
||||
return nil, errors.Join(processors.ErrDatasource, err)
|
||||
}
|
||||
if errors.Is(err, shujubao.ErrQueryEmpty) {
|
||||
return nil, errors.Join(processors.ErrNotFound, err)
|
||||
}
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
respBytes, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
return respBytes, nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"go.uber.org/zap"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||
user_repositories "tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/infrastructure/external/notification"
|
||||
"tyapi-server/internal/infrastructure/external/sms"
|
||||
)
|
||||
|
||||
@@ -27,6 +29,7 @@ type BalanceAlertServiceImpl struct {
|
||||
smsService *sms.AliSMSService
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
wechatWorkService *notification.WeChatWorkService
|
||||
}
|
||||
|
||||
// NewBalanceAlertService 创建余额预警服务
|
||||
@@ -38,6 +41,10 @@ func NewBalanceAlertService(
|
||||
config *config.Config,
|
||||
logger *zap.Logger,
|
||||
) BalanceAlertService {
|
||||
var wechatSvc *notification.WeChatWorkService
|
||||
if config != nil && config.WechatWork.WebhookURL != "" {
|
||||
wechatSvc = notification.NewWeChatWorkService(config.WechatWork.WebhookURL, config.WechatWork.Secret, logger)
|
||||
}
|
||||
return &BalanceAlertServiceImpl{
|
||||
apiUserRepo: apiUserRepo,
|
||||
userRepo: userRepo,
|
||||
@@ -45,6 +52,7 @@ func NewBalanceAlertService(
|
||||
smsService: smsService,
|
||||
config: config,
|
||||
logger: logger,
|
||||
wechatWorkService: wechatSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +162,27 @@ func (s *BalanceAlertServiceImpl) sendArrearsAlert(ctx context.Context, apiUser
|
||||
zap.Float64("balance", balance),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", enterpriseName)
|
||||
if err := s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, 0, "arrears", enterpriseName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 企业微信欠费告警通知(仅展示企业名称和联系手机)
|
||||
if s.wechatWorkService != nil {
|
||||
content := fmt.Sprintf(
|
||||
"### 【天远API】用户余额欠费告警\n"+
|
||||
"<font color=\"warning\">该企业已发生欠费,请及时联系并处理。</font>\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 当前余额:%.2f 元\n"+
|
||||
"> 时间:%s\n",
|
||||
enterpriseName,
|
||||
apiUser.AlertPhone,
|
||||
balance,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkService.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sendLowBalanceAlert 发送低余额预警
|
||||
@@ -182,5 +210,27 @@ func (s *BalanceAlertServiceImpl) sendLowBalanceAlert(ctx context.Context, apiUs
|
||||
zap.Float64("threshold", apiUser.BalanceAlertThreshold),
|
||||
zap.String("enterprise_name", enterpriseName))
|
||||
|
||||
return s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", enterpriseName)
|
||||
if err := s.smsService.SendBalanceAlert(ctx, apiUser.AlertPhone, balance, apiUser.BalanceAlertThreshold, "low_balance", enterpriseName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 企业微信余额预警通知(仅展示企业名称和联系手机)
|
||||
if s.wechatWorkService != nil {
|
||||
content := fmt.Sprintf(
|
||||
"### 【天远API】用户余额预警\n"+
|
||||
"<font color=\"warning\">用户余额已低于预警阈值,请及时跟进。</font>\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 当前余额:%.2f 元\n"+
|
||||
"> 预警阈值:%.2f 元\n"+
|
||||
"> 时间:%s\n",
|
||||
enterpriseName,
|
||||
apiUser.AlertPhone,
|
||||
balance,
|
||||
apiUser.BalanceAlertThreshold,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
)
|
||||
_ = s.wechatWorkService.SendMarkdownMessage(ctx, content)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// calculateAlipayRechargeBonus 计算支付宝充值赠送金额
|
||||
// calculateAlipayRechargeBonus 计算支付宝充值赠送金额(受 recharge_bonus_enabled 开关控制)
|
||||
func calculateAlipayRechargeBonus(rechargeAmount decimal.Decimal, walletConfig *config.WalletConfig) decimal.Decimal {
|
||||
if walletConfig == nil || len(walletConfig.AliPayRechargeBonus) == 0 {
|
||||
if walletConfig == nil || !walletConfig.RechargeBonusEnabled || len(walletConfig.AliPayRechargeBonus) == 0 {
|
||||
return decimal.Zero
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
)
|
||||
|
||||
func TestCalculateAlipayRechargeBonus(t *testing.T) {
|
||||
// 创建测试配置
|
||||
// 创建测试配置(开启赠送)
|
||||
walletConfig := &config.WalletConfig{
|
||||
RechargeBonusEnabled: true,
|
||||
AliPayRechargeBonus: []config.AliPayRechargeBonusRule{
|
||||
{RechargeAmount: 1000.00, BonusAmount: 50.00}, // 充1000送50
|
||||
{RechargeAmount: 5000.00, BonusAmount: 300.00}, // 充5000送300
|
||||
@@ -74,6 +75,7 @@ func TestCalculateAlipayRechargeBonus(t *testing.T) {
|
||||
func TestCalculateAlipayRechargeBonus_EmptyConfig(t *testing.T) {
|
||||
// 测试空配置
|
||||
walletConfig := &config.WalletConfig{
|
||||
RechargeBonusEnabled: true,
|
||||
AliPayRechargeBonus: []config.AliPayRechargeBonusRule{},
|
||||
}
|
||||
|
||||
@@ -85,4 +87,17 @@ func TestCalculateAlipayRechargeBonus_EmptyConfig(t *testing.T) {
|
||||
assert.True(t, bonus.Equal(decimal.Zero), "nil配置应该返回零赠送金额")
|
||||
}
|
||||
|
||||
func TestCalculateAlipayRechargeBonus_Disabled(t *testing.T) {
|
||||
// 关闭赠送时,任意金额均不赠送
|
||||
walletConfig := &config.WalletConfig{
|
||||
RechargeBonusEnabled: false,
|
||||
AliPayRechargeBonus: []config.AliPayRechargeBonusRule{
|
||||
{RechargeAmount: 1000.00, BonusAmount: 50.00},
|
||||
{RechargeAmount: 10000.00, BonusAmount: 800.00},
|
||||
},
|
||||
}
|
||||
bonus := calculateAlipayRechargeBonus(decimal.NewFromFloat(10000.00), walletConfig)
|
||||
assert.True(t, bonus.Equal(decimal.Zero), "关闭赠送时应返回零")
|
||||
}
|
||||
|
||||
|
||||
@@ -10,18 +10,20 @@ import (
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/domains/user/entities"
|
||||
"tyapi-server/internal/domains/user/repositories"
|
||||
"tyapi-server/internal/infrastructure/external/captcha"
|
||||
"tyapi-server/internal/infrastructure/external/sms"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// SMSCodeService 短信验证码服务
|
||||
type SMSCodeService struct {
|
||||
repo repositories.SMSCodeRepository
|
||||
smsClient *sms.AliSMSService
|
||||
cache interfaces.CacheService
|
||||
config config.SMSConfig
|
||||
appConfig config.AppConfig
|
||||
logger *zap.Logger
|
||||
repo repositories.SMSCodeRepository
|
||||
smsClient *sms.AliSMSService
|
||||
cache interfaces.CacheService
|
||||
captchaSvc *captcha.CaptchaService
|
||||
config config.SMSConfig
|
||||
appConfig config.AppConfig
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewSMSCodeService 创建短信验证码服务
|
||||
@@ -29,22 +31,58 @@ func NewSMSCodeService(
|
||||
repo repositories.SMSCodeRepository,
|
||||
smsClient *sms.AliSMSService,
|
||||
cache interfaces.CacheService,
|
||||
captchaSvc *captcha.CaptchaService,
|
||||
config config.SMSConfig,
|
||||
appConfig config.AppConfig,
|
||||
logger *zap.Logger,
|
||||
) *SMSCodeService {
|
||||
return &SMSCodeService{
|
||||
repo: repo,
|
||||
smsClient: smsClient,
|
||||
cache: cache,
|
||||
config: config,
|
||||
appConfig: appConfig,
|
||||
logger: logger,
|
||||
repo: repo,
|
||||
smsClient: smsClient,
|
||||
cache: cache,
|
||||
captchaSvc: captchaSvc,
|
||||
config: config,
|
||||
appConfig: appConfig,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entities.SMSScene, clientIP, userAgent string) error {
|
||||
func (s *SMSCodeService) SendCode(ctx context.Context, phone string, scene entities.SMSScene, clientIP, userAgent, captchaVerifyParam string) error {
|
||||
// 0. 验证滑块验证码(如果启用)
|
||||
if s.config.CaptchaEnabled && s.captchaSvc != nil {
|
||||
if err := s.captchaSvc.Verify(captchaVerifyParam); err != nil {
|
||||
s.logger.Warn("滑块验证码校验失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)),
|
||||
zap.Error(err))
|
||||
return captcha.ErrCaptchaVerifyFailed
|
||||
}
|
||||
}
|
||||
|
||||
// 0.1. 发送前安全限流检查
|
||||
if err := s.CheckRateLimit(ctx, phone, scene, clientIP, userAgent); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 0.1. 检查同一手机号同一场景的1分钟间隔限制
|
||||
canResend, err := s.CanResendCode(ctx, phone, scene)
|
||||
if err != nil {
|
||||
s.logger.Warn("检查验证码重发限制失败",
|
||||
zap.String("phone", phone),
|
||||
zap.String("scene", string(scene)),
|
||||
zap.Error(err))
|
||||
// 检查失败时继续执行,避免影响正常流程
|
||||
} else if !canResend {
|
||||
// 获取最近的验证码记录以计算剩余等待时间
|
||||
recentCode, err := s.repo.GetValidByPhoneAndScene(ctx, phone, scene)
|
||||
if err == nil {
|
||||
remainingTime := s.config.RateLimit.MinInterval - time.Since(recentCode.CreatedAt)
|
||||
return fmt.Errorf("短信发送过于频繁,请等待 %d 秒后重试", int(remainingTime.Seconds())+1)
|
||||
}
|
||||
return fmt.Errorf("短信发送过于频繁,请稍后再试")
|
||||
}
|
||||
|
||||
// 1. 生成验证码
|
||||
code := s.smsClient.GenerateCode(s.config.CodeLength)
|
||||
|
||||
@@ -181,58 +219,68 @@ func (s *SMSCodeService) GetCodeStatus(ctx context.Context, phone string, scene
|
||||
}
|
||||
|
||||
// checkRateLimit 检查发送频率限制
|
||||
func (s *SMSCodeService) CheckRateLimit(ctx context.Context, phone string, scene entities.SMSScene) error {
|
||||
now := time.Now()
|
||||
func (s *SMSCodeService) CheckRateLimit(ctx context.Context, phone string, scene entities.SMSScene, clientIP, userAgent string) error {
|
||||
// 设备标识(这里使用 User-Agent + IP 的组合做近似设备ID,可根据实际情况调整)
|
||||
deviceID := fmt.Sprintf("ua:%s|ip:%s", userAgent, clientIP)
|
||||
phoneBanKey := fmt.Sprintf("sms:ban:phone:%s", phone)
|
||||
// deviceBanKey := fmt.Sprintf("sms:ban:device:%s", deviceID)
|
||||
|
||||
// 检查最小发送间隔
|
||||
lastSentKey := fmt.Sprintf("sms:last_sent:%s:%s", scene, phone)
|
||||
var lastSent time.Time
|
||||
if err := s.cache.Get(ctx, lastSentKey, &lastSent); err == nil {
|
||||
if now.Sub(lastSent) < s.config.RateLimit.MinInterval {
|
||||
return fmt.Errorf("请等待 %v 后再试", s.config.RateLimit.MinInterval)
|
||||
}
|
||||
// 1. 按手机号的时间窗口限流
|
||||
// 10分钟窗口
|
||||
if err := s.checkWindowLimit(ctx, fmt.Sprintf("sms:phone:%s:10m", phone), 10*time.Minute, 10); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查每小时发送限制
|
||||
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
||||
var hourlyCount int
|
||||
if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil {
|
||||
if hourlyCount >= s.config.RateLimit.HourlyLimit {
|
||||
return fmt.Errorf("每小时最多发送 %d 条短信", s.config.RateLimit.HourlyLimit)
|
||||
}
|
||||
// 30分钟窗口
|
||||
if err := s.checkWindowLimit(ctx, fmt.Sprintf("sms:phone:%s:30m", phone), 30*time.Minute, 10); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查每日发送限制
|
||||
dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102"))
|
||||
// 1小时窗口
|
||||
if err := s.checkWindowLimit(ctx, fmt.Sprintf("sms:phone:%s:1h", phone), time.Hour, 20); err != nil {
|
||||
return err
|
||||
}
|
||||
// 1天窗口:超过30次则永久封禁该手机号
|
||||
dailyKey := fmt.Sprintf("sms:phone:%s:1d", phone)
|
||||
var dailyCount int
|
||||
if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil {
|
||||
if dailyCount >= s.config.RateLimit.DailyLimit {
|
||||
return fmt.Errorf("每日最多发送 %d 条短信", s.config.RateLimit.DailyLimit)
|
||||
if dailyCount >= 30 {
|
||||
// 设置手机号永久封禁标记(不过期)
|
||||
s.cache.Set(ctx, phoneBanKey, true, 0)
|
||||
return fmt.Errorf("该手机号短信发送次数异常,已被永久限制")
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 设备维度限流与多IP检测
|
||||
if deviceID != "ua:|ip:" {
|
||||
// 3.1 设备多窗口限流(与手机号一致的窗口参数)
|
||||
if err := s.checkWindowLimit(ctx, fmt.Sprintf("sms:device:%s:10m", deviceID), 10*time.Minute, 10); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.checkWindowLimit(ctx, fmt.Sprintf("sms:device:%s:30m", deviceID), 30*time.Minute, 10); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.checkWindowLimit(ctx, fmt.Sprintf("sms:device:%s:1h", deviceID), time.Hour, 20); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkWindowLimit 通用时间窗口计数检查
|
||||
func (s *SMSCodeService) checkWindowLimit(ctx context.Context, key string, ttl time.Duration, limit int) error {
|
||||
var count int
|
||||
if err := s.cache.Get(ctx, key, &count); err == nil {
|
||||
if count >= limit {
|
||||
return fmt.Errorf("短信发送过于频繁,请稍后再试")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateSendRecord 更新发送记录
|
||||
func (s *SMSCodeService) updateSendRecord(ctx context.Context, phone string, scene entities.SMSScene) {
|
||||
now := time.Now()
|
||||
|
||||
// 更新最后发送时间
|
||||
lastSentKey := fmt.Sprintf("sms:last_sent:%s:%s", scene, phone)
|
||||
s.cache.Set(ctx, lastSentKey, now, s.config.RateLimit.MinInterval)
|
||||
|
||||
// 更新每小时计数
|
||||
hourlyKey := fmt.Sprintf("sms:hourly:%s:%s", phone, now.Format("2006010215"))
|
||||
var hourlyCount int
|
||||
if err := s.cache.Get(ctx, hourlyKey, &hourlyCount); err == nil {
|
||||
s.cache.Set(ctx, hourlyKey, hourlyCount+1, time.Hour)
|
||||
} else {
|
||||
s.cache.Set(ctx, hourlyKey, 1, time.Hour)
|
||||
}
|
||||
|
||||
// 更新每日计数
|
||||
dailyKey := fmt.Sprintf("sms:daily:%s:%s", phone, now.Format("20060102"))
|
||||
// 更新每日计数(用于后续达到上限时永久封禁)
|
||||
dailyKey := fmt.Sprintf("sms:phone:%s:1d", phone)
|
||||
var dailyCount int
|
||||
if err := s.cache.Get(ctx, dailyKey, &dailyCount); err == nil {
|
||||
s.cache.Set(ctx, dailyKey, dailyCount+1, 24*time.Hour)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tyapi-server/internal/domains/api/entities"
|
||||
"tyapi-server/internal/domains/api/repositories"
|
||||
"tyapi-server/internal/shared/database"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
ReportsTable = "reports"
|
||||
)
|
||||
|
||||
// GormReportRepository 报告记录 GORM 仓储实现
|
||||
type GormReportRepository struct {
|
||||
*database.BaseRepositoryImpl
|
||||
}
|
||||
|
||||
var _ repositories.ReportRepository = (*GormReportRepository)(nil)
|
||||
|
||||
// NewGormReportRepository 创建报告记录仓储实现
|
||||
func NewGormReportRepository(db *gorm.DB, logger *zap.Logger) repositories.ReportRepository {
|
||||
return &GormReportRepository{
|
||||
BaseRepositoryImpl: database.NewBaseRepositoryImpl(db, logger),
|
||||
}
|
||||
}
|
||||
|
||||
// Create 创建报告记录
|
||||
func (r *GormReportRepository) Create(ctx context.Context, report *entities.Report) error {
|
||||
return r.CreateEntity(ctx, report)
|
||||
}
|
||||
|
||||
// FindByReportID 根据报告编号查询记录
|
||||
func (r *GormReportRepository) FindByReportID(ctx context.Context, reportID string) (*entities.Report, error) {
|
||||
var report entities.Report
|
||||
if err := r.FindOneByField(ctx, &report, "report_id", reportID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &report, nil
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
UsersTable = "users"
|
||||
UsersTable = "users"
|
||||
UserCacheTTL = 30 * 60 // 30分钟
|
||||
)
|
||||
|
||||
@@ -415,7 +415,7 @@ func (r *GormUserRepository) GetSystemUserStatsByDateRange(ctx context.Context,
|
||||
// GetSystemDailyUserStats 获取系统每日用户统计
|
||||
func (r *GormUserRepository) GetSystemDailyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(created_at) as date,
|
||||
@@ -426,19 +426,19 @@ func (r *GormUserRepository) GetSystemDailyUserStats(ctx context.Context, startD
|
||||
GROUP BY DATE(created_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetSystemMonthlyUserStats 获取系统每月用户统计
|
||||
func (r *GormUserRepository) GetSystemMonthlyUserStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(created_at, 'YYYY-MM') as month,
|
||||
@@ -449,19 +449,19 @@ func (r *GormUserRepository) GetSystemMonthlyUserStats(ctx context.Context, star
|
||||
GROUP BY TO_CHAR(created_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetSystemDailyCertificationStats 获取系统每日认证用户统计(基于is_certified字段)
|
||||
func (r *GormUserRepository) GetSystemDailyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
DATE(updated_at) as date,
|
||||
@@ -473,19 +473,19 @@ func (r *GormUserRepository) GetSystemDailyCertificationStats(ctx context.Contex
|
||||
GROUP BY DATE(updated_at)
|
||||
ORDER BY date ASC
|
||||
`
|
||||
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate.Format("2006-01-02"), endDate.Format("2006-01-02")).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetSystemMonthlyCertificationStats 获取系统每月认证用户统计(基于is_certified字段)
|
||||
func (r *GormUserRepository) GetSystemMonthlyCertificationStats(ctx context.Context, startDate, endDate time.Time) ([]map[string]interface{}, error) {
|
||||
var results []map[string]interface{}
|
||||
|
||||
|
||||
sql := `
|
||||
SELECT
|
||||
TO_CHAR(updated_at, 'YYYY-MM') as month,
|
||||
@@ -497,12 +497,12 @@ func (r *GormUserRepository) GetSystemMonthlyCertificationStats(ctx context.Cont
|
||||
GROUP BY TO_CHAR(updated_at, 'YYYY-MM')
|
||||
ORDER BY month ASC
|
||||
`
|
||||
|
||||
|
||||
err := r.GetDB(ctx).Raw(sql, startDate, endDate).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ func (r *GormUserRepository) GetSystemMonthlyCertificationStats(ctx context.Cont
|
||||
func (r *GormUserRepository) GetUserCallRankingByCalls(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
var args []interface{}
|
||||
|
||||
|
||||
switch period {
|
||||
case "today":
|
||||
sql = `
|
||||
@@ -565,13 +565,13 @@ func (r *GormUserRepository) GetUserCallRankingByCalls(ctx context.Context, peri
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的时间周期: %s", period)
|
||||
}
|
||||
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -579,7 +579,7 @@ func (r *GormUserRepository) GetUserCallRankingByCalls(ctx context.Context, peri
|
||||
func (r *GormUserRepository) GetUserCallRankingByConsumption(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
var args []interface{}
|
||||
|
||||
|
||||
switch period {
|
||||
case "today":
|
||||
sql = `
|
||||
@@ -634,13 +634,13 @@ func (r *GormUserRepository) GetUserCallRankingByConsumption(ctx context.Context
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的时间周期: %s", period)
|
||||
}
|
||||
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
@@ -648,7 +648,7 @@ func (r *GormUserRepository) GetUserCallRankingByConsumption(ctx context.Context
|
||||
func (r *GormUserRepository) GetRechargeRanking(ctx context.Context, period string, limit int) ([]map[string]interface{}, error) {
|
||||
var sql string
|
||||
var args []interface{}
|
||||
|
||||
|
||||
switch period {
|
||||
case "today":
|
||||
sql = `
|
||||
@@ -709,12 +709,12 @@ func (r *GormUserRepository) GetRechargeRanking(ctx context.Context, period stri
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的时间周期: %s", period)
|
||||
}
|
||||
|
||||
|
||||
var results []map[string]interface{}
|
||||
err := r.GetDB(ctx).Raw(sql, args...).Scan(&results).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
return results, nil
|
||||
}
|
||||
}
|
||||
|
||||
134
internal/infrastructure/external/captcha/captcha_service.go
vendored
Normal file
134
internal/infrastructure/external/captcha/captcha_service.go
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
package captcha
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/alibabacloud-go/tea/tea"
|
||||
captcha20230305 "github.com/alibabacloud-go/captcha-20230305/client"
|
||||
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCaptchaVerifyFailed = errors.New("图形验证码校验失败")
|
||||
ErrCaptchaConfig = errors.New("验证码配置错误")
|
||||
ErrCaptchaEncryptMissing = errors.New("加密模式需要配置 EncryptKey(控制台 ekey)")
|
||||
)
|
||||
|
||||
// CaptchaConfig 阿里云验证码配置
|
||||
type CaptchaConfig struct {
|
||||
AccessKeyID string
|
||||
AccessKeySecret string
|
||||
EndpointURL string
|
||||
SceneID string
|
||||
// EncryptKey 加密模式使用的密钥(控制台 ekey,Base64 编码的 32 字节),用于生成 EncryptedSceneId
|
||||
EncryptKey string
|
||||
}
|
||||
|
||||
// CaptchaService 阿里云验证码服务
|
||||
type CaptchaService struct {
|
||||
config CaptchaConfig
|
||||
}
|
||||
|
||||
// NewCaptchaService 创建验证码服务实例
|
||||
func NewCaptchaService(config CaptchaConfig) *CaptchaService {
|
||||
return &CaptchaService{
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Verify 验证滑块验证码
|
||||
func (s *CaptchaService) Verify(captchaVerifyParam string) error {
|
||||
if captchaVerifyParam == "" {
|
||||
return ErrCaptchaVerifyFailed
|
||||
}
|
||||
|
||||
if s.config.AccessKeyID == "" || s.config.AccessKeySecret == "" {
|
||||
return ErrCaptchaConfig
|
||||
}
|
||||
|
||||
clientCfg := &openapi.Config{
|
||||
AccessKeyId: tea.String(s.config.AccessKeyID),
|
||||
AccessKeySecret: tea.String(s.config.AccessKeySecret),
|
||||
}
|
||||
clientCfg.Endpoint = tea.String(s.config.EndpointURL)
|
||||
|
||||
client, err := captcha20230305.NewClient(clientCfg)
|
||||
if err != nil {
|
||||
return errors.Join(ErrCaptchaConfig, err)
|
||||
}
|
||||
|
||||
req := &captcha20230305.VerifyIntelligentCaptchaRequest{
|
||||
SceneId: tea.String(s.config.SceneID),
|
||||
CaptchaVerifyParam: tea.String(captchaVerifyParam),
|
||||
}
|
||||
|
||||
resp, err := client.VerifyIntelligentCaptcha(req)
|
||||
if err != nil {
|
||||
return errors.Join(ErrCaptchaVerifyFailed, err)
|
||||
}
|
||||
|
||||
if resp.Body == nil || !tea.BoolValue(resp.Body.Result.VerifyResult) {
|
||||
return ErrCaptchaVerifyFailed
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEncryptedSceneId 生成加密场景 ID(EncryptedSceneId),供前端加密模式初始化验证码使用。
|
||||
// 算法:AES-256-CBC,明文 sceneId×tamp&expireTime,密钥为控制台 ekey(Base64 解码后 32 字节)。
|
||||
// expireTimeSec 有效期为 1~86400 秒。
|
||||
func (s *CaptchaService) GetEncryptedSceneId(expireTimeSec int) (string, error) {
|
||||
if expireTimeSec <= 0 || expireTimeSec > 86400 {
|
||||
return "", fmt.Errorf("expireTimeSec 必须在 1~86400 之间")
|
||||
}
|
||||
if s.config.EncryptKey == "" {
|
||||
return "", ErrCaptchaEncryptMissing
|
||||
}
|
||||
if s.config.SceneID == "" {
|
||||
return "", ErrCaptchaConfig
|
||||
}
|
||||
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(s.config.EncryptKey)
|
||||
if err != nil || len(keyBytes) != 32 {
|
||||
return "", errors.Join(ErrCaptchaConfig, fmt.Errorf("EncryptKey 必须为 Base64 编码的 32 字节"))
|
||||
}
|
||||
|
||||
plaintext := fmt.Sprintf("%s&%d&%d", s.config.SceneID, time.Now().Unix(), expireTimeSec)
|
||||
plainBytes := []byte(plaintext)
|
||||
plainBytes = pkcs7Pad(plainBytes, aes.BlockSize)
|
||||
|
||||
block, err := aes.NewCipher(keyBytes)
|
||||
if err != nil {
|
||||
return "", errors.Join(ErrCaptchaConfig, err)
|
||||
}
|
||||
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
ciphertext := make([]byte, len(plainBytes))
|
||||
mode.CryptBlocks(ciphertext, plainBytes)
|
||||
|
||||
result := make([]byte, len(iv)+len(ciphertext))
|
||||
copy(result, iv)
|
||||
copy(result[len(iv):], ciphertext)
|
||||
return base64.StdEncoding.EncodeToString(result), nil
|
||||
}
|
||||
|
||||
func pkcs7Pad(data []byte, blockSize int) []byte {
|
||||
n := blockSize - (len(data) % blockSize)
|
||||
pad := make([]byte, n)
|
||||
for i := range pad {
|
||||
pad[i] = byte(n)
|
||||
}
|
||||
return append(data, pad...)
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func (s *WeChatWorkService) sendNewApplicationNotification(ctx context.Context,
|
||||
applicantName := data["applicant_name"].(string)
|
||||
applicationID := data["application_id"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## 🆕 新的企业认证申请
|
||||
content := fmt.Sprintf(`## 【天远API】🆕 新的企业认证申请
|
||||
|
||||
**企业名称**: %s
|
||||
**申请人**: %s
|
||||
@@ -183,7 +183,7 @@ func (s *WeChatWorkService) sendOCRSuccessNotification(ctx context.Context, data
|
||||
confidence := data["confidence"].(float64)
|
||||
applicationID := data["application_id"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ✅ OCR识别成功
|
||||
content := fmt.Sprintf(`## 【天远API】✅ OCR识别成功
|
||||
|
||||
**企业名称**: %s
|
||||
**识别置信度**: %.2f%%
|
||||
@@ -204,7 +204,7 @@ func (s *WeChatWorkService) sendOCRFailedNotification(ctx context.Context, data
|
||||
applicationID := data["application_id"].(string)
|
||||
errorMsg := data["error_message"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ❌ OCR识别失败
|
||||
content := fmt.Sprintf(`## 【天远API】❌ OCR识别失败
|
||||
|
||||
**申请ID**: %s
|
||||
**错误信息**: %s
|
||||
@@ -224,7 +224,7 @@ func (s *WeChatWorkService) sendFaceVerifySuccessNotification(ctx context.Contex
|
||||
applicationID := data["application_id"].(string)
|
||||
confidence := data["confidence"].(float64)
|
||||
|
||||
content := fmt.Sprintf(`## ✅ 人脸识别成功
|
||||
content := fmt.Sprintf(`## 【天远API】✅ 人脸识别成功
|
||||
|
||||
**申请人**: %s
|
||||
**申请ID**: %s
|
||||
@@ -246,7 +246,7 @@ func (s *WeChatWorkService) sendFaceVerifyFailedNotification(ctx context.Context
|
||||
applicationID := data["application_id"].(string)
|
||||
errorMsg := data["error_message"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ❌ 人脸识别失败
|
||||
content := fmt.Sprintf(`## 【天远API】❌ 人脸识别失败
|
||||
|
||||
**申请人**: %s
|
||||
**申请ID**: %s
|
||||
@@ -269,7 +269,7 @@ func (s *WeChatWorkService) sendAdminApprovedNotification(ctx context.Context, d
|
||||
adminName := data["admin_name"].(string)
|
||||
comment := data["comment"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ✅ 管理员审核通过
|
||||
content := fmt.Sprintf(`## 【天远API】✅ 管理员审核通过
|
||||
|
||||
**企业名称**: %s
|
||||
**申请ID**: %s
|
||||
@@ -294,7 +294,7 @@ func (s *WeChatWorkService) sendAdminRejectedNotification(ctx context.Context, d
|
||||
adminName := data["admin_name"].(string)
|
||||
reason := data["reason"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## ❌ 管理员审核拒绝
|
||||
content := fmt.Sprintf(`## 【天远API】❌ 管理员审核拒绝
|
||||
|
||||
**企业名称**: %s
|
||||
**申请ID**: %s
|
||||
@@ -318,7 +318,7 @@ func (s *WeChatWorkService) sendContractSignedNotification(ctx context.Context,
|
||||
applicationID := data["application_id"].(string)
|
||||
signerName := data["signer_name"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## 📝 电子合同已签署
|
||||
content := fmt.Sprintf(`## 【天远API】📝 电子合同已签署
|
||||
|
||||
**企业名称**: %s
|
||||
**申请ID**: %s
|
||||
@@ -340,7 +340,7 @@ func (s *WeChatWorkService) sendCertificationCompletedNotification(ctx context.C
|
||||
applicationID := data["application_id"].(string)
|
||||
walletAddress := data["wallet_address"].(string)
|
||||
|
||||
content := fmt.Sprintf(`## 🎉 企业认证完成
|
||||
content := fmt.Sprintf(`## 【天远API】🎉 企业认证完成
|
||||
|
||||
**企业名称**: %s
|
||||
**申请ID**: %s
|
||||
@@ -475,7 +475,7 @@ func (s *WeChatWorkService) SendSystemAlert(ctx context.Context, level, title, m
|
||||
icon = "📢"
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(`## %s 系统告警
|
||||
content := fmt.Sprintf(`## 【天远API】%s 系统告警
|
||||
|
||||
**级别**: %s
|
||||
**标题**: %s
|
||||
@@ -496,7 +496,7 @@ func (s *WeChatWorkService) SendSystemAlert(ctx context.Context, level, title, m
|
||||
func (s *WeChatWorkService) SendDailyReport(ctx context.Context, reportData map[string]interface{}) error {
|
||||
s.logger.Info("发送每日报告")
|
||||
|
||||
content := fmt.Sprintf(`## 📊 企业认证系统每日报告
|
||||
content := fmt.Sprintf(`## 【天远API】📊 企业认证系统每日报告
|
||||
|
||||
**报告日期**: %s
|
||||
|
||||
|
||||
148
internal/infrastructure/external/notification/wechat_work_service_test.go
vendored
Normal file
148
internal/infrastructure/external/notification/wechat_work_service_test.go
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
package notification_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/infrastructure/external/notification"
|
||||
)
|
||||
|
||||
// newTestWeChatWorkService 创建用于测试的企业微信服务实例
|
||||
// 默认使用环境变量 WECOM_WEBHOOK,若未设置则使用项目配置中的 webhook。
|
||||
func newTestWeChatWorkService(t *testing.T) *notification.WeChatWorkService {
|
||||
t.Helper()
|
||||
|
||||
webhook := os.Getenv("WECOM_WEBHOOK")
|
||||
if webhook == "" {
|
||||
// 使用你提供的 webhook 地址
|
||||
webhook = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=649bf737-28ca-4f30-ad5f-cfb65b2af113"
|
||||
}
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
return notification.NewWeChatWorkService(webhook, "", logger)
|
||||
}
|
||||
|
||||
// TestWeChatWork_SendAllBusinessNotifications
|
||||
// 手动运行该用例,将依次向企业微信群推送 5 种业务场景的通知:
|
||||
// 1. 用户充值成功
|
||||
// 2. 用户申请开发票
|
||||
// 3. 用户企业认证成功
|
||||
// 4. 用户余额低于阈值
|
||||
// 5. 用户余额欠费
|
||||
//
|
||||
// 注意:
|
||||
// - 通知中只使用企业名称和手机号码,不展示用户ID
|
||||
// - 默认使用示例企业名称和手机号,实际使用时请根据需要修改
|
||||
func TestWeChatWork_SendAllBusinessNotifications(t *testing.T) {
|
||||
svc := newTestWeChatWorkService(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// 示例企业信息(实际可按需修改)
|
||||
enterpriseName := "测试企业有限公司"
|
||||
phone := "13800000000"
|
||||
|
||||
now := time.Now().Format("2006-01-02 15:04:05")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
}{
|
||||
{
|
||||
name: "recharge_success",
|
||||
content: fmt.Sprintf(
|
||||
"### 【天远API】用户充值成功通知\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 充值金额:%s 元\n"+
|
||||
"> 入账总额:%s 元(含赠送)\n"+
|
||||
"> 时间:%s\n",
|
||||
enterpriseName,
|
||||
phone,
|
||||
"1000.00",
|
||||
"1050.00",
|
||||
now,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "invoice_applied",
|
||||
content: fmt.Sprintf(
|
||||
"### 【天远API】用户申请开发票\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 申请开票金额:%s 元\n"+
|
||||
"> 发票类型:%s\n"+
|
||||
"> 申请时间:%s\n"+
|
||||
"\n请财务尽快审核并开具发票。",
|
||||
enterpriseName,
|
||||
phone,
|
||||
"500.00",
|
||||
"增值税专用发票",
|
||||
now,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "certification_completed",
|
||||
content: fmt.Sprintf(
|
||||
"### 【天远API】企业认证成功\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 完成时间:%s\n"+
|
||||
"\n该企业已完成认证,请相关同事同步更新内部系统并关注后续接入情况。",
|
||||
enterpriseName,
|
||||
phone,
|
||||
now,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "low_balance_alert",
|
||||
content: fmt.Sprintf(
|
||||
"### 【天远API】用户余额预警\n"+
|
||||
"<font color=\"warning\">用户余额已低于预警阈值,请及时跟进。</font>\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 当前余额:%s 元\n"+
|
||||
"> 预警阈值:%s 元\n"+
|
||||
"> 时间:%s\n",
|
||||
enterpriseName,
|
||||
phone,
|
||||
"180.00",
|
||||
"200.00",
|
||||
now,
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "arrears_alert",
|
||||
content: fmt.Sprintf(
|
||||
"### 【天远API】用户余额欠费告警\n"+
|
||||
"<font color=\"warning\">该企业已发生欠费,请及时联系并处理。</font>\n"+
|
||||
"> 企业名称:%s\n"+
|
||||
"> 联系手机:%s\n"+
|
||||
"> 当前余额:%s 元\n"+
|
||||
"> 欠费金额:%s 元\n"+
|
||||
"> 时间:%s\n",
|
||||
enterpriseName,
|
||||
phone,
|
||||
"-50.00",
|
||||
"50.00",
|
||||
now,
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := svc.SendMarkdownMessage(ctx, tc.content); err != nil {
|
||||
t.Fatalf("发送场景[%s]通知失败: %v", tc.name, err)
|
||||
}
|
||||
// 简单间隔,避免瞬时发送过多消息
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
47
internal/infrastructure/external/shujubao/crypto.go
vendored
Normal file
47
internal/infrastructure/external/shujubao/crypto.go
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SignMethod 签名方法
|
||||
type SignMethod string
|
||||
|
||||
const (
|
||||
SignMethodMD5 SignMethod = "md5"
|
||||
SignMethodHMACMD5 SignMethod = "hmac"
|
||||
)
|
||||
|
||||
// GenerateSignMD5 使用 MD5 生成签名:md5(app_secret + timestamp),32 位小写
|
||||
func GenerateSignMD5(appSecret, timestamp string) string {
|
||||
h := md5.Sum([]byte(appSecret + timestamp))
|
||||
sign := strings.ToLower(hex.EncodeToString(h[:]))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignHMAC 使用 HMAC-MD5 生成签名(仅 timestamp,兼容旧逻辑)
|
||||
func GenerateSignHMAC(appSecret, timestamp string) string {
|
||||
mac := hmac.New(md5.New, []byte(appSecret))
|
||||
mac.Write([]byte(timestamp))
|
||||
sign := strings.ToLower(hex.EncodeToString(mac.Sum(nil)))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignFromParamsMD5 根据入参生成签名:入参按 ASCII 排序组合后与 app_secret 做 MD5。
|
||||
// sortedParamStr 格式为 key1=value1&key2=value2&...(key 按字母序)。
|
||||
func GenerateSignFromParamsMD5(appSecret, sortedParamStr string) string {
|
||||
h := md5.Sum([]byte(appSecret + sortedParamStr))
|
||||
sign := strings.ToLower(hex.EncodeToString(h[:]))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignFromParamsHMAC 根据入参生成签名:入参按 ASCII 排序组合后与 app_secret 做 HMAC-MD5。
|
||||
func GenerateSignFromParamsHMAC(appSecret, sortedParamStr string) string {
|
||||
mac := hmac.New(md5.New, []byte(appSecret))
|
||||
mac.Write([]byte(sortedParamStr))
|
||||
sign := strings.ToLower(hex.EncodeToString(mac.Sum(nil)))
|
||||
return sign
|
||||
}
|
||||
124
internal/infrastructure/external/shujubao/shujubao_errors.go
vendored
Normal file
124
internal/infrastructure/external/shujubao/shujubao_errors.go
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ShujubaoError 数据宝服务错误
|
||||
type ShujubaoError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error 实现 error 接口
|
||||
func (e *ShujubaoError) Error() string {
|
||||
return fmt.Sprintf("数据宝错误 [%s]: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否成功
|
||||
func (e *ShujubaoError) IsSuccess() bool {
|
||||
return e.Code == "200" || e.Code == "0" || e.Code == "10000"
|
||||
}
|
||||
|
||||
// NewShujubaoError 创建新的数据宝错误
|
||||
func NewShujubaoError(code, message string) *ShujubaoError {
|
||||
return &ShujubaoError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// 数据宝全系统错误码与描述映射(Code -> Desc)
|
||||
var systemErrorCodeDesc = map[string]string{
|
||||
"10000": "成功",
|
||||
"10001": "查空",
|
||||
"10002": "查询失败",
|
||||
"10003": "系统处理异常",
|
||||
"10004": "系统处理超时",
|
||||
"10005": "服务异常",
|
||||
"10006": "查无",
|
||||
"10017": "查询失败",
|
||||
"10018": "参数错误",
|
||||
"10019": "系统异常",
|
||||
"10020": "同一参数请求次数超限",
|
||||
"99999": "其他错误",
|
||||
"999": "接口处理异常",
|
||||
"000": "key参数不能为空",
|
||||
"001": "找不到这个key",
|
||||
"002": "调用次数已用完",
|
||||
"003": "用户该接口状态不可用",
|
||||
"004": "接口信息不存在",
|
||||
"005": "你没有认证信息",
|
||||
"008": "当前接口只允许“企业认证”通过的账户进行调用,请在数据宝官网个人中心进行企业认证后再进行调用,谢谢!",
|
||||
"009": "触发风控",
|
||||
"011": "接口缺少参数",
|
||||
"012": "没有ip访问权限",
|
||||
"013": "接口模板不存在",
|
||||
"015": "该接口已下架",
|
||||
"020": "调用第三方产生异常",
|
||||
"022": "调用第三方返回的数据格式错误",
|
||||
"025": "你没有购买此接口",
|
||||
"026": "用户信息不存在",
|
||||
"027": "请求第三方地址超时,请稍后再试",
|
||||
"028": "请求第三方地址被拒绝,请稍后再试",
|
||||
"034": "签名不合法",
|
||||
"035": "请求参数加密有误",
|
||||
"036": "验签失败",
|
||||
"037": "timestamp不能为空",
|
||||
"038": "请求繁忙,请稍后联系管理员再试",
|
||||
"039": "请在个人中心接口设置加密状态",
|
||||
"040": "timestamp不合法",
|
||||
"041": "timestamp已过期",
|
||||
"042": "身份证手机号姓名银行卡等不符合规则",
|
||||
"043": "该号段不支持验证",
|
||||
"047": "请在个人中心获取密钥",
|
||||
"048": "找不到这个secretKey",
|
||||
"049": "用户还未申购该产品",
|
||||
"050": "请联系客服开启验签",
|
||||
"051": "超过当日调用次数",
|
||||
"052": "机房限制调用,请联系客服切换其他机房",
|
||||
"053": "系统错误",
|
||||
"054": "token无效",
|
||||
"055": "配置信息未完善,请联系数据宝工作人员",
|
||||
"056": "apiName参数不能为空",
|
||||
"057": "并发量超过限制,请联系客服",
|
||||
"058": "撞库风控预警,请联系客服",
|
||||
}
|
||||
|
||||
// GetSystemErrorDesc 根据错误码获取系统错误描述(支持带 SYSTEM_ 前缀或纯数字)
|
||||
func GetSystemErrorDesc(code string) string {
|
||||
// 去掉 SYSTEM_ 前缀
|
||||
key := code
|
||||
if len(code) > 7 && code[:7] == "SYSTEM_" {
|
||||
key = code[7:]
|
||||
}
|
||||
if desc, ok := systemErrorCodeDesc[key]; ok {
|
||||
return desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// NewShujubaoErrorFromCode 根据状态码创建错误
|
||||
func NewShujubaoErrorFromCode(code, message string) *ShujubaoError {
|
||||
if message != "" {
|
||||
return NewShujubaoError(code, message)
|
||||
}
|
||||
if desc := GetSystemErrorDesc(code); desc != "" {
|
||||
return NewShujubaoError(code, desc)
|
||||
}
|
||||
return NewShujubaoError(code, "未知错误")
|
||||
}
|
||||
|
||||
// IsShujubaoError 检查是否是数据宝错误
|
||||
func IsShujubaoError(err error) bool {
|
||||
_, ok := err.(*ShujubaoError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetShujubaoError 获取数据宝错误
|
||||
func GetShujubaoError(err error) *ShujubaoError {
|
||||
if shujubaoErr, ok := err.(*ShujubaoError); ok {
|
||||
return shujubaoErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
66
internal/infrastructure/external/shujubao/shujubao_factory.go
vendored
Normal file
66
internal/infrastructure/external/shujubao/shujubao_factory.go
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewShujubaoServiceWithConfig 使用配置创建数据宝服务
|
||||
func NewShujubaoServiceWithConfig(cfg *config.Config) (*ShujubaoService, error) {
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Shujubao.Logging.Enabled,
|
||||
LogDir: cfg.Shujubao.Logging.LogDir,
|
||||
ServiceName: "shujubao",
|
||||
UseDaily: cfg.Shujubao.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Shujubao.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
for k, v := range cfg.Shujubao.Logging.LevelConfigs {
|
||||
loggingConfig.LevelConfigs[k] = external_logger.ExternalServiceLevelFileConfig{
|
||||
MaxSize: v.MaxSize,
|
||||
MaxBackups: v.MaxBackups,
|
||||
MaxAge: v.MaxAge,
|
||||
Compress: v.Compress,
|
||||
}
|
||||
}
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var signMethod SignMethod
|
||||
if cfg.Shujubao.SignMethod == "md5" {
|
||||
signMethod = SignMethodMD5
|
||||
} else {
|
||||
signMethod = SignMethodHMACMD5
|
||||
}
|
||||
timeout := 60 * time.Second
|
||||
if cfg.Shujubao.Timeout > 0 {
|
||||
timeout = cfg.Shujubao.Timeout
|
||||
}
|
||||
|
||||
return NewShujubaoService(
|
||||
cfg.Shujubao.URL,
|
||||
cfg.Shujubao.AppSecret,
|
||||
signMethod,
|
||||
timeout,
|
||||
logger,
|
||||
), nil
|
||||
}
|
||||
|
||||
// NewShujubaoServiceWithLogging 使用自定义日志配置创建数据宝服务
|
||||
func NewShujubaoServiceWithLogging(url, appSecret string, signMethod SignMethod, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*ShujubaoService, error) {
|
||||
loggingConfig.ServiceName = "shujubao"
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewShujubaoService(url, appSecret, signMethod, timeout, logger), nil
|
||||
}
|
||||
|
||||
// NewShujubaoServiceSimple 创建无日志的数据宝服务
|
||||
func NewShujubaoServiceSimple(url, appSecret string, signMethod SignMethod, timeout time.Duration) *ShujubaoService {
|
||||
return NewShujubaoService(url, appSecret, signMethod, timeout, nil)
|
||||
}
|
||||
308
internal/infrastructure/external/shujubao/shujubao_service.go
vendored
Normal file
308
internal/infrastructure/external/shujubao/shujubao_service.go
vendored
Normal file
@@ -0,0 +1,308 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// 错误日志中单条入参值的最大长度,避免 base64 等长内容打满日志
|
||||
maxLogParamValueLen = 300
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrQueryEmpty = errors.New("查询为空")
|
||||
)
|
||||
|
||||
// truncateForLog 将字符串截断到指定长度,用于错误日志,避免 base64 等过长内容
|
||||
func truncateForLog(s string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return s
|
||||
}
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "...[truncated, total " + strconv.Itoa(len(s)) + " chars]"
|
||||
}
|
||||
|
||||
// paramsForLog 返回适合写入错误日志的入参副本(长字符串会被截断)
|
||||
func paramsForLog(params map[string]interface{}) map[string]interface{} {
|
||||
if params == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]interface{}, len(params))
|
||||
for k, v := range params {
|
||||
if v == nil {
|
||||
out[k] = nil
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
out[k] = truncateForLog(val, maxLogParamValueLen)
|
||||
default:
|
||||
s := fmt.Sprint(v)
|
||||
out[k] = truncateForLog(s, maxLogParamValueLen)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ShujubaoResp 数据宝 API 通用响应(按实际文档调整)
|
||||
type ShujubaoResp struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// ShujubaoConfig 数据宝服务配置
|
||||
type ShujubaoConfig struct {
|
||||
URL string
|
||||
AppSecret string
|
||||
SignMethod SignMethod
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// ShujubaoService 数据宝服务
|
||||
type ShujubaoService struct {
|
||||
config ShujubaoConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewShujubaoService 创建数据宝服务实例
|
||||
func NewShujubaoService(url, appSecret string, signMethod SignMethod, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *ShujubaoService {
|
||||
if signMethod == "" {
|
||||
signMethod = SignMethodHMACMD5
|
||||
}
|
||||
if timeout == 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
return &ShujubaoService{
|
||||
config: ShujubaoConfig{
|
||||
URL: url,
|
||||
AppSecret: appSecret,
|
||||
SignMethod: signMethod,
|
||||
Timeout: timeout,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求 ID
|
||||
func (s *ShujubaoService) generateRequestID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, s.config.AppSecret)))
|
||||
return fmt.Sprintf("shujubao_%x", hash[:8])
|
||||
}
|
||||
|
||||
// buildSortedParamStr 将入参按 key 的 ASCII 排序组合为 key1=value1&key2=value2&...
|
||||
func buildSortedParamStr(params map[string]interface{}) string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte('&')
|
||||
}
|
||||
v := params[k]
|
||||
var vs string
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
vs = val
|
||||
case nil:
|
||||
vs = ""
|
||||
default:
|
||||
vs = fmt.Sprint(val)
|
||||
}
|
||||
b.WriteString(k)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(vs)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// buildFormUrlEncodedBody 按 key 的 ASCII 排序构建 application/x-www-form-urlencoded 请求体(键与值均已 URL 编码)
|
||||
func buildFormUrlEncodedBody(params map[string]interface{}) string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte('&')
|
||||
}
|
||||
v := params[k]
|
||||
var vs string
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
vs = val
|
||||
case nil:
|
||||
vs = ""
|
||||
default:
|
||||
vs = fmt.Sprint(val)
|
||||
}
|
||||
b.WriteString(url.QueryEscape(k))
|
||||
b.WriteByte('=')
|
||||
b.WriteString(url.QueryEscape(vs))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// generateSign 根据入参与时间戳生成签名。入参按 ASCII 排序组合后与 app_secret 做 MD5/HMAC。
|
||||
// 对于开启了加密的接口需传 sign 与 timestamp;明文传输的接口则无需传这两个参数。
|
||||
func (s *ShujubaoService) generateSign(timestamp string, params map[string]interface{}) string {
|
||||
// 合并 timestamp 到入参后参与排序
|
||||
merged := make(map[string]interface{}, len(params)+1)
|
||||
for k, v := range params {
|
||||
merged[k] = v
|
||||
}
|
||||
merged["timestamp"] = timestamp
|
||||
sortedStr := buildSortedParamStr(merged)
|
||||
switch s.config.SignMethod {
|
||||
case SignMethodMD5:
|
||||
return GenerateSignFromParamsMD5(s.config.AppSecret, sortedStr)
|
||||
default:
|
||||
return GenerateSignFromParamsHMAC(s.config.AppSecret, sortedStr)
|
||||
}
|
||||
}
|
||||
|
||||
// buildRequestURL 拼接接口地址得到最终请求 URL,如 https://api.chinadatapay.com/communication/personal/197
|
||||
func (s *ShujubaoService) buildRequestURL(apiPath string) string {
|
||||
base := strings.TrimSuffix(s.config.URL, "/")
|
||||
if apiPath == "" {
|
||||
return base
|
||||
}
|
||||
return base + "/" + strings.TrimPrefix(apiPath, "/")
|
||||
}
|
||||
|
||||
// CallAPI 调用数据宝 API(POST)。最终请求地址 = url + 拼接接口地址值;body 为业务参数;sign、timestamp 按原样传 header。
|
||||
func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params map[string]interface{}) (data interface{}, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := s.generateRequestID()
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 /personal/197
|
||||
requestURL := s.buildRequestURL(apiPath)
|
||||
|
||||
var transactionID string
|
||||
if id, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = id
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.LogRequest(requestID, transactionID, apiPath, requestURL)
|
||||
}
|
||||
|
||||
// 使用 application/x-www-form-urlencoded,贵司接口暂不支持 JSON 入参
|
||||
formBody := buildFormUrlEncodedBody(params)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, strings.NewReader(formBody))
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("timestamp", timestamp)
|
||||
req.Header.Set("sign", s.generateSign(timestamp, params))
|
||||
|
||||
client := &http.Client{Timeout: s.config.Timeout}
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
isTimeout := false
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
isTimeout = true
|
||||
} else if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
isTimeout = true
|
||||
} else if errStr := err.Error(); errStr == "context deadline exceeded" ||
|
||||
errStr == "timeout" ||
|
||||
errStr == "Client.Timeout exceeded" ||
|
||||
errStr == "net/http: request canceled" {
|
||||
isTimeout = true
|
||||
}
|
||||
if isTimeout {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", err))
|
||||
} else {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
}
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
duration := time.Since(startTime)
|
||||
s.logger.LogResponse(requestID, transactionID, apiPath, response.StatusCode, duration)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", response.StatusCode))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var shujubaoResp ShujubaoResp
|
||||
if err := json.Unmarshal(respBody, &shujubaoResp); err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, paramsForLog(params))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code := shujubaoResp.Code
|
||||
if code == "10001" || code == "10006" {
|
||||
// 查空/查无:返回空数组,不视为错误
|
||||
return []interface{}{}, nil
|
||||
}
|
||||
if code != "10000" {
|
||||
shujubaoErr := NewShujubaoErrorFromCode(code, shujubaoResp.Message)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, shujubaoErr, paramsForLog(params))
|
||||
}
|
||||
return nil, errors.Join(ErrDatasource, shujubaoErr)
|
||||
}
|
||||
|
||||
return shujubaoResp.Data, nil
|
||||
}
|
||||
@@ -16,12 +16,52 @@ import (
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// 错误日志中单条入参值的最大长度,避免 base64 等长内容打满日志
|
||||
maxLogParamValueLen = 300
|
||||
// 错误日志中 response_body 的最大长度
|
||||
maxLogResponseBodyLen = 500
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrNotFound = errors.New("查询为空")
|
||||
)
|
||||
|
||||
// truncateForLog 将字符串截断到指定长度,用于错误日志,避免 base64 等过长内容
|
||||
func truncateForLog(s string, maxLen int) string {
|
||||
if maxLen <= 0 {
|
||||
return s
|
||||
}
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "...[truncated, total " + strconv.Itoa(len(s)) + " chars]"
|
||||
}
|
||||
|
||||
// requestParamsForLog 返回适合写入错误日志的入参副本(长字符串会被截断)
|
||||
func requestParamsForLog(reqFormData map[string]interface{}) map[string]interface{} {
|
||||
if reqFormData == nil {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]interface{}, len(reqFormData))
|
||||
for k, v := range reqFormData {
|
||||
if v == nil {
|
||||
out[k] = nil
|
||||
continue
|
||||
}
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
out[k] = truncateForLog(val, maxLogParamValueLen)
|
||||
default:
|
||||
s := fmt.Sprint(v)
|
||||
out[k] = truncateForLog(s, maxLogParamValueLen)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ShumaiResponse 数脉 API 通用响应(占位,按实际文档调整)
|
||||
type ShumaiResponse struct {
|
||||
Code int `json:"code"` // 状态码
|
||||
@@ -188,7 +228,7 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, nil)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": requestParamsForLog(reqFormData)})
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -216,7 +256,7 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
err = errors.Join(ErrSystem, err)
|
||||
}
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, nil)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": requestParamsForLog(reqFormData)})
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -227,7 +267,7 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, nil)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, map[string]interface{}{"request_params": requestParamsForLog(reqFormData)})
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -235,21 +275,11 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP %d", resp.StatusCode))
|
||||
if s.logger != nil {
|
||||
var errorResponse interface{} = string(raw)
|
||||
// 尝试解析 JSON 获取 msg
|
||||
var tempResp ShumaiResponse
|
||||
if json.Unmarshal(raw, &tempResp) == nil {
|
||||
msg := tempResp.Msg
|
||||
if msg == "" {
|
||||
msg = tempResp.Message
|
||||
}
|
||||
if msg != "" {
|
||||
errorResponse = map[string]interface{}{
|
||||
"msg": msg,
|
||||
}
|
||||
}
|
||||
errorPayload := map[string]interface{}{
|
||||
"request_params": requestParamsForLog(reqFormData),
|
||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||
}
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, errorResponse)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, errorPayload)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
@@ -260,11 +290,14 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
|
||||
var shumaiResp ShumaiResponse
|
||||
if err := json.Unmarshal(raw, &shumaiResp); err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
parseErr := errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, string(raw))
|
||||
s.logger.LogError(requestID, transactionID, apiPath, parseErr, map[string]interface{}{
|
||||
"request_params": requestParamsForLog(reqFormData),
|
||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||
})
|
||||
}
|
||||
return nil, err
|
||||
return nil, parseErr
|
||||
}
|
||||
|
||||
codeStr := strconv.Itoa(shumaiResp.Code)
|
||||
@@ -279,10 +312,10 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
shumaiErr = NewShumaiError(codeStr, msg)
|
||||
}
|
||||
if s.logger != nil {
|
||||
errorResponse := map[string]interface{}{
|
||||
"msg": msg,
|
||||
}
|
||||
s.logger.LogError(requestID, transactionID, apiPath, shumaiErr, errorResponse)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, shumaiErr, map[string]interface{}{
|
||||
"request_params": requestParamsForLog(reqFormData),
|
||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||
})
|
||||
}
|
||||
if shumaiErr.IsNoRecord() {
|
||||
return nil, errors.Join(ErrNotFound, shumaiErr)
|
||||
@@ -296,14 +329,14 @@ func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqForm
|
||||
|
||||
dataBytes, err := json.Marshal(shumaiResp.Data)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("data 序列化失败: %w", err))
|
||||
marshalErr := errors.Join(ErrSystem, fmt.Errorf("data 序列化失败: %w", err))
|
||||
if s.logger != nil {
|
||||
errorResponse := map[string]interface{}{
|
||||
"msg": msg,
|
||||
}
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, errorResponse)
|
||||
s.logger.LogError(requestID, transactionID, apiPath, marshalErr, map[string]interface{}{
|
||||
"request_params": requestParamsForLog(reqFormData),
|
||||
"response_body": truncateForLog(string(raw), maxLogResponseBodyLen),
|
||||
})
|
||||
}
|
||||
return nil, err
|
||||
return nil, marshalErr
|
||||
}
|
||||
return dataBytes, nil
|
||||
}
|
||||
|
||||
@@ -210,8 +210,13 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st
|
||||
return nil, ErrDatasource
|
||||
}
|
||||
|
||||
// 201 表示查询为空,返回空对象
|
||||
// 201 表示查询为空,兼容其它情况如果data也为空,则返回空对象
|
||||
if zhichaResp.Code == "201" {
|
||||
// 先做类型断言
|
||||
dataMap, ok := zhichaResp.Data.(map[string]interface{})
|
||||
if ok && len(dataMap) > 0 {
|
||||
return dataMap, nil
|
||||
}
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -84,20 +84,25 @@ func TestEncryptWithInvalidKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDecryptWithInvalidData(t *testing.T) {
|
||||
key := "1234567890abcdef1234567890abcdef"
|
||||
key := "af4ca0098e6a202a5c08c413ebd9fd62"
|
||||
|
||||
// 测试无效的加密数据
|
||||
invalidData := []string{
|
||||
"", // 空数据
|
||||
"invalid_base64", // 无效的Base64
|
||||
"dGVzdA==", // 有效的Base64但不是AES加密数据
|
||||
"i96w+SDjwENjuvsokMFbLw==",
|
||||
"oaihmICgEcszWMk0gXoB12E/ygF4g78x0/sC3/KHnBk=",
|
||||
"5bx+WvXvdNRVVOp9UuNFHg==",
|
||||
}
|
||||
|
||||
for _, data := range invalidData {
|
||||
_, err := Decrypt(data, key)
|
||||
decrypted, err := Decrypt(data, key)
|
||||
if err == nil {
|
||||
t.Errorf("使用无效数据 %s 应该返回错误", data)
|
||||
}
|
||||
fmt.Println("data: ", data)
|
||||
fmt.Println("decrypted: ", decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
92
internal/infrastructure/http/handlers/captcha_handler.go
Normal file
92
internal/infrastructure/http/handlers/captcha_handler.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/infrastructure/external/captcha"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// CaptchaHandler 验证码(滑块)HTTP 处理器
|
||||
type CaptchaHandler struct {
|
||||
captchaService *captcha.CaptchaService
|
||||
response interfaces.ResponseBuilder
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCaptchaHandler 创建验证码处理器
|
||||
func NewCaptchaHandler(
|
||||
captchaService *captcha.CaptchaService,
|
||||
response interfaces.ResponseBuilder,
|
||||
cfg *config.Config,
|
||||
logger *zap.Logger,
|
||||
) *CaptchaHandler {
|
||||
return &CaptchaHandler{
|
||||
captchaService: captchaService,
|
||||
response: response,
|
||||
config: cfg,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptedSceneIdReq 获取加密场景 ID 的请求(可选参数)
|
||||
type EncryptedSceneIdReq struct {
|
||||
ExpireSeconds *int `form:"expire_seconds" json:"expire_seconds"` // 有效期秒数,1~86400,默认 3600
|
||||
}
|
||||
|
||||
// GetEncryptedSceneId 获取加密场景 ID,供前端加密模式初始化阿里云验证码
|
||||
// @Summary 获取验证码加密场景ID
|
||||
// @Description 用于加密模式下发 EncryptedSceneId,前端用此初始化滑块验证码
|
||||
// @Tags 验证码
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body EncryptedSceneIdReq false "可选:expire_seconds 有效期(1-86400),默认3600"
|
||||
// @Success 200 {object} map[string]interface{} "encryptedSceneId"
|
||||
// @Failure 400 {object} map[string]interface{} "配置未启用或参数错误"
|
||||
// @Failure 500 {object} map[string]interface{} "服务器内部错误"
|
||||
// @Router /api/v1/captcha/encryptedSceneId [post]
|
||||
func (h *CaptchaHandler) GetEncryptedSceneId(c *gin.Context) {
|
||||
expireSec := 3600
|
||||
if c.Request.ContentLength > 0 {
|
||||
var req EncryptedSceneIdReq
|
||||
if err := c.ShouldBindJSON(&req); err == nil && req.ExpireSeconds != nil {
|
||||
expireSec = *req.ExpireSeconds
|
||||
}
|
||||
}
|
||||
if expireSec <= 0 || expireSec > 86400 {
|
||||
h.response.BadRequest(c, "expire_seconds 必须在 1~86400 之间")
|
||||
return
|
||||
}
|
||||
|
||||
encrypted, err := h.captchaService.GetEncryptedSceneId(expireSec)
|
||||
if err != nil {
|
||||
if err == captcha.ErrCaptchaEncryptMissing || err == captcha.ErrCaptchaConfig {
|
||||
h.logger.Warn("验证码加密场景ID生成失败", zap.Error(err))
|
||||
h.response.BadRequest(c, "验证码加密模式未配置或配置错误")
|
||||
return
|
||||
}
|
||||
h.logger.Error("验证码加密场景ID生成失败", zap.Error(err))
|
||||
h.response.InternalError(c, "生成失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
|
||||
h.response.Success(c, map[string]string{"encryptedSceneId": encrypted}, "ok")
|
||||
}
|
||||
|
||||
// GetConfig 获取验证码前端配置(是否启用、场景ID等),便于前端决定是否展示滑块
|
||||
// @Summary 获取验证码配置
|
||||
// @Description 返回是否启用滑块、场景ID(非加密模式用)
|
||||
// @Tags 验证码
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{} "captchaEnabled, sceneId"
|
||||
// @Router /api/v1/captcha/config [get]
|
||||
func (h *CaptchaHandler) GetConfig(c *gin.Context) {
|
||||
data := map[string]interface{}{
|
||||
"captchaEnabled": h.config.SMS.CaptchaEnabled,
|
||||
"sceneId": h.config.SMS.SceneID,
|
||||
}
|
||||
h.response.Success(c, data, "ok")
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handlers
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -42,8 +43,8 @@ func (h *PDFGHandler) DownloadPDF(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 从缓存获取PDF
|
||||
pdfBytes, hit, createdAt, err := h.cacheManager.GetByCacheKey(reportID)
|
||||
// 通过报告ID获取PDF文件
|
||||
pdfBytes, hit, createdAt, err := h.cacheManager.GetByReportID(reportID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取PDF缓存失败",
|
||||
zap.String("report_id", reportID),
|
||||
@@ -74,7 +75,14 @@ func (h *PDFGHandler) DownloadPDF(c *gin.Context) {
|
||||
|
||||
// 设置响应头
|
||||
c.Header("Content-Type", "application/pdf")
|
||||
c.Header("Content-Disposition", `attachment; filename="大数据租赁风险报告.pdf"`)
|
||||
// 使用报告ID前缀作为下载文件名的一部分
|
||||
filename := "大数据租赁风险报告.pdf"
|
||||
if idx := strings.LastIndex(reportID, "-"); idx > 0 {
|
||||
// 使用前缀(报告编号部分)作为文件名的一部分
|
||||
prefix := reportID[:idx]
|
||||
filename = fmt.Sprintf("大数据租赁风险报告_%s.pdf", prefix)
|
||||
}
|
||||
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
||||
c.Header("Content-Length", fmt.Sprintf("%d", len(pdfBytes)))
|
||||
|
||||
// 发送PDF文件
|
||||
|
||||
137
internal/infrastructure/http/handlers/qygl_report_handler.go
Normal file
137
internal/infrastructure/http/handlers/qygl_report_handler.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"tyapi-server/internal/application/api/commands"
|
||||
"tyapi-server/internal/domains/api/dto"
|
||||
api_repositories "tyapi-server/internal/domains/api/repositories"
|
||||
api_services "tyapi-server/internal/domains/api/services"
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||
)
|
||||
|
||||
// QYGLReportHandler 企业全景报告页面渲染处理器
|
||||
// 使用 QYGLJ1U9 聚合接口生成企业报告数据,并通过模板引擎渲染 qiye.html
|
||||
type QYGLReportHandler struct {
|
||||
apiRequestService *api_services.ApiRequestService
|
||||
logger *zap.Logger
|
||||
|
||||
reportRepo api_repositories.ReportRepository
|
||||
}
|
||||
|
||||
// NewQYGLReportHandler 创建企业报告页面处理器
|
||||
func NewQYGLReportHandler(
|
||||
apiRequestService *api_services.ApiRequestService,
|
||||
logger *zap.Logger,
|
||||
reportRepo api_repositories.ReportRepository,
|
||||
) *QYGLReportHandler {
|
||||
return &QYGLReportHandler{
|
||||
apiRequestService: apiRequestService,
|
||||
logger: logger,
|
||||
reportRepo: reportRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// GetQYGLReportPage 企业全景报告页面
|
||||
// GET /reports/qygl?ent_code=xxx&ent_name=yyy&ent_reg_no=zzz
|
||||
func (h *QYGLReportHandler) GetQYGLReportPage(c *gin.Context) {
|
||||
// 读取查询参数
|
||||
entCode := c.Query("ent_code")
|
||||
entName := c.Query("ent_name")
|
||||
entRegNo := c.Query("ent_reg_no")
|
||||
|
||||
// 组装 QYGLUY3S 入参
|
||||
req := dto.QYGLUY3SReq{
|
||||
EntName: entName,
|
||||
EntRegno: entRegNo,
|
||||
EntCode: entCode,
|
||||
}
|
||||
|
||||
params, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
h.logger.Error("序列化企业全景报告入参失败", zap.Error(err))
|
||||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
|
||||
// 通过 ApiRequestService 调用 QYGLJ1U9 聚合处理器
|
||||
options := &commands.ApiCallOptions{}
|
||||
callCtx := &processors.CallContext{}
|
||||
|
||||
ctx := c.Request.Context()
|
||||
respBytes, err := h.apiRequestService.PreprocessRequestApi(ctx, "QYGLJ1U9", params, options, callCtx)
|
||||
if err != nil {
|
||||
h.logger.Error("调用企业全景报告处理器失败", zap.Error(err))
|
||||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
|
||||
var report map[string]interface{}
|
||||
if err := json.Unmarshal(respBytes, &report); err != nil {
|
||||
h.logger.Error("解析企业全景报告结果失败", zap.Error(err))
|
||||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
|
||||
reportJSONBytes, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
h.logger.Error("序列化企业全景报告结果失败", zap.Error(err))
|
||||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后重试")
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 template.JS 避免在脚本中被转义,直接作为 JS 对象字面量注入
|
||||
reportJSON := template.JS(reportJSONBytes)
|
||||
|
||||
c.HTML(http.StatusOK, "qiye.html", gin.H{
|
||||
"ReportJSON": reportJSON,
|
||||
})
|
||||
}
|
||||
|
||||
// GetQYGLReportPageByID 通过编号查看企业全景报告页面
|
||||
// GET /reports/qygl/:id
|
||||
func (h *QYGLReportHandler) GetQYGLReportPageByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.String(http.StatusBadRequest, "报告编号不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 优先从数据库中查询报告记录
|
||||
if h.reportRepo != nil {
|
||||
if entity, err := h.reportRepo.FindByReportID(c.Request.Context(), id); err == nil && entity != nil {
|
||||
reportJSON := template.JS(entity.ReportData)
|
||||
c.HTML(http.StatusOK, "qiye.html", gin.H{
|
||||
"ReportJSON": reportJSON,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到进程内存缓存(兼容老的访问方式)
|
||||
report, ok := qygl.GetQYGLReport(id)
|
||||
if !ok {
|
||||
c.String(http.StatusNotFound, "报告不存在或已过期")
|
||||
return
|
||||
}
|
||||
|
||||
reportJSONBytes, err := json.Marshal(report)
|
||||
if err != nil {
|
||||
h.logger.Error("序列化企业全景报告结果失败", zap.Error(err))
|
||||
c.String(http.StatusInternalServerError, "生成企业报告失败,请稍后再试")
|
||||
return
|
||||
}
|
||||
|
||||
reportJSON := template.JS(reportJSONBytes)
|
||||
|
||||
c.HTML(http.StatusOK, "qiye.html", gin.H{
|
||||
"ReportJSON": reportJSON,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
@@ -11,6 +15,8 @@ import (
|
||||
"tyapi-server/internal/application/user/dto/commands"
|
||||
"tyapi-server/internal/application/user/dto/queries"
|
||||
_ "tyapi-server/internal/application/user/dto/responses"
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/crypto"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
"tyapi-server/internal/shared/middleware"
|
||||
)
|
||||
@@ -22,6 +28,8 @@ type UserHandler struct {
|
||||
validator interfaces.RequestValidator
|
||||
logger *zap.Logger
|
||||
jwtAuth *middleware.JWTAuthMiddleware
|
||||
config *config.Config
|
||||
cache interfaces.CacheService
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户处理器
|
||||
@@ -31,6 +39,8 @@ func NewUserHandler(
|
||||
validator interfaces.RequestValidator,
|
||||
logger *zap.Logger,
|
||||
jwtAuth *middleware.JWTAuthMiddleware,
|
||||
cfg *config.Config,
|
||||
cache interfaces.CacheService,
|
||||
) *UserHandler {
|
||||
return &UserHandler{
|
||||
appService: appService,
|
||||
@@ -38,16 +48,27 @@ func NewUserHandler(
|
||||
validator: validator,
|
||||
logger: logger,
|
||||
jwtAuth: jwtAuth,
|
||||
config: cfg,
|
||||
cache: cache,
|
||||
}
|
||||
}
|
||||
|
||||
// decodedSendCodeData 解码后的请求数据结构
|
||||
type decodedSendCodeData struct {
|
||||
Phone string `json:"phone"`
|
||||
Scene string `json:"scene"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Nonce string `json:"nonce"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// SendCode 发送验证码
|
||||
// @Summary 发送短信验证码
|
||||
// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景
|
||||
// @Description 向指定手机号发送验证码,支持注册、登录、修改密码等场景。需要提供有效的签名验证。只接收编码后的data字段(使用自定义编码方案)
|
||||
// @Tags 用户认证
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body commands.SendCodeCommand true "发送验证码请求"
|
||||
// @Param request body commands.SendCodeCommand true "发送验证码请求(包含data字段和可选的captchaVerifyParam字段)"
|
||||
// @Success 200 {object} map[string]interface{} "验证码发送成功"
|
||||
// @Failure 400 {object} map[string]interface{} "请求参数错误"
|
||||
// @Failure 429 {object} map[string]interface{} "请求频率限制"
|
||||
@@ -55,14 +76,65 @@ func NewUserHandler(
|
||||
// @Router /api/v1/users/send-code [post]
|
||||
func (h *UserHandler) SendCode(c *gin.Context) {
|
||||
var cmd commands.SendCodeCommand
|
||||
if err := h.validator.BindAndValidate(c, &cmd); err != nil {
|
||||
|
||||
// 绑定请求(包含data字段和可选的captchaVerifyParam字段)
|
||||
if err := c.ShouldBindJSON(&cmd); err != nil {
|
||||
h.response.BadRequest(c, "请求参数格式错误,必须提供data字段")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证data字段不为空
|
||||
if cmd.Data == "" {
|
||||
h.response.BadRequest(c, "data字段不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 解码自定义编码的数据
|
||||
decodedData, err := h.decodeRequestData(cmd.Data)
|
||||
if err != nil {
|
||||
h.logger.Warn("解码请求数据失败",
|
||||
zap.String("client_ip", c.ClientIP()),
|
||||
zap.Error(err))
|
||||
h.response.BadRequest(c, "请求数据解码失败")
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必要字段
|
||||
if decodedData.Phone == "" || decodedData.Scene == "" {
|
||||
h.response.BadRequest(c, "手机号和场景不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 如果启用了签名验证,进行签名校验(包含nonce唯一性检查,防止重放攻击)
|
||||
if h.config.SMS.SignatureEnabled {
|
||||
if err := h.verifyDecodedSignature(c.Request.Context(), decodedData); err != nil {
|
||||
h.logger.Warn("短信发送签名验证失败",
|
||||
zap.String("phone", decodedData.Phone),
|
||||
zap.String("scene", decodedData.Scene),
|
||||
zap.String("client_ip", c.ClientIP()),
|
||||
zap.Error(err))
|
||||
|
||||
// 根据错误类型返回不同的用户友好消息(不暴露技术细节)
|
||||
userMessage := h.getSignatureErrorMessage(err)
|
||||
h.response.BadRequest(c, userMessage)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 构建SendCodeCommand用于调用应用服务
|
||||
serviceCmd := &commands.SendCodeCommand{
|
||||
Phone: decodedData.Phone,
|
||||
Scene: decodedData.Scene,
|
||||
Timestamp: decodedData.Timestamp,
|
||||
Nonce: decodedData.Nonce,
|
||||
Signature: decodedData.Signature,
|
||||
CaptchaVerifyParam: cmd.CaptchaVerifyParam,
|
||||
}
|
||||
|
||||
clientIP := c.ClientIP()
|
||||
userAgent := c.GetHeader("User-Agent")
|
||||
|
||||
if err := h.appService.SendCode(c.Request.Context(), &cmd, clientIP, userAgent); err != nil {
|
||||
if err := h.appService.SendCode(c.Request.Context(), serviceCmd, clientIP, userAgent); err != nil {
|
||||
h.response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -70,6 +142,67 @@ func (h *UserHandler) SendCode(c *gin.Context) {
|
||||
h.response.Success(c, nil, "验证码发送成功")
|
||||
}
|
||||
|
||||
// decodeRequestData 解码自定义编码的请求数据
|
||||
func (h *UserHandler) decodeRequestData(encodedData string) (*decodedSendCodeData, error) {
|
||||
// 使用自定义编码方案解码
|
||||
decodedData, err := crypto.DecodeRequest(encodedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("自定义编码解码失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析JSON
|
||||
var decoded decodedSendCodeData
|
||||
if err := json.Unmarshal([]byte(decodedData), &decoded); err != nil {
|
||||
return nil, fmt.Errorf("JSON解析失败: %w", err)
|
||||
}
|
||||
|
||||
return &decoded, nil
|
||||
}
|
||||
|
||||
// verifyDecodedSignature 验证解码后的签名(包含nonce唯一性检查,防止重放攻击)
|
||||
func (h *UserHandler) verifyDecodedSignature(ctx context.Context, data *decodedSendCodeData) error {
|
||||
// 构建参数map(包含signature字段,VerifySignature会自动排除它)
|
||||
params := map[string]string{
|
||||
"phone": data.Phone,
|
||||
"scene": data.Scene,
|
||||
"signature": data.Signature,
|
||||
}
|
||||
|
||||
// 验证签名并检查nonce唯一性(防止重放攻击)
|
||||
return crypto.VerifySignatureWithNonceCheck(
|
||||
ctx,
|
||||
params,
|
||||
h.config.SMS.SignatureSecret,
|
||||
data.Timestamp,
|
||||
data.Nonce,
|
||||
h.cache,
|
||||
"sms:signature", // 缓存键前缀
|
||||
)
|
||||
}
|
||||
|
||||
// getSignatureErrorMessage 根据错误类型返回用户友好的错误消息(不暴露技术细节)
|
||||
func (h *UserHandler) getSignatureErrorMessage(err error) string {
|
||||
errMsg := err.Error()
|
||||
|
||||
// 根据错误消息内容判断错误类型,返回通用的用户友好消息
|
||||
if strings.Contains(errMsg, "请求已被使用") || strings.Contains(errMsg, "重复提交") {
|
||||
// 重放攻击:返回通用消息,不暴露具体原因
|
||||
return "请求无效,请重新操作"
|
||||
}
|
||||
if strings.Contains(errMsg, "时间戳") || strings.Contains(errMsg, "过期") {
|
||||
// 时间戳过期:返回通用消息
|
||||
return "请求已过期,请重新操作"
|
||||
}
|
||||
if strings.Contains(errMsg, "签名") {
|
||||
// 签名错误:返回通用消息
|
||||
return "请求验证失败,请重新操作"
|
||||
}
|
||||
|
||||
// 其他错误:返回通用消息
|
||||
return "请求验证失败,请重新操作"
|
||||
}
|
||||
|
||||
|
||||
// Register 用户注册
|
||||
// @Summary 用户注册
|
||||
// @Description 使用手机号、密码和验证码进行用户注册,需要确认密码
|
||||
|
||||
33
internal/infrastructure/http/routes/captcha_routes.go
Normal file
33
internal/infrastructure/http/routes/captcha_routes.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CaptchaRoutes 验证码路由
|
||||
type CaptchaRoutes struct {
|
||||
handler *handlers.CaptchaHandler
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewCaptchaRoutes 创建验证码路由
|
||||
func NewCaptchaRoutes(handler *handlers.CaptchaHandler, logger *zap.Logger) *CaptchaRoutes {
|
||||
return &CaptchaRoutes{
|
||||
handler: handler,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 注册验证码相关路由
|
||||
func (r *CaptchaRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
engine := router.GetEngine()
|
||||
captchaGroup := engine.Group("/api/v1/captcha")
|
||||
{
|
||||
captchaGroup.POST("/encryptedSceneId", r.handler.GetEncryptedSceneId)
|
||||
captchaGroup.GET("/config", r.handler.GetConfig)
|
||||
}
|
||||
r.logger.Info("验证码路由注册完成")
|
||||
}
|
||||
32
internal/infrastructure/http/routes/qygl_report_routes.go
Normal file
32
internal/infrastructure/http/routes/qygl_report_routes.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/infrastructure/http/handlers"
|
||||
sharedhttp "tyapi-server/internal/shared/http"
|
||||
)
|
||||
|
||||
// QYGLReportRoutes 企业报告页面路由注册器
|
||||
type QYGLReportRoutes struct {
|
||||
handler *handlers.QYGLReportHandler
|
||||
}
|
||||
|
||||
// NewQYGLReportRoutes 创建企业报告页面路由注册器
|
||||
func NewQYGLReportRoutes(
|
||||
handler *handlers.QYGLReportHandler,
|
||||
) *QYGLReportRoutes {
|
||||
return &QYGLReportRoutes{
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 注册企业报告页面路由
|
||||
func (r *QYGLReportRoutes) Register(router *sharedhttp.GinRouter) {
|
||||
engine := router.GetEngine()
|
||||
|
||||
// 企业全景报告页面(实时生成)
|
||||
engine.GET("/reports/qygl", r.handler.GetQYGLReportPage)
|
||||
|
||||
// 企业全景报告页面(通过编号查看)
|
||||
engine.GET("/reports/qygl/:id", r.handler.GetQYGLReportPageByID)
|
||||
}
|
||||
|
||||
358
internal/shared/crypto/signature.go
Normal file
358
internal/shared/crypto/signature.go
Normal file
@@ -0,0 +1,358 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
const (
|
||||
// SignatureTimestampTolerance 签名时间戳容差(秒),防止重放攻击
|
||||
SignatureTimestampTolerance = 300 // 5分钟
|
||||
)
|
||||
|
||||
// GenerateSignature 生成HMAC-SHA256签名
|
||||
// params: 需要签名的参数map
|
||||
// secretKey: 签名密钥
|
||||
// timestamp: 时间戳(秒)
|
||||
// nonce: 随机字符串
|
||||
func GenerateSignature(params map[string]string, secretKey string, timestamp int64, nonce string) string {
|
||||
// 1. 构建待签名字符串:按key排序,拼接成 key1=value1&key2=value2 格式
|
||||
var keys []string
|
||||
for k := range params {
|
||||
if k != "signature" { // 排除签名字段本身
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var parts []string
|
||||
for _, k := range keys {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
|
||||
}
|
||||
|
||||
// 2. 添加时间戳和随机数
|
||||
parts = append(parts, fmt.Sprintf("timestamp=%d", timestamp))
|
||||
parts = append(parts, fmt.Sprintf("nonce=%s", nonce))
|
||||
|
||||
// 3. 拼接成待签名字符串
|
||||
signString := strings.Join(parts, "&")
|
||||
|
||||
// 4. 使用HMAC-SHA256计算签名
|
||||
mac := hmac.New(sha256.New, []byte(secretKey))
|
||||
mac.Write([]byte(signString))
|
||||
signature := mac.Sum(nil)
|
||||
|
||||
// 5. 返回hex编码的签名
|
||||
return hex.EncodeToString(signature)
|
||||
}
|
||||
|
||||
// VerifySignature 验证HMAC-SHA256签名
|
||||
// params: 请求参数map(包含signature字段)
|
||||
// secretKey: 签名密钥
|
||||
// timestamp: 时间戳(秒)
|
||||
// nonce: 随机字符串
|
||||
func VerifySignature(params map[string]string, secretKey string, timestamp int64, nonce string) error {
|
||||
// 1. 检查签名字段是否存在
|
||||
signature, exists := params["signature"]
|
||||
if !exists || signature == "" {
|
||||
return errors.New("签名字段缺失")
|
||||
}
|
||||
|
||||
// 2. 验证时间戳(防止重放攻击)
|
||||
now := time.Now().Unix()
|
||||
if timestamp <= 0 {
|
||||
return errors.New("时间戳无效")
|
||||
}
|
||||
if abs(now-timestamp) > SignatureTimestampTolerance {
|
||||
return fmt.Errorf("请求已过期,时间戳超出容差范围(当前时间:%d,请求时间:%d)", now, timestamp)
|
||||
}
|
||||
|
||||
// 3. 重新计算签名
|
||||
expectedSignature := GenerateSignature(params, secretKey, timestamp, nonce)
|
||||
|
||||
// 4. 将hex字符串转换为字节数组进行比较
|
||||
signatureBytes, err := hex.DecodeString(signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("签名格式错误: %w", err)
|
||||
}
|
||||
expectedBytes, err := hex.DecodeString(expectedSignature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("签名计算错误: %w", err)
|
||||
}
|
||||
|
||||
// 5. 使用常量时间比较防止时序攻击
|
||||
if !hmac.Equal(signatureBytes, expectedBytes) {
|
||||
return errors.New("签名验证失败")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifySignatureWithNonceCheck 验证HMAC-SHA256签名并检查nonce唯一性(防止重放攻击)
|
||||
// params: 请求参数map(包含signature字段)
|
||||
// secretKey: 签名密钥
|
||||
// timestamp: 时间戳(秒)
|
||||
// nonce: 随机字符串
|
||||
// cache: 缓存服务,用于存储已使用的nonce
|
||||
// cacheKeyPrefix: 缓存键前缀
|
||||
func VerifySignatureWithNonceCheck(
|
||||
ctx context.Context,
|
||||
params map[string]string,
|
||||
secretKey string,
|
||||
timestamp int64,
|
||||
nonce string,
|
||||
cache interfaces.CacheService,
|
||||
cacheKeyPrefix string,
|
||||
) error {
|
||||
// 1. 先进行基础签名验证
|
||||
if err := VerifySignature(params, secretKey, timestamp, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 检查nonce是否已被使用(防止重放攻击)
|
||||
// 使用请求指纹:phone+timestamp+nonce 作为唯一标识
|
||||
phone := params["phone"]
|
||||
if phone == "" {
|
||||
return errors.New("手机号不能为空")
|
||||
}
|
||||
|
||||
// 构建nonce唯一性检查的缓存键
|
||||
nonceKey := fmt.Sprintf("%s:nonce:%s:%d:%s", cacheKeyPrefix, phone, timestamp, nonce)
|
||||
|
||||
// 检查nonce是否已被使用
|
||||
exists, err := cache.Exists(ctx, nonceKey)
|
||||
if err != nil {
|
||||
// 缓存查询失败,记录错误但继续验证(避免缓存故障导致服务不可用)
|
||||
return fmt.Errorf("检查nonce唯一性失败: %w", err)
|
||||
}
|
||||
if exists {
|
||||
return errors.New("请求已被使用,请勿重复提交")
|
||||
}
|
||||
|
||||
// 3. 将nonce标记为已使用,TTL设置为时间戳容差+1分钟(确保在容差范围内不会重复使用)
|
||||
ttl := time.Duration(SignatureTimestampTolerance+60) * time.Second
|
||||
if err := cache.Set(ctx, nonceKey, true, ttl); err != nil {
|
||||
// 记录错误但不影响验证流程(避免缓存故障导致服务不可用)
|
||||
return fmt.Errorf("标记nonce已使用失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 自定义编码字符集(不使用标准Base64字符集,增加破解难度)
|
||||
// 使用自定义字符集:数字+大写字母(排除易混淆的I和O)+小写字母(排除易混淆的i和l)+特殊字符
|
||||
const customEncodeCharset = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?"
|
||||
|
||||
// EncodeRequest 使用自定义编码方案编码请求参数
|
||||
// 编码方式:类似Base64,但使用自定义字符集,并加入简单的混淆
|
||||
func EncodeRequest(data string) string {
|
||||
// 1. 将字符串转换为字节数组
|
||||
bytes := []byte(data)
|
||||
|
||||
// 2. 使用自定义Base64变种编码
|
||||
encoded := customBase64Encode(bytes)
|
||||
|
||||
// 3. 添加简单的字符混淆(字符偏移)
|
||||
confused := applyCharShift(encoded, 7) // 偏移7个位置
|
||||
|
||||
return confused
|
||||
}
|
||||
|
||||
// DecodeRequest 解码请求参数
|
||||
func DecodeRequest(encodedData string) (string, error) {
|
||||
// 1. 先还原字符混淆
|
||||
unconfused := reverseCharShift(encodedData, 7)
|
||||
|
||||
// 2. 使用自定义Base64变种解码
|
||||
decoded, err := customBase64Decode(unconfused)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解码失败: %w", err)
|
||||
}
|
||||
|
||||
return string(decoded), nil
|
||||
}
|
||||
|
||||
// customBase64Encode 自定义Base64编码(使用自定义字符集)
|
||||
func customBase64Encode(data []byte) string {
|
||||
if len(data) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result []byte
|
||||
charset := []byte(customEncodeCharset)
|
||||
|
||||
// 将3个字节(24位)编码为4个字符
|
||||
for i := 0; i < len(data); i += 3 {
|
||||
// 读取3个字节
|
||||
var b1, b2, b3 byte
|
||||
b1 = data[i]
|
||||
if i+1 < len(data) {
|
||||
b2 = data[i+1]
|
||||
}
|
||||
if i+2 < len(data) {
|
||||
b3 = data[i+2]
|
||||
}
|
||||
|
||||
// 组合成24位
|
||||
combined := uint32(b1)<<16 | uint32(b2)<<8 | uint32(b3)
|
||||
|
||||
// 分成4个6位段
|
||||
result = append(result, charset[(combined>>18)&0x3F])
|
||||
result = append(result, charset[(combined>>12)&0x3F])
|
||||
|
||||
if i+1 < len(data) {
|
||||
result = append(result, charset[(combined>>6)&0x3F])
|
||||
} else {
|
||||
result = append(result, '=') // 填充字符
|
||||
}
|
||||
|
||||
if i+2 < len(data) {
|
||||
result = append(result, charset[combined&0x3F])
|
||||
} else {
|
||||
result = append(result, '=') // 填充字符
|
||||
}
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// customBase64Decode 自定义Base64解码
|
||||
func customBase64Decode(encoded string) ([]byte, error) {
|
||||
if len(encoded) == 0 {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
charset := []byte(customEncodeCharset)
|
||||
charsetMap := make(map[byte]int)
|
||||
for i, c := range charset {
|
||||
charsetMap[c] = i
|
||||
}
|
||||
|
||||
var result []byte
|
||||
data := []byte(encoded)
|
||||
|
||||
// 将4个字符解码为3个字节
|
||||
for i := 0; i < len(data); i += 4 {
|
||||
if i+3 >= len(data) {
|
||||
return nil, fmt.Errorf("编码数据长度不正确")
|
||||
}
|
||||
|
||||
// 获取4个字符的索引
|
||||
var idx [4]int
|
||||
for j := 0; j < 4; j++ {
|
||||
if data[i+j] == '=' {
|
||||
idx[j] = 0 // 填充字符
|
||||
} else {
|
||||
val, ok := charsetMap[data[i+j]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("无效的编码字符: %c", data[i+j])
|
||||
}
|
||||
idx[j] = val
|
||||
}
|
||||
}
|
||||
|
||||
// 组合成24位
|
||||
combined := uint32(idx[0])<<18 | uint32(idx[1])<<12 | uint32(idx[2])<<6 | uint32(idx[3])
|
||||
|
||||
// 提取3个字节
|
||||
result = append(result, byte((combined>>16)&0xFF))
|
||||
if data[i+2] != '=' {
|
||||
result = append(result, byte((combined>>8)&0xFF))
|
||||
}
|
||||
if data[i+3] != '=' {
|
||||
result = append(result, byte(combined&0xFF))
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// applyCharShift 应用字符偏移混淆
|
||||
func applyCharShift(data string, shift int) string {
|
||||
charset := customEncodeCharset
|
||||
charsetLen := len(charset)
|
||||
result := make([]byte, len(data))
|
||||
|
||||
for i, c := range []byte(data) {
|
||||
if c == '=' {
|
||||
result[i] = c // 填充字符不变
|
||||
continue
|
||||
}
|
||||
|
||||
// 查找字符在字符集中的位置
|
||||
idx := -1
|
||||
for j, ch := range []byte(charset) {
|
||||
if ch == c {
|
||||
idx = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
result[i] = c // 不在字符集中,保持不变
|
||||
} else {
|
||||
// 应用偏移
|
||||
newIdx := (idx + shift) % charsetLen
|
||||
result[i] = charset[newIdx]
|
||||
}
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// reverseCharShift 还原字符偏移混淆
|
||||
func reverseCharShift(data string, shift int) string {
|
||||
charset := customEncodeCharset
|
||||
charsetLen := len(charset)
|
||||
result := make([]byte, len(data))
|
||||
|
||||
for i, c := range []byte(data) {
|
||||
if c == '=' {
|
||||
result[i] = c // 填充字符不变
|
||||
continue
|
||||
}
|
||||
|
||||
// 查找字符在字符集中的位置
|
||||
idx := -1
|
||||
for j, ch := range []byte(charset) {
|
||||
if ch == c {
|
||||
idx = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
result[i] = c // 不在字符集中,保持不变
|
||||
} else {
|
||||
// 还原偏移
|
||||
newIdx := (idx - shift + charsetLen) % charsetLen
|
||||
result[i] = charset[newIdx]
|
||||
}
|
||||
}
|
||||
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// abs 计算绝对值
|
||||
func abs(x int64) int64 {
|
||||
if x < 0 {
|
||||
return -x
|
||||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// ParseTimestamp 从字符串解析时间戳
|
||||
func ParseTimestamp(ts string) (int64, error) {
|
||||
return strconv.ParseInt(ts, 10, 64)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
@@ -37,6 +38,18 @@ func NewGinRouter(cfg *config.Config, logger *zap.Logger) *GinRouter {
|
||||
// 创建Gin引擎
|
||||
engine := gin.New()
|
||||
|
||||
// 加载HTML模板(企业报告等页面)
|
||||
// 为避免生产环境文件不存在导致panic,这里先检查文件是否存在
|
||||
const reportTemplatePath = "resources/qiye.html"
|
||||
if _, err := os.Stat(reportTemplatePath); err == nil {
|
||||
engine.LoadHTMLFiles(reportTemplatePath)
|
||||
logger.Info("已加载企业报告模板文件", zap.String("template", reportTemplatePath))
|
||||
} else {
|
||||
logger.Warn("未找到企业报告模板文件,将跳过模板加载(请确认部署时包含 resources/qiye.html)",
|
||||
zap.String("template", reportTemplatePath),
|
||||
zap.Error(err))
|
||||
}
|
||||
|
||||
return &GinRouter{
|
||||
engine: engine,
|
||||
config: cfg,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user