package jiyi import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "time" "hyapi-server/internal/shared/external_logger" ) const defaultRequestTimeout = 4 * time.Second // queryBillingAPIKeys 查询计费接口:未查得/空结果仍按成功返回空数据,由平台侧计费 var queryBillingAPIKeys = map[string]struct{}{ "jy000008": {}, // 探针C "jy000017": {}, // 探针A "jy000019": {}, // 商业险有效性 "jy000020": {}, // 交强险有效性 "jy000022": {}, // 洞侦1.0 "jy000028": {}, // 车VIN查车牌号 "jy000042": {}, // 借贷意向验证3.0 "jy000048": {}, // 申请借贷 "jy000052": {}, // 无间司南-纯黑A版 } func isQueryBillingAPIKey(apiKey string) bool { _, ok := queryBillingAPIKeys[apiKey] return ok } // jiyiResponse 集奕通用响应 type jiyiResponse struct { Code int `json:"code"` Msg string `json:"msg"` Fee string `json:"fee"` SeqNo string `json:"seqNo"` Data interface{} `json:"data"` } // Response 对外暴露的响应别名,便于处理器取值 type Response = jiyiResponse type serviceConfig struct { URL string AppID string AppSecret string Timeout time.Duration } // JiyiService 集奕数据服务 type JiyiService struct { config serviceConfig logger *external_logger.ExternalServiceLogger } // NewJiyiService 创建集奕服务实例 func NewJiyiService(url, appID, appSecret string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *JiyiService { if timeout <= 0 { timeout = defaultRequestTimeout } return &JiyiService{ config: serviceConfig{ URL: url, AppID: appID, AppSecret: appSecret, Timeout: timeout, }, logger: logger, } } func (s *JiyiService) logResponse(transactionID, apiKey string, statusCode int, duration time.Duration, seqNo string) { if s.logger == nil { return } s.logger.LogResponse(seqNo, transactionID, apiKey, statusCode, duration) } func (s *JiyiService) logError(transactionID, apiKey, seqNo string, err error, payload interface{}) { if s.logger == nil { return } s.logger.LogError(seqNo, transactionID, apiKey, err, payload) } // CallAPI 调用集奕接口。opts 为空时按明文、不注入额外字段处理。 func (s *JiyiService) CallAPI(ctx context.Context, apiKey, apiPath string, body map[string]string, opts ...CallOptions) (*Response, error) { requestURL := strings.TrimSuffix(s.config.URL, "/") if apiPath != "" { if !strings.HasPrefix(apiPath, "/") { apiPath = "/" + apiPath } requestURL += apiPath } startTime := time.Now() var transactionID string if id, ok := ctx.Value("transaction_id").(string); ok { transactionID = id } opt := DefaultCallOptions() if len(opts) > 0 { opt = opts[0] } requestBody := cloneBody(body) switch opt.Mode { case CallModeBodyEncrypt: encType := opt.EncryptValue if _, ok := requestBody["timestamp"]; !ok || strings.TrimSpace(requestBody["timestamp"]) == "" { requestBody["timestamp"] = strconv.FormatInt(time.Now().UnixMilli(), 10) } requestBody["encryptType"] = strconv.Itoa(encType) if encType == 1 { requestBody = encryptBodyMD5(requestBody) } case CallModeTopEncrypt: if opt.EncryptValue == 2 { requestBody = encryptBodyMD5(requestBody) } } sign := Sign(requestBody, s.config.AppSecret) requestPayload := map[string]interface{}{ "appId": s.config.AppID, "sign": sign, "apiKey": apiKey, "body": requestBody, } if opt.Mode == CallModeTopEncrypt { encVal := opt.EncryptValue if encVal <= 0 { encVal = 1 } // 顶层 encryptionType:部分接口要求 JSON number,统一传整型 requestPayload["encryptionType"] = encVal } if s.logger != nil { s.logger.LogRequest("", transactionID, apiKey, requestURL) } bodyBytes, err := json.Marshal(requestPayload) if err != nil { err = errors.Join(ErrSystem, err) s.logError(transactionID, apiKey, "", err, requestPayload) return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes)) if err != nil { err = errors.Join(ErrSystem, err) s.logError(transactionID, apiKey, "", err, requestPayload) return nil, err } req.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: s.config.Timeout} resp, err := client.Do(req) if err != nil { err = wrapHTTPError(err) s.logError(transactionID, apiKey, "", err, requestPayload) return nil, err } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { err = errors.Join(ErrSystem, err) s.logError(transactionID, apiKey, "", err, requestPayload) return nil, err } duration := time.Since(startTime) if resp.StatusCode != http.StatusOK { err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", resp.StatusCode)) s.logError(transactionID, apiKey, "", err, requestPayload) return nil, err } var jiyiResp jiyiResponse if err := json.Unmarshal(respBody, &jiyiResp); err != nil { err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err)) s.logError(transactionID, apiKey, "", err, requestPayload) return nil, err } if jiyiResp.Code != CodeSuccess { jiyiErr := NewJiyiError(jiyiResp.Code, jiyiResp.Msg) err = errors.Join(GetErrByPlatformCode(jiyiResp.Code), jiyiErr) s.logError(transactionID, apiKey, jiyiResp.SeqNo, jiyiErr, requestPayload) return nil, err } if jiyiResp.Data == nil { // 部分接口查空时 data 可能为 null if isQueryBillingAPIKey(apiKey) { jiyiResp.Data = map[string]interface{}{} s.logResponse(transactionID, apiKey, resp.StatusCode, duration, jiyiResp.SeqNo) return &jiyiResp, nil } err = errors.Join(ErrNotFound, errors.New("响应 data 为空")) s.logError(transactionID, apiKey, jiyiResp.SeqNo, err, requestPayload) return nil, err } busiCode, busiMsg, ok := parseDataBusiInfo(jiyiResp.Data) if !ok || (busiCode == 0 && strings.TrimSpace(busiMsg) == "") { err = errors.Join(ErrSystem, errors.New("响应 data 无法解析 busiCode")) s.logError(transactionID, apiKey, jiyiResp.SeqNo, err, requestPayload) return nil, err } if busiCode != BusiCodeSuccess { // 查询计费产品:未查得仍返回空数据,由平台计费 if busiCode == BusiCodeNotFound && isQueryBillingAPIKey(apiKey) { jiyiResp.Data = map[string]interface{}{} s.logResponse(transactionID, apiKey, resp.StatusCode, duration, jiyiResp.SeqNo) return &jiyiResp, nil } busiErr := NewJiyiBusiError(busiCode, busiMsg) err = errors.Join(GetErrByBusiCode(busiCode), busiErr) s.logError(transactionID, apiKey, jiyiResp.SeqNo, busiErr, requestPayload) return nil, err } cleanedData, err := stripBusiMetaFromData(jiyiResp.Data) if err != nil { err = errors.Join(ErrSystem, fmt.Errorf("响应 data 清理失败: %w", err)) s.logError(transactionID, apiKey, jiyiResp.SeqNo, err, requestPayload) return nil, err } if isEmptyBusinessResult(cleanedData) { if isQueryBillingAPIKey(apiKey) { jiyiResp.Data = map[string]interface{}{} s.logResponse(transactionID, apiKey, resp.StatusCode, duration, jiyiResp.SeqNo) return &jiyiResp, nil } emptyErr := NewJiyiBusiError(BusiCodeNotFound, "未查得") err = errors.Join(ErrNotFound, emptyErr) s.logError(transactionID, apiKey, jiyiResp.SeqNo, err, requestPayload) return nil, err } jiyiResp.Data = cleanedData s.logResponse(transactionID, apiKey, resp.StatusCode, duration, jiyiResp.SeqNo) return &jiyiResp, nil } type jiyiDataBusiMeta struct { BusiCode int `json:"busiCode"` BusiMsg string `json:"busiMsg"` } func parseDataBusiInfo(data interface{}) (busiCode int, busiMsg string, ok bool) { if data == nil { return 0, "", false } raw, err := json.Marshal(data) if err != nil { return 0, "", false } var meta jiyiDataBusiMeta if err := json.Unmarshal(raw, &meta); err != nil { return 0, "", false } return meta.BusiCode, meta.BusiMsg, true } func stripBusiMetaFromData(data interface{}) (interface{}, error) { raw, err := json.Marshal(data) if err != nil { return nil, err } var payload map[string]interface{} if err := json.Unmarshal(raw, &payload); err != nil { return nil, err } delete(payload, "busiCode") delete(payload, "busiMsg") return payload, nil } // isEmptyBusinessResult 判断业务载荷是否为空(无 result,或 result 无有效内容) func isEmptyBusinessResult(data interface{}) bool { payload, ok := data.(map[string]interface{}) if !ok || len(payload) == 0 { return true } result, exists := payload["result"] if !exists || result == nil { return true } return isEmptyValue(result) } func isEmptyValue(v interface{}) bool { if v == nil { return true } switch x := v.(type) { case string: return strings.TrimSpace(x) == "" case map[string]interface{}: if len(x) == 0 { return true } for _, item := range x { if !isEmptyValue(item) { return false } } return true case []interface{}: return len(x) == 0 default: return false } } func cloneBody(body map[string]string) map[string]string { if body == nil { return map[string]string{} } cloned := make(map[string]string, len(body)+2) for k, v := range body { cloned[k] = v } return cloned } func wrapHTTPError(err error) error { if err == context.DeadlineExceeded { return errors.Join(ErrDatasource, err) } if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() { return errors.Join(ErrDatasource, err) } switch err.Error() { case "context deadline exceeded", "timeout", "Client.Timeout exceeded", "net/http: request canceled": return errors.Join(ErrDatasource, err) default: return errors.Join(ErrSystem, err) } }