This commit is contained in:
2025-11-02 20:33:28 +08:00
parent bb88c78c82
commit 2773c1a60b
13 changed files with 521 additions and 132 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,
}
// 创建请求

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
// 创建带超时的context5秒超时避免阻塞验证流程
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

View File

@@ -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, // 使用全局校验器
}
}