diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 002460c..2a4a040 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -246,6 +246,12 @@ type IVYZZQT3Req struct { IDCard string `json:"id_card" validate:"required,validIDCard"` PhotoData string `json:"photo_data" validate:"required,validBase64Image"` } +type IVYZZQ3BReq struct { + Name string `json:"name" validate:"required,min=1,validName"` + IDCard string `json:"id_card" validate:"required,validIDCard"` + ImageUrl string `json:"image_url" validate:"required,url"` + Authorized string `json:"authorized" validate:"required,oneof=0 1"` +} type IVYZSFELReq struct { Name string `json:"name" validate:"required,min=1,validName"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 964509c..a3117ff 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -318,6 +318,7 @@ func registerAllProcessors(combService *comb.CombService) { "IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书 "IVYZ9H2M": ivyz.ProcessIVYZ9H2MRequest, //极光个人婚姻查询(V2版) "IVYZZQT3": ivyz.ProcessIVYZZQT3Request, //人脸比对V3 + "IVYZZQ3B": ivyz.ProcessIVYZZQ3BRequest, //人脸比对B(B:similarity + verification_result) "IVYZBPQ2": ivyz.ProcessIVYZBPQ2Request, //人脸比对V2 "IVYZSFEL": ivyz.ProcessIVYZSFELRequest, //全国自然人人像三要素核验_V1 "IVYZ0S0D": ivyz.ProcessIVYZ0S0DRequest, //劳动仲裁信息查询(个人版) diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 80155c2..1bd2fc4 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -206,6 +206,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "QYGL5CMP": &dto.QYGL5CMPReq{}, //企业五要素验证 "QCXG4896": &dto.QCXG4896Req{}, //网约车风险查询 "IVYZZQT3": &dto.IVYZZQT3Req{}, //人脸比对V3 + "IVYZZQ3B": &dto.IVYZZQ3BReq{}, //人脸比对B(B:similarity + verification_result) "IVYZBPQ2": &dto.IVYZBPQ2Req{}, //人脸比对V2 "IVYZSFEL": &dto.IVYZSFELReq{}, //全国自然人人像三要素核验_V1 "QYGL66SL": &dto.QYGL66SLReq{}, //全国企业司法模型服务查询_V1 @@ -475,7 +476,7 @@ func (s *FormConfigServiceImpl) generateFieldLabel(jsonTag string) string { "authorized": "是否授权", "authorization_url": "授权链接", "unique_id": "唯一标识", - "return_url": "返回链接", + "return_url": "回调地址", "mobile_type": "手机类型", "start_date": "开始日期", "years": "年数", @@ -544,7 +545,7 @@ func (s *FormConfigServiceImpl) generateExampleValue(fieldType reflect.Type, jso "mobile_type": "移动", "start_date": "2024-01-01", "unique_id": "UNIQUE123456", - "return_url": "https://example.com/return", + "return_url": "https://example.com/return ,回调地址链接用于接收回调数据。", "authorization_url": "https://example.com/auth20250101.pdf 注意:请不要使用示例链接,示例链接仅作为参考格式。必须为实际的被查询人授权具有法律效益的授权书文件链接,如访问不到或为不实授权书将追究责任。协议必须为http https", "user_type": "1", "vehicle_type": "0", @@ -619,7 +620,7 @@ func (s *FormConfigServiceImpl) generatePlaceholder(jsonTag string, fieldType st "mobile_type": "请选择手机类型", "start_date": "请选择开始日期", "unique_id": "请输入唯一标识", - "return_url": "请输入返回链接", + "return_url": "请输入回调地址链接", "authorization_url": "请输入授权链接", "user_type": "请选择关系类型", "vehicle_type": "请选择车辆类型", @@ -696,7 +697,7 @@ func (s *FormConfigServiceImpl) generateDescription(jsonTag string, validation s "mobile_type": "请选择手机类型", "start_date": "请选择开始日期", "unique_id": "请输入唯一标识", - "return_url": "请输入返回链接", + "return_url": "请输入回调地址链接", "authorization_url": "请输入授权链接", "user_type": "关系类型:1-ETC开户人;2-车辆所有人;3-ETC经办人(默认1-ETC开户人)", "vehicle_type": "车辆类型:0-客车;1-货车;2-全部(默认查全部)", diff --git a/internal/domains/api/services/form_config_service_test.go b/internal/domains/api/services/form_config_service_test.go index 7711d3f..e9a1b85 100644 --- a/internal/domains/api/services/form_config_service_test.go +++ b/internal/domains/api/services/form_config_service_test.go @@ -28,10 +28,10 @@ func TestFormConfigService_GetFormConfig(t *testing.T) { // 验证字段信息 expectedFields := map[string]bool{ - "man_name": false, - "man_id_card": false, - "woman_name": false, - "woman_id_card": false, + "man_name": false, + "man_id_card": false, + "woman_name": false, + "woman_id_card": false, } for _, field := range config.Fields { diff --git a/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go new file mode 100644 index 0000000..c878331 --- /dev/null +++ b/internal/domains/api/services/processors/ivyz/ivyzzq3B_processor.go @@ -0,0 +1,112 @@ +package ivyz + +import ( + "context" + "encoding/json" + "errors" + "strconv" + "time" + + "tyapi-server/internal/domains/api/dto" + "tyapi-server/internal/domains/api/services/processors" + "tyapi-server/internal/infrastructure/external/zhicha" +) + +// ProcessIVYZZQ3BRequest IVYZZQ3B 人脸比对 B +func ProcessIVYZZQ3BRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.IVYZZQ3BReq + if err := json.Unmarshal(params, ¶msDto); err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + if err := deps.Validator.ValidateStruct(paramsDto); err != nil { + return nil, errors.Join(processors.ErrInvalidParam, err) + } + encryptedName, err := deps.ZhichaService.Encrypt(paramsDto.Name) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + encryptedIDCard, err := deps.ZhichaService.Encrypt(paramsDto.IDCard) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + encryptedImageUrl, err := deps.ZhichaService.Encrypt(paramsDto.ImageUrl) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + reqData := map[string]interface{}{ + "idCard": encryptedIDCard, + "name": encryptedName, + "imageId": encryptedImageUrl, + "authorized": paramsDto.Authorized, + } + + respData, err := deps.ZhichaService.CallAPI(ctx, "ZCI062", reqData) + if err != nil { + if errors.Is(err, zhicha.ErrDatasource) { + return nil, errors.Join(processors.ErrDatasource, err) + } else { + return nil, errors.Join(processors.ErrSystem, err) + } + } + + respBytes, err := json.Marshal(respData) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + + outBytes, err := mapZCI062RespToIVYZZQ3B(respBytes) + if err != nil { + return nil, errors.Join(processors.ErrSystem, err) + } + return outBytes, nil +} + +type IVYZZQ3BOut struct { + HandleTime string `json:"handleTime"` + ResultData IVYZZQ3BOutResultData `json:"resultData"` +} + +type IVYZZQ3BOutResultData struct { + // VerificationResult 审核校验结果:valid 身份审核通过;invalid 身份审核不通过(与 similarity 区间联动,见 mapVerificationResultFromSimilarity) + VerificationResult string `json:"verification_result"` + // Similarity 照片相似度分数字符串(0–1000)。区间说明:(0,600)不同人;(600,700)不能确定是否同人;(700,1000)同人。数值为上游 score(0~1)×1000。 + Similarity string `json:"similarity"` +} + +// zci062UpstreamResp 智查 ZCI062 成功返回体中的分数字段(分值越大相似度越高) +type zci062UpstreamResp struct { + Score interface{} `json:"score"` +} + +func mapZCI062RespToIVYZZQ3B(respBytes []byte) ([]byte, error) { + var r zci062UpstreamResp + if err := json.Unmarshal(respBytes, &r); err != nil { + return nil, err + } + + score := parseScoreToFloat64(r.Score) + similarityVal := score * 1000 + similarity := strconv.FormatFloat(similarityVal, 'f', 2, 64) + verificationResult := mapVerificationResultFromSimilarity(similarityVal) + + out := IVYZZQ3BOut{ + HandleTime: time.Now().Format("2006-01-02 15:04:05"), + ResultData: IVYZZQ3BOutResultData{ + VerificationResult: verificationResult, + Similarity: similarity, + }, + } + + return json.Marshal(out) +} + +// mapVerificationResultFromSimilarity 与 similarity(0–1000)区间说明对齐: +// (700,1000】系统判断为同一人 → 身份审核通过 valid;其余 → invalid。 +func mapVerificationResultFromSimilarity(similarity float64) string { + if similarity >= 700 { + return "valid" + } + return "invalid" +} diff --git a/internal/infrastructure/external/huibo/huibo_service.go b/internal/infrastructure/external/huibo/huibo_service.go index 29dfb0f..a9773e5 100644 --- a/internal/infrastructure/external/huibo/huibo_service.go +++ b/internal/infrastructure/external/huibo/huibo_service.go @@ -32,11 +32,11 @@ var ( ) const ( - headerAuthorization = "Authorization" - headerYMDate = "YmDate" - headerOrderCode = "X-ORDER-CODE" - headerResponseType = "X-RESPONSE-TYPE" - headerResponseTypeDataVal = "data" + headerAuthorization = "Authorization" + headerWorkOrderCode = "workOrderCode" + headerOrderCode = "X-ORDER-CODE" + headerSecretIDHdr = "secretId" + headerAESKeyHdr = "aesKey" ) // 汇博常见状态码 @@ -225,9 +225,7 @@ func (s *HuiboService) CallEducationBackgroundDetailed(ctx context.Context, name func (s *HuiboService) callAPI(ctx context.Context, reqOuterJSON []byte, pdfBytes []byte) ([]byte, error) { var body bytes.Buffer writer := multipart.NewWriter(&body) - if err := writer.WriteField("req", string(reqOuterJSON)); err != nil { - return nil, errors.Join(ErrSystem, err) - } + // 与对接示例一致:先 file 再 req part, err := writer.CreateFormFile("file", "authorization.pdf") if err != nil { return nil, errors.Join(ErrSystem, err) @@ -235,6 +233,9 @@ func (s *HuiboService) callAPI(ctx context.Context, reqOuterJSON []byte, pdfByte if _, err = part.Write(pdfBytes); err != nil { return nil, errors.Join(ErrSystem, err) } + if err := writer.WriteField("req", string(reqOuterJSON)); err != nil { + return nil, errors.Join(ErrSystem, err) + } if err = writer.Close(); err != nil { return nil, errors.Join(ErrSystem, err) } @@ -244,23 +245,49 @@ func (s *HuiboService) callAPI(ctx context.Context, reqOuterJSON []byte, pdfByte return nil, errors.Join(ErrSystem, err) } req.Header.Set(headerAuthorization, s.config.AppID+"::"+s.config.AppKey) - req.Header.Set(headerYMDate, strconv.FormatInt(time.Now().UnixMilli(), 10)) + req.Header.Set(headerWorkOrderCode, s.config.WorkOrderCode) req.Header.Set(headerOrderCode, s.config.XOrderCode) - req.Header.Set(headerResponseType, headerResponseTypeDataVal) + req.Header.Set(headerSecretIDHdr, s.config.SecretID) + req.Header.Set(headerAESKeyHdr, s.config.AESKey) req.Header.Set("Content-Type", writer.FormDataContentType()) client := &http.Client{Timeout: 60 * time.Second} resp, err := client.Do(req) if err != nil { + if s.logger != nil { + s.logger.LogErrorWithFields("汇博 HTTP 请求失败", + zap.String("url", s.config.URL), + zap.Error(err), + ) + } return nil, errors.Join(ErrDatasource, err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { + if s.logger != nil { + s.logger.LogErrorWithFields("汇博 读取响应体失败", + zap.String("url", s.config.URL), + zap.Int("http_status", resp.StatusCode), + zap.Error(err), + ) + } return nil, errors.Join(ErrSystem, err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { + if s.logger != nil { + bodySnippet := string(respBody) + const maxLog = 1024 + if len(bodySnippet) > maxLog { + bodySnippet = bodySnippet[:maxLog] + "...(truncated)" + } + s.logger.LogErrorWithFields("汇博 HTTP 状态异常", + zap.String("url", s.config.URL), + zap.Int("http_status", resp.StatusCode), + zap.String("response_body", bodySnippet), + ) + } return nil, errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码异常: %d, body: %s", resp.StatusCode, string(respBody))) } return respBody, nil @@ -272,6 +299,7 @@ func (s *HuiboService) validateConfig() error { strings.TrimSpace(s.config.AppKey) == "" || strings.TrimSpace(s.config.SecretID) == "" || strings.TrimSpace(s.config.AESKey) == "" || + strings.TrimSpace(s.config.WorkOrderCode) == "" || strings.TrimSpace(s.config.XOrderCode) == "" { return errors.New("汇博配置不完整") } diff --git a/internal/shared/middleware/daily_rate_limit.go b/internal/shared/middleware/daily_rate_limit.go index a486518..5f64058 100644 --- a/internal/shared/middleware/daily_rate_limit.go +++ b/internal/shared/middleware/daily_rate_limit.go @@ -251,7 +251,7 @@ func (m *DailyRateLimitMiddleware) shouldRecordNearLimit(current, max int) bool if max <= 0 { return false } - threshold := int(math.Ceil(float64(max) * 0.8)) + threshold := int(math.Ceil(float64(max) * 0.9)) if threshold < 1 { threshold = 1 } diff --git a/internal/shared/validator/custom_validators.go b/internal/shared/validator/custom_validators.go index d6e7c13..abbf1cf 100644 --- a/internal/shared/validator/custom_validators.go +++ b/internal/shared/validator/custom_validators.go @@ -9,8 +9,9 @@ import ( "strings" "time" - "github.com/go-playground/validator/v10" "tyapi-server/internal/shared/pdfvalidate" + + "github.com/go-playground/validator/v10" ) // RegisterCustomValidators 注册所有自定义验证器 @@ -630,7 +631,7 @@ func validateEnterpriseName(fl validator.FieldLevel) bool { // 支持:有限公司、股份有限公司、工作室、个体工商户、合伙企业等 validSuffixes := []string{ "有限公司", "有限责任公司", "股份有限公司", "股份公司", - "工作室", "个体工商户", "个人独资企业", "合伙企业", + "工作室", "个体工商户", "个人独资企业", "个人独资", "合伙企业", "集团有限公司", "集团股份有限公司", "分公司", "子公司", "办事处", "代表处", "Co.,Ltd", "Co., Ltd", "Ltd", "LLC", "Inc", "Corp", @@ -915,7 +916,7 @@ func ValidateEnterpriseName(enterpriseName string) error { // 支持:有限公司、股份有限公司、工作室、个体工商户、合伙企业等 validSuffixes := []string{ "有限公司", "有限责任公司", "股份有限公司", "股份公司", - "工作室", "个体工商户", "个人独资企业", "合伙企业", + "工作室", "个体工商户", "个人独资企业", "个人独资", "合伙企业", "集团有限公司", "集团股份有限公司", "分公司", "子公司", "办事处", "代表处", "Co.,Ltd", "Co., Ltd", "Ltd", "LLC", "Inc", "Corp",