This commit is contained in:
2026-05-09 19:08:04 +08:00
8 changed files with 171 additions and 22 deletions

View File

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

View File

@@ -318,6 +318,7 @@ func registerAllProcessors(combService *comb.CombService) {
"IVYZ6M8P": ivyz.ProcessIVYZ6M8PRequest, //职业资格证书
"IVYZ9H2M": ivyz.ProcessIVYZ9H2MRequest, //极光个人婚姻查询V2版
"IVYZZQT3": ivyz.ProcessIVYZZQT3Request, //人脸比对V3
"IVYZZQ3B": ivyz.ProcessIVYZZQ3BRequest, //人脸比对BBsimilarity + verification_result
"IVYZBPQ2": ivyz.ProcessIVYZBPQ2Request, //人脸比对V2
"IVYZSFEL": ivyz.ProcessIVYZSFELRequest, //全国自然人人像三要素核验_V1
"IVYZ0S0D": ivyz.ProcessIVYZ0S0DRequest, //劳动仲裁信息查询(个人版)

View File

@@ -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{}, //人脸比对BBsimilarity + 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-全部(默认查全部)",

View File

@@ -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, &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)
}
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 照片相似度分数字符串01000。区间说明0600不同人600700不能确定是否同人7001000同人。数值为上游 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 与 similarity01000区间说明对齐
// 7001000】系统判断为同一人 → 身份审核通过 valid其余 → invalid。
func mapVerificationResultFromSimilarity(similarity float64) string {
if similarity >= 700 {
return "valid"
}
return "invalid"
}

View File

@@ -33,10 +33,10 @@ var (
const (
headerAuthorization = "Authorization"
headerYMDate = "YmDate"
headerWorkOrderCode = "workOrderCode"
headerOrderCode = "X-ORDER-CODE"
headerResponseType = "X-RESPONSE-TYPE"
headerResponseTypeDataVal = "data"
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("汇博配置不完整")
}

View File

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

View File

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