fix
This commit is contained in:
@@ -329,31 +329,31 @@ func (s *FormConfigServiceImpl) getFieldType(fieldType reflect.Type, validation
|
||||
func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||
// 将下划线命名转换为中文标签
|
||||
labelMap := map[string]string{
|
||||
"mobile_no": "手机号码",
|
||||
"id_card": "身份证号",
|
||||
"name": "姓名",
|
||||
"man_name": "男方姓名",
|
||||
"woman_name": "女方姓名",
|
||||
"man_id_card": "男方身份证",
|
||||
"woman_id_card": "女方身份证",
|
||||
"ent_name": "企业名称",
|
||||
"legal_person": "法人姓名",
|
||||
"ent_code": "企业代码",
|
||||
"auth_date": "授权日期",
|
||||
"time_range": "时间范围",
|
||||
"authorized": "是否授权",
|
||||
"authorization_url": "授权链接",
|
||||
"unique_id": "唯一标识",
|
||||
"return_url": "返回链接",
|
||||
"mobile_type": "手机类型",
|
||||
"start_date": "开始日期",
|
||||
"years": "年数",
|
||||
"bank_card": "银行卡号",
|
||||
"user_type": "关系类型",
|
||||
"vehicle_type": "车辆类型",
|
||||
"page_num": "页码",
|
||||
"page_size": "每页数量",
|
||||
"use_scenario": "使用场景",
|
||||
"mobile_no": "手机号码",
|
||||
"id_card": "身份证号",
|
||||
"name": "姓名",
|
||||
"man_name": "男方姓名",
|
||||
"woman_name": "女方姓名",
|
||||
"man_id_card": "男方身份证",
|
||||
"woman_id_card": "女方身份证",
|
||||
"ent_name": "企业名称",
|
||||
"legal_person": "法人姓名",
|
||||
"ent_code": "企业代码",
|
||||
"auth_date": "授权日期",
|
||||
"time_range": "时间范围",
|
||||
"authorized": "是否授权",
|
||||
"authorization_url": "授权链接",
|
||||
"unique_id": "唯一标识",
|
||||
"return_url": "返回链接",
|
||||
"mobile_type": "手机类型",
|
||||
"start_date": "开始日期",
|
||||
"years": "年数",
|
||||
"bank_card": "银行卡号",
|
||||
"user_type": "关系类型",
|
||||
"vehicle_type": "车辆类型",
|
||||
"page_num": "页码",
|
||||
"page_size": "每页数量",
|
||||
"use_scenario": "使用场景",
|
||||
"auth_authorize_file_code": "授权文件编码",
|
||||
}
|
||||
|
||||
@@ -368,29 +368,29 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string {
|
||||
// generateExampleValue 生成示例值
|
||||
func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jsonTag string) string {
|
||||
exampleMap := map[string]string{
|
||||
"mobile_no": "13800138000",
|
||||
"id_card": "110101199001011234",
|
||||
"name": "张三",
|
||||
"man_name": "张三",
|
||||
"woman_name": "李四",
|
||||
"ent_name": "示例企业有限公司",
|
||||
"legal_person": "王五",
|
||||
"ent_code": "91110000123456789X",
|
||||
"auth_date": "20240101-20241231",
|
||||
"time_range": "09:00-18:00",
|
||||
"authorized": "1",
|
||||
"years": "5",
|
||||
"bank_card": "6222021234567890123",
|
||||
"mobile_type": "移动",
|
||||
"start_date": "2024-01-01",
|
||||
"unique_id": "UNIQUE123456",
|
||||
"return_url": "https://example.com/return",
|
||||
"authorization_url": "https://example.com/auth20250101.pdf",
|
||||
"user_type": "1",
|
||||
"vehicle_type": "0",
|
||||
"page_num": "1",
|
||||
"page_size": "10",
|
||||
"use_scenario": "1",
|
||||
"mobile_no": "13800138000",
|
||||
"id_card": "110101199001011234",
|
||||
"name": "张三",
|
||||
"man_name": "张三",
|
||||
"woman_name": "李四",
|
||||
"ent_name": "示例企业有限公司",
|
||||
"legal_person": "王五",
|
||||
"ent_code": "91110000123456789X",
|
||||
"auth_date": "20240101-20241231",
|
||||
"time_range": "09:00-18:00",
|
||||
"authorized": "1",
|
||||
"years": "5",
|
||||
"bank_card": "6222021234567890123",
|
||||
"mobile_type": "移动",
|
||||
"start_date": "2024-01-01",
|
||||
"unique_id": "UNIQUE123456",
|
||||
"return_url": "https://example.com/return",
|
||||
"authorization_url": "https://example.com/auth20250101.pdf 注意:请不要使用示例链接,示例链接仅作为参考格式。必须为实际的被查询人授权具有法律效益的授权书文件链接,如访问不到或为不实授权书将追究责任。协议必须为http https",
|
||||
"user_type": "1",
|
||||
"vehicle_type": "0",
|
||||
"page_num": "1",
|
||||
"page_size": "10",
|
||||
"use_scenario": "1",
|
||||
"auth_authorize_file_code": "AUTH123456",
|
||||
}
|
||||
|
||||
@@ -414,29 +414,29 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso
|
||||
// generatePlaceholder 生成占位符
|
||||
func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType string) string {
|
||||
placeholderMap := map[string]string{
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"auth_date": "请输入授权日期范围(YYYYMMDD-YYYYMMDD)",
|
||||
"time_range": "请输入时间范围(HH:MM-HH:MM)",
|
||||
"authorized": "请选择是否授权",
|
||||
"years": "请输入查询年数(0-100)",
|
||||
"bank_card": "请输入银行卡号",
|
||||
"mobile_type": "请选择手机类型",
|
||||
"start_date": "请选择开始日期",
|
||||
"unique_id": "请输入唯一标识",
|
||||
"return_url": "请输入返回链接",
|
||||
"authorization_url": "请输入授权链接",
|
||||
"user_type": "请选择关系类型",
|
||||
"vehicle_type": "请选择车辆类型",
|
||||
"page_num": "请输入页码",
|
||||
"page_size": "请输入每页数量(1-100)",
|
||||
"use_scenario": "请选择使用场景",
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"auth_date": "请输入授权日期范围(YYYYMMDD-YYYYMMDD)",
|
||||
"time_range": "请输入时间范围(HH:MM-HH:MM)",
|
||||
"authorized": "请选择是否授权",
|
||||
"years": "请输入查询年数(0-100)",
|
||||
"bank_card": "请输入银行卡号",
|
||||
"mobile_type": "请选择手机类型",
|
||||
"start_date": "请选择开始日期",
|
||||
"unique_id": "请输入唯一标识",
|
||||
"return_url": "请输入返回链接",
|
||||
"authorization_url": "请输入授权链接",
|
||||
"user_type": "请选择关系类型",
|
||||
"vehicle_type": "请选择车辆类型",
|
||||
"page_num": "请输入页码",
|
||||
"page_size": "请输入每页数量(1-100)",
|
||||
"use_scenario": "请选择使用场景",
|
||||
"auth_authorize_file_code": "请输入授权文件编码",
|
||||
}
|
||||
|
||||
@@ -462,29 +462,29 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st
|
||||
// generateDescription 生成字段描述
|
||||
func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation string) string {
|
||||
descMap := map[string]string{
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"auth_date": "请输入授权日期范围,格式:YYYYMMDD-YYYYMMDD,且日期范围必须包括今天",
|
||||
"time_range": "请输入时间范围,格式:HH:MM-HH:MM",
|
||||
"authorized": "请输入是否授权:0-未授权,1-已授权",
|
||||
"years": "请输入查询年数(0-100)",
|
||||
"bank_card": "请输入银行卡号",
|
||||
"mobile_type": "请选择手机类型",
|
||||
"start_date": "请选择开始日期",
|
||||
"unique_id": "请输入唯一标识",
|
||||
"return_url": "请输入返回链接",
|
||||
"authorization_url": "请输入授权链接",
|
||||
"user_type": "关系类型:1-ETC开户人;2-车辆所有人;3-ETC经办人(默认1-ETC开户人)",
|
||||
"vehicle_type": "车辆类型:0-客车;1-货车;2-全部(默认查全部)",
|
||||
"page_num": "请输入页码,从1开始",
|
||||
"page_size": "请输入每页数量,范围1-100",
|
||||
"use_scenario": "使用场景:1-信贷审核;2-保险评估;3-招聘背景调查;4-其他业务场景;99-其他",
|
||||
"mobile_no": "请输入11位手机号码",
|
||||
"id_card": "请输入18位身份证号码",
|
||||
"name": "请输入真实姓名",
|
||||
"man_name": "请输入男方真实姓名",
|
||||
"woman_name": "请输入女方真实姓名",
|
||||
"ent_name": "请输入企业全称",
|
||||
"legal_person": "请输入法人真实姓名",
|
||||
"ent_code": "请输入统一社会信用代码",
|
||||
"auth_date": "请输入授权日期范围,格式:YYYYMMDD-YYYYMMDD,且日期范围必须包括今天",
|
||||
"time_range": "请输入时间范围,格式:HH:MM-HH:MM",
|
||||
"authorized": "请输入是否授权:0-未授权,1-已授权",
|
||||
"years": "请输入查询年数(0-100)",
|
||||
"bank_card": "请输入银行卡号",
|
||||
"mobile_type": "请选择手机类型",
|
||||
"start_date": "请选择开始日期",
|
||||
"unique_id": "请输入唯一标识",
|
||||
"return_url": "请输入返回链接",
|
||||
"authorization_url": "请输入授权链接",
|
||||
"user_type": "关系类型:1-ETC开户人;2-车辆所有人;3-ETC经办人(默认1-ETC开户人)",
|
||||
"vehicle_type": "车辆类型:0-客车;1-货车;2-全部(默认查全部)",
|
||||
"page_num": "请输入页码,从1开始",
|
||||
"page_size": "请输入每页数量,范围1-100",
|
||||
"use_scenario": "使用场景:1-信贷审核;2-保险评估;3-招聘背景调查;4-其他业务场景;99-其他",
|
||||
"auth_authorize_file_code": "请输入授权文件编码",
|
||||
}
|
||||
|
||||
@@ -501,7 +501,7 @@ func (s *FormConfigServiceImpl) mergeCombPackageDTOs(ctx context.Context, apiCod
|
||||
if s.productManagementService == nil {
|
||||
return &struct{}{}, nil
|
||||
}
|
||||
|
||||
|
||||
// 1. 从数据库获取组合包产品信息
|
||||
packageProduct, err := s.productManagementService.GetProductByCode(ctx, apiCode)
|
||||
if err != nil {
|
||||
@@ -560,7 +560,7 @@ func (s *FormConfigServiceImpl) mergeCombPackageDTOs(ctx context.Context, apiCod
|
||||
|
||||
// 创建结构体类型
|
||||
structType := reflect.StructOf(fields)
|
||||
|
||||
|
||||
// 创建并返回结构体实例
|
||||
structValue := reflect.New(structType)
|
||||
return structValue.Interface(), nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
@@ -136,11 +137,37 @@ func (cs *CombService) processSingleSubProduct(
|
||||
}
|
||||
|
||||
// combineResults 组合所有子产品的结果
|
||||
// 只要至少有一个子产品成功,就返回成功结果(部分成功也算成功)
|
||||
// 只有当所有子产品都失败时,才返回错误
|
||||
func (cs *CombService) combineResults(results []*processors.SubProductResult) (*processors.CombinedResult, error) {
|
||||
// 检查是否至少有一个成功的子产品
|
||||
hasSuccess := false
|
||||
for _, result := range results {
|
||||
if result.Success {
|
||||
hasSuccess = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 构建组合结果
|
||||
combinedResult := &processors.CombinedResult{
|
||||
Responses: results,
|
||||
}
|
||||
|
||||
// 如果所有子产品都失败,返回错误
|
||||
if !hasSuccess && len(results) > 0 {
|
||||
// 构建错误信息,包含所有失败的原因
|
||||
errorMessages := make([]string, 0, len(results))
|
||||
for _, result := range results {
|
||||
if result.Error != "" {
|
||||
errorMessages = append(errorMessages, fmt.Sprintf("%s: %s", result.ApiCode, result.Error))
|
||||
}
|
||||
}
|
||||
errorMsg := fmt.Sprintf("组合包所有子产品调用失败: %s", strings.Join(errorMessages, "; "))
|
||||
return nil, fmt.Errorf(errorMsg)
|
||||
}
|
||||
|
||||
// 至少有一个成功,返回成功结果
|
||||
return combinedResult, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -58,10 +59,27 @@ func (a *AlicloudService) CallAPI(path string, params map[string]interface{}) (r
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
|
||||
req.Header.Set("Authorization", "APPCODE "+a.config.AppCode)
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{}
|
||||
// 发送请求,超时时间设置为60秒
|
||||
client := &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// 检查是否是超时错误
|
||||
isTimeout := false
|
||||
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 {
|
||||
return nil, fmt.Errorf("%w: API请求超时: %s", ErrDatasource, err.Error())
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -52,7 +52,7 @@ func NewWeChatWorkService(webhookURL, secret string, logger *zap.Logger) *WeChat
|
||||
return &WeChatWorkService{
|
||||
webhookURL: webhookURL,
|
||||
secret: secret,
|
||||
timeout: 30 * time.Second,
|
||||
timeout: 60 * time.Second,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -385,7 +385,25 @@ func (s *WeChatWorkService) sendMessage(ctx context.Context, message map[string]
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送请求失败: %w", err)
|
||||
// 检查是否是超时错误
|
||||
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
|
||||
}
|
||||
|
||||
errorMsg := "发送请求失败"
|
||||
if isTimeout {
|
||||
errorMsg = "发送请求超时"
|
||||
}
|
||||
return fmt.Errorf("%s: %w", errorMsg, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func NewBaiduOCRService(apiKey, secretKey string, logger *zap.Logger) *BaiduOCRS
|
||||
apiKey: apiKey,
|
||||
secretKey: secretKey,
|
||||
endpoint: "https://aip.baidubce.com",
|
||||
timeout: 30 * time.Second,
|
||||
timeout: 60 * time.Second,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
@@ -258,6 +258,23 @@ func (s *BaiduOCRService) sendRequest(ctx context.Context, method, url string, b
|
||||
// 发送请求
|
||||
resp, 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 {
|
||||
return nil, fmt.Errorf("API请求超时: %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("发送请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -443,7 +460,7 @@ func (s *BaiduOCRService) extractWords(result map[string]interface{}) []string {
|
||||
func (s *BaiduOCRService) downloadImage(ctx context.Context, imageURL string) ([]byte, error) {
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
|
||||
@@ -285,9 +285,9 @@ func (s *QiNiuStorageService) UploadFromReader(ctx context.Context, reader io.Re
|
||||
func (s *QiNiuStorageService) DownloadFile(ctx context.Context, fileURL string) ([]byte, error) {
|
||||
s.logger.Info("开始从七牛云下载文件", zap.String("file_url", fileURL))
|
||||
|
||||
// 创建HTTP客户端
|
||||
// 创建HTTP客户端,超时时间设置为60秒
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// 创建请求
|
||||
@@ -299,11 +299,29 @@ func (s *QiNiuStorageService) DownloadFile(ctx context.Context, fileURL string)
|
||||
// 发送请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
s.logger.Error("下载文件失败",
|
||||
// 检查是否是超时错误
|
||||
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
|
||||
}
|
||||
|
||||
errorMsg := "下载文件失败"
|
||||
if isTimeout {
|
||||
errorMsg = "下载文件超时"
|
||||
}
|
||||
s.logger.Error(errorMsg,
|
||||
zap.String("file_url", fileURL),
|
||||
zap.Error(err),
|
||||
)
|
||||
return nil, fmt.Errorf("下载文件失败: %w", err)
|
||||
return nil, fmt.Errorf("%s: %w", errorMsg, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ type TianYanChaResponse struct {
|
||||
// NewTianYanChaService 创建天眼查服务实例
|
||||
func NewTianYanChaService(baseURL, token string, timeout time.Duration) *TianYanChaService {
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
|
||||
return &TianYanChaService{
|
||||
@@ -112,6 +112,23 @@ func (t *TianYanChaService) CallAPI(ctx context.Context, apiCode string, params
|
||||
client := &http.Client{Timeout: t.config.Timeout}
|
||||
resp, 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 {
|
||||
return nil, errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", err))
|
||||
}
|
||||
return nil, errors.Join(ErrDatasource, fmt.Errorf("API 请求异常: %v", err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -120,11 +120,31 @@ func (w *WestDexService) CallAPI(ctx context.Context, code string, reqData map[s
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{}
|
||||
// 发送请求,超时时间设置为60秒
|
||||
client := &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
httpResp, clientDoErr := client.Do(req)
|
||||
if clientDoErr != nil {
|
||||
err = errors.Join(ErrSystem, clientDoErr)
|
||||
// 检查是否是超时错误
|
||||
isTimeout := false
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
isTimeout = true
|
||||
} else if netErr, ok := clientDoErr.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
isTimeout = true
|
||||
} else if errStr := clientDoErr.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", clientDoErr))
|
||||
} else {
|
||||
err = errors.Join(ErrSystem, clientDoErr)
|
||||
}
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, transactionID, code, err, reqData)
|
||||
}
|
||||
@@ -273,11 +293,31 @@ func (w *WestDexService) G05HZ01CallAPI(ctx context.Context, code string, reqDat
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{}
|
||||
// 发送请求,超时时间设置为60秒
|
||||
client := &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
httpResp, clientDoErr := client.Do(req)
|
||||
if clientDoErr != nil {
|
||||
err = errors.Join(ErrSystem, clientDoErr)
|
||||
// 检查是否是超时错误
|
||||
isTimeout := false
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
isTimeout = true
|
||||
} else if netErr, ok := clientDoErr.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
isTimeout = true
|
||||
} else if errStr := clientDoErr.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", clientDoErr))
|
||||
} else {
|
||||
err = errors.Join(ErrSystem, clientDoErr)
|
||||
}
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, transactionID, code, err, reqData)
|
||||
}
|
||||
|
||||
@@ -158,15 +158,33 @@ func (x *XingweiService) CallAPI(ctx context.Context, projectID string, params m
|
||||
req.Header.Set("API-ID", x.config.ApiID)
|
||||
req.Header.Set("project_id", projectID)
|
||||
|
||||
// 创建HTTP客户端
|
||||
// 创建HTTP客户端,超时时间设置为60秒
|
||||
client := &http.Client{
|
||||
Timeout: 20 * time.Second,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
httpResp, clientDoErr := client.Do(req)
|
||||
if clientDoErr != nil {
|
||||
err = errors.Join(ErrSystem, clientDoErr)
|
||||
// 检查是否是超时错误
|
||||
isTimeout := false
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
isTimeout = true
|
||||
} else if netErr, ok := clientDoErr.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
isTimeout = true
|
||||
} else if errStr := clientDoErr.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", clientDoErr))
|
||||
} else {
|
||||
err = errors.Join(ErrSystem, clientDoErr)
|
||||
}
|
||||
if x.logger != nil {
|
||||
x.logger.LogError(requestID, transactionID, "xingwei_api", err, params)
|
||||
}
|
||||
|
||||
@@ -107,9 +107,9 @@ func (y *YushanService) CallAPI(ctx context.Context, code string, params map[str
|
||||
// 将加密后的数据编码为 Base64 字符串
|
||||
content := base64.StdEncoding.EncodeToString(cipherText)
|
||||
|
||||
// 发起 HTTP 请求
|
||||
// 发起 HTTP 请求,超时时间设置为60秒
|
||||
client := &http.Client{
|
||||
Timeout: 20 * time.Second,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", y.config.URL, strings.NewReader(content))
|
||||
if err != nil {
|
||||
@@ -125,7 +125,25 @@ func (y *YushanService) CallAPI(ctx context.Context, code string, params map[str
|
||||
// 执行请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
// 检查是否是超时错误
|
||||
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 y.logger != nil {
|
||||
y.logger.LogError(requestID, transactionID, code, err, params)
|
||||
}
|
||||
|
||||
@@ -118,15 +118,35 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st
|
||||
req.Header.Set("timestamp", strconv.FormatInt(timestamp, 10))
|
||||
req.Header.Set("sign", z.generateSign(timestamp))
|
||||
|
||||
// 创建HTTP客户端
|
||||
// 创建HTTP客户端,超时时间设置为60秒
|
||||
client := &http.Client{
|
||||
Timeout: 20 * time.Second,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
// 检查是否是超时错误
|
||||
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 z.logger != nil {
|
||||
z.logger.LogError(requestID, transactionID, proID, err, params)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -211,6 +215,8 @@ func validateUUID(fl validator.FieldLevel) bool {
|
||||
// validateURL URL验证
|
||||
func validateURL(fl validator.FieldLevel) bool {
|
||||
urlStr := fl.Field().String()
|
||||
// 去除首尾空白字符
|
||||
urlStr = strings.TrimSpace(urlStr)
|
||||
_, err := url.ParseRequestURI(urlStr)
|
||||
return err == nil
|
||||
}
|
||||
@@ -462,19 +468,31 @@ func validateLuhn(cardNumber string) bool {
|
||||
}
|
||||
|
||||
// validateAuthorizationURL 授权书URL验证器
|
||||
// 验证URL格式、可访问性和文件类型
|
||||
// 安全措施:
|
||||
// 1. 仅允许http/https协议
|
||||
// 2. 设置超时时间防止阻塞
|
||||
// 3. 限制重定向次数
|
||||
// 4. 检查Content-Type和文件签名
|
||||
func validateAuthorizationURL(fl validator.FieldLevel) bool {
|
||||
urlStr := fl.Field().String()
|
||||
if urlStr == "" {
|
||||
return true // 空值由required标签处理
|
||||
}
|
||||
|
||||
// 去除首尾空白字符
|
||||
urlStr = strings.TrimSpace(urlStr)
|
||||
if urlStr == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 解析URL
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查协议
|
||||
// 检查协议(仅允许http和https)
|
||||
if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
|
||||
return false
|
||||
}
|
||||
@@ -489,8 +507,165 @@ func validateAuthorizationURL(fl validator.FieldLevel) bool {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasValidExtension {
|
||||
return false
|
||||
}
|
||||
|
||||
return hasValidExtension
|
||||
// 验证URL可访问性和文件类型
|
||||
return validateURLAccessibility(parsedURL)
|
||||
}
|
||||
|
||||
// validateURLAccessibility 验证URL可访问性和文件类型
|
||||
func validateURLAccessibility(parsedURL *url.URL) bool {
|
||||
// 创建带超时的context(5秒超时,避免阻塞验证流程)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 创建HTTP客户端,设置安全参数
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
// 限制重定向次数,防止重定向攻击
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
// 最多允许3次重定向
|
||||
if len(via) >= 3 {
|
||||
return fmt.Errorf("重定向次数过多")
|
||||
}
|
||||
// 检查重定向后的URL是否仍然是http/https
|
||||
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
|
||||
return fmt.Errorf("不允许重定向到非HTTP协议")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// 先尝试HEAD请求(更高效,不下载文件内容)
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 设置User-Agent,避免某些服务器拒绝请求
|
||||
req.Header.Set("User-Agent", "TYAPI-Validator/1.0")
|
||||
|
||||
// 发送HEAD请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
// HEAD请求失败,尝试GET请求(某些服务器不支持HEAD)
|
||||
return validateWithGETRequest(ctx, client, parsedURL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查HTTP状态码
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证Content-Type
|
||||
if !isValidContentType(resp.Header.Get("Content-Type")) {
|
||||
// Content-Type无效,尝试读取文件签名验证
|
||||
return validateWithGETRequest(ctx, client, parsedURL)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// validateWithGETRequest 使用GET请求验证文件(仅在HEAD失败时使用)
|
||||
func validateWithGETRequest(ctx context.Context, client *http.Client, parsedURL *url.URL) bool {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", parsedURL.String(), nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", "TYAPI-Validator/1.0")
|
||||
// 只读取部分内容,不下载整个文件
|
||||
req.Header.Set("Range", "bytes=0-1023") // 只读取前1024字节
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查HTTP状态码(206是部分内容,200是完整内容)
|
||||
if resp.StatusCode != 200 && resp.StatusCode != 206 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证Content-Type
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if isValidContentType(contentType) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 读取文件签名(magic bytes)验证文件类型
|
||||
return validateFileSignature(resp.Body)
|
||||
}
|
||||
|
||||
// isValidContentType 检查Content-Type是否有效
|
||||
func isValidContentType(contentType string) bool {
|
||||
contentType = strings.ToLower(strings.TrimSpace(contentType))
|
||||
if contentType == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 移除charset等参数,只检查主类型
|
||||
if idx := strings.Index(contentType, ";"); idx != -1 {
|
||||
contentType = contentType[:idx]
|
||||
}
|
||||
contentType = strings.TrimSpace(contentType)
|
||||
|
||||
// 允许的Content-Type列表
|
||||
validContentTypes := []string{
|
||||
"application/pdf", // PDF
|
||||
"image/jpeg", // JPEG
|
||||
"image/jpg", // JPG
|
||||
"image/png", // PNG
|
||||
"image/bmp", // BMP
|
||||
"image/x-ms-bmp", // BMP (另一种MIME类型)
|
||||
}
|
||||
|
||||
for _, validType := range validContentTypes {
|
||||
if contentType == validType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// validateFileSignature 通过文件签名(magic bytes)验证文件类型
|
||||
func validateFileSignature(body io.Reader) bool {
|
||||
// 读取文件前16字节(足够识别所有支持的文件类型)
|
||||
buffer := make([]byte, 16)
|
||||
n, err := body.Read(buffer)
|
||||
if err != nil && err != io.EOF {
|
||||
return false
|
||||
}
|
||||
if n < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// PDF签名: %PDF (前4字节)
|
||||
if n >= 4 && bytes.Equal(buffer[0:4], []byte("%PDF")) {
|
||||
return true
|
||||
}
|
||||
|
||||
// JPEG签名: FF D8 FF (前3字节)
|
||||
if n >= 3 && buffer[0] == 0xFF && buffer[1] == 0xD8 && buffer[2] == 0xFF {
|
||||
return true
|
||||
}
|
||||
|
||||
// PNG签名: 89 50 4E 47 0D 0A 1A 0A (前8字节)
|
||||
if n >= 8 && bytes.Equal(buffer[0:8], []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}) {
|
||||
return true
|
||||
}
|
||||
|
||||
// BMP签名: BM (前2字节: 42 4D)
|
||||
if n >= 2 && bytes.Equal(buffer[0:2], []byte("BM")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// validateUniqueID 唯一标识验证器(小于等于32位字符串)
|
||||
@@ -517,6 +692,9 @@ func validateReturnURL(fl validator.FieldLevel) bool {
|
||||
return true // 空值由required标签处理
|
||||
}
|
||||
|
||||
// 去除首尾空白字符
|
||||
returnURL = strings.TrimSpace(returnURL)
|
||||
|
||||
// 检查长度:不能超过500字符
|
||||
if len(returnURL) > 500 {
|
||||
return false
|
||||
|
||||
@@ -27,21 +27,21 @@ func InitGlobalValidator() {
|
||||
once.Do(func() {
|
||||
// 1. 创建新的校验器实例
|
||||
globalValidator = validator.New()
|
||||
|
||||
|
||||
// 2. 创建中文翻译器
|
||||
zhLocale := zh.New()
|
||||
uni := ut.New(zhLocale, zhLocale)
|
||||
globalTranslator, _ = uni.GetTranslator("zh")
|
||||
|
||||
|
||||
// 3. 注册官方中文翻译
|
||||
zh_translations.RegisterDefaultTranslations(globalValidator, globalTranslator)
|
||||
|
||||
|
||||
// 4. 注册自定义校验规则
|
||||
RegisterCustomValidators(globalValidator)
|
||||
|
||||
|
||||
// 5. 注册自定义中文翻译
|
||||
RegisterCustomTranslations(globalValidator, globalTranslator)
|
||||
|
||||
|
||||
// 6. 设置到Gin全局校验器(确保Gin使用我们的校验器)
|
||||
if binding.Validator.Engine() != nil {
|
||||
// 如果Gin已经初始化,则替换其校验器
|
||||
@@ -78,11 +78,11 @@ type RequestValidator struct {
|
||||
func NewRequestValidator(response interfaces.ResponseBuilder) interfaces.RequestValidator {
|
||||
// 确保全局校验器已初始化
|
||||
InitGlobalValidator()
|
||||
|
||||
|
||||
return &RequestValidator{
|
||||
response: response,
|
||||
translator: globalTranslator, // 使用全局翻译器
|
||||
validator: globalValidator, // 使用全局校验器
|
||||
translator: globalTranslator, // 使用全局翻译器
|
||||
validator: globalValidator, // 使用全局校验器
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user