Compare commits

...

92 Commits

Author SHA1 Message Date
c5970da195 f 2026-03-10 19:15:54 +08:00
1bcb4a9c2e f 2026-03-10 19:12:35 +08:00
8877cf9691 f 2026-03-10 19:07:29 +08:00
f63e6df9f9 f 2026-03-10 19:03:42 +08:00
a00fe12141 f 2026-03-10 17:56:31 +08:00
6b80182986 f 2026-03-10 17:49:53 +08:00
bf4c114ee2 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-10 17:28:22 +08:00
4cd3954574 f 2026-03-10 17:28:04 +08:00
Mrx
d7a5589873 f 2026-03-09 11:31:40 +08:00
Mrx
b0ec75d1af f 2026-03-06 16:39:00 +08:00
Mrx
57d18be972 f 2026-03-06 16:28:25 +08:00
Mrx
3d8775b6dc f 2026-03-06 15:20:27 +08:00
Mrx
f40950f890 f 2026-03-06 15:12:58 +08:00
Mrx
ba21a8f965 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-06 10:56:37 +08:00
Mrx
7d2716da7a f 2026-03-06 10:56:36 +08:00
9a7bda9527 f 2026-03-05 19:03:18 +08:00
abdae033f0 f 2026-03-05 18:44:17 +08:00
96abacd392 f 2026-03-05 18:41:00 +08:00
4e6c93413e Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-05 17:44:59 +08:00
9c8dbd458f f 2026-03-05 17:44:50 +08:00
Mrx
9e9cee02f5 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-05 16:24:56 +08:00
Mrx
360bd579ce f 2026-03-05 16:24:53 +08:00
db889ccba0 f 2026-03-05 14:17:48 +08:00
25a4961328 f 2026-03-05 12:26:02 +08:00
Mrx
578e68a76b f 2026-03-05 11:05:01 +08:00
Mrx
019e47896d f 2026-03-05 10:57:06 +08:00
Mrx
c0898e6829 f 2026-03-05 10:54:16 +08:00
Mrx
4ee6e891cd f 2026-03-04 14:10:30 +08:00
Mrx
44b5f6b145 f 2026-03-04 13:20:44 +08:00
Mrx
677b7362cf f 2026-03-04 13:19:55 +08:00
Mrx
02dbc02fe8 f 2026-03-04 12:59:45 +08:00
Mrx
374143995e f 2026-03-04 12:41:08 +08:00
Mrx
7a957a6b87 f 2026-03-04 12:39:01 +08:00
Mrx
c885d562ee f 2026-03-02 19:21:23 +08:00
Mrx
9f36cd8b63 f 2026-03-02 12:21:25 +08:00
Mrx
4122f874fc f 2026-03-02 12:16:27 +08:00
Mrx
9a32387b21 f 2026-03-02 11:56:47 +08:00
Mrx
7bf9150cfc f 2026-03-02 11:39:53 +08:00
Mrx
fecd5a38fd Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-03-02 11:38:30 +08:00
Mrx
2636d9dff6 f 2026-03-02 11:38:19 +08:00
927b08b871 f 2026-02-28 14:49:42 +08:00
dedd4a60a4 f 2026-02-28 14:05:54 +08:00
a54a19e439 f 2026-02-27 16:49:45 +08:00
6dd392f673 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-02-27 16:43:06 +08:00
8d5da9d88e f 2026-02-27 16:42:38 +08:00
Mrx
bc6dce21ee f 2026-02-27 14:59:23 +08:00
Mrx
5630d93de6 f 2026-02-27 14:53:08 +08:00
Mrx
d12529307b add 2026-02-27 14:49:29 +08:00
f17e22f4c8 f 2026-02-25 19:33:56 +08:00
8c0c16006e f 2026-02-25 17:50:28 +08:00
532503ffe3 f 2026-02-25 17:45:14 +08:00
af37f8620c f 2026-02-25 14:54:49 +08:00
fb495bc0ca f 2026-02-14 17:42:59 +08:00
2c57ac0dab Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-02-14 17:26:15 +08:00
4b437ecf56 f 2026-02-14 17:26:01 +08:00
Mrx
abf6284482 open judiciaRiskInfos 2026-02-14 16:06:28 +08:00
e6ab833099 f 2026-02-12 13:43:03 +08:00
47cbc5b3a5 f 2026-02-12 13:36:24 +08:00
f400052f95 f 2026-02-12 13:27:08 +08:00
Mrx
a38c58c357 de|| paramsDto.IDCard == "320682198910134998" 2026-02-12 12:48:36 +08:00
Mrx
ede446fb90 f 2026-02-12 11:35:09 +08:00
e23897a13f f 2026-02-11 20:15:24 +08:00
debd0a973f f 2026-02-11 20:12:58 +08:00
b424082f01 f 2026-02-11 20:08:27 +08:00
4afbd7dec7 f 2026-02-11 19:39:27 +08:00
070310cfa7 f 2026-02-11 19:15:38 +08:00
Mrx
62e220a7b8 f 2026-02-10 18:00:20 +08:00
Mrx
c37cf2b54a f 2026-02-10 17:37:15 +08:00
Mrx
25a3b4a761 f 2026-02-10 15:36:48 +08:00
Mrx
1c83102e06 f 2026-02-10 14:32:58 +08:00
Mrx
d55520e69c f 2026-02-10 11:47:24 +08:00
Mrx
4ea7cf4ecb ff 2026-02-10 11:27:33 +08:00
Mrx
3eae08a576 f 2026-02-08 12:32:26 +08:00
Mrx
c9126dd780 ff 2026-02-08 11:54:32 +08:00
Mrx
f48289b32b f 2026-01-31 17:43:31 +08:00
Mrx
0091e01574 f 2026-01-31 12:27:11 +08:00
Mrx
d2b806eda0 f 2026-01-31 12:20:32 +08:00
Mrx
ff86cb6fb9 f 2026-01-31 12:15:42 +08:00
Mrx
a6f858dbd3 f 2026-01-31 11:32:53 +08:00
Mrx
6a1a59de8d f 2026-01-30 18:32:16 +08:00
Mrx
dbcecde4e0 f 2026-01-30 18:25:30 +08:00
Mrx
3158bf8c04 f 2026-01-29 17:10:44 +08:00
Mrx
860756b767 Merge branch 'main' of http://1.117.67.95:3000/team/tyapi-server 2026-01-29 16:51:15 +08:00
Mrx
3c90144d51 f 2026-01-29 16:51:11 +08:00
29c49d6a00 f 2026-01-29 16:40:19 +08:00
5bff33547c f 2026-01-29 16:34:37 +08:00
f50e11a052 f 2026-01-29 16:12:42 +08:00
d1e06984ac f 2026-01-29 16:00:51 +08:00
2325a110b6 f 2026-01-29 15:48:07 +08:00
167dd63a7a f 2026-01-29 15:35:25 +08:00
360fed3907 f 2026-01-29 15:27:45 +08:00
88787c6145 f 2026-01-29 15:25:30 +08:00
107 changed files with 14968 additions and 1390 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -37,3 +37,12 @@ logger:
# ===========================================
jwt:
secret: test-jwt-secret-key-for-testing-only
# ===========================================
# 📱 短信服务配置
# ===========================================
sms:
# 滑块验证码配置
captcha_enabled: true # 是否启用滑块验证码
captcha_secret: "" # 阿里云验证码密钥(可选)
scene_id: "wynt39to" # 阿里云验证码场景ID

View File

@@ -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

View 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&timestamp=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
View File

@@ -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
View File

@@ -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=

View File

@@ -260,6 +260,7 @@ func (a *Application) autoMigrate(db *gorm.DB) error {
// api
&apiEntities.ApiUser{},
&apiEntities.ApiCall{},
&apiEntities.Report{},
// 任务域
&taskEntities.AsyncTask{},

View File

@@ -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{

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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 获取用户发票信息

View File

@@ -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 更新用户信息命令

View File

@@ -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
}

View File

@@ -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

View File

@@ -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"` // 开发环境服务地址

View File

@@ -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, // 加密模式 ekeyBase64 编码的 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()

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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

View 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"
}

View File

@@ -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)
}

View File

@@ -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, //人车关系核验ETC10093 月更
// DWBG系列处理器 - 多维报告
"DWBG6A2C": dwbg.ProcessDWBG6A2CRequest,

View File

@@ -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{}, //人车关系核验ETC10093 月更
"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": "查询原因ID1-授信审批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 {

View File

@@ -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

View File

@@ -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,
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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 需要的字段名

View File

@@ -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)

View File

@@ -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)

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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 映射为接口 state1-匹配 2-不匹配 3-异常情况
func resultToState(result int) int {
switch result {
case 0: // 一致 → 匹配
return 1
case 1: // 不一致 → 不匹配
return 2
case 2: // 无记录(预留) → 异常情况
return 3
default:
return 3 // 未知/异常 → 异常情况
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -58,6 +58,5 @@ func ProcessJRZQDCBERequest(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err)
}
}
return respBytes, nil
}

View File

@@ -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) {

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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)
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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 人车关系核验ETC10093 月更 API 处理方法(使用数据宝服务示例)
func ProcessQCXG3M7ZRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG3M7ZReq
if err := json.Unmarshal(params, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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)

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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)

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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}
}()
}
// 企业全量信息核验V2QYGLUY3S
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)
// 企业司法涉诉V2QYGL5S1I
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

View File

@@ -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)
}
})
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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
}

View File

@@ -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" // 不在网但未明确原因,默认空号
}

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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 {

View File

@@ -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 ""
}
}

View File

@@ -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_rangeYYYYMMDD-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

View File

@@ -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

View File

@@ -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, &paramsDto); 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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), "关闭赠送时应返回零")
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}
}

View 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 加密模式使用的密钥(控制台 ekeyBase64 编码的 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 生成加密场景 IDEncryptedSceneId供前端加密模式初始化验证码使用。
// 算法AES-256-CBC明文 sceneId&timestamp&expireTime密钥为控制台 ekeyBase64 解码后 32 字节)。
// expireTimeSec 有效期为 186400 秒。
func (s *CaptchaService) GetEncryptedSceneId(expireTimeSec int) (string, error) {
if expireTimeSec <= 0 || expireTimeSec > 86400 {
return "", fmt.Errorf("expireTimeSec 必须在 186400 之间")
}
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...)
}

View File

@@ -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

View 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)
})
}
}

View 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
}

View 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
}

View 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)
}

View 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 调用数据宝 APIPOST。最终请求地址 = 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
}

View 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"` // 有效期秒数186400默认 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 必须在 186400 之间")
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")
}

View File

@@ -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文件

View 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,
})
}

View File

@@ -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 使用手机号、密码和验证码进行用户注册,需要确认密码

View 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("验证码路由注册完成")
}

View 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)
}

View 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)
}

View File

@@ -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