This commit is contained in:
2026-07-25 13:56:32 +08:00
51 changed files with 2656 additions and 40 deletions

View File

@@ -846,6 +846,7 @@ type RongxingConfig struct {
AppID string `mapstructure:"app_id"` // 应用 ID
PrivateKey string `mapstructure:"private_key"` // RSA 私钥PEM 或 PKCS#8 Base64
Timeout time.Duration `mapstructure:"timeout"`
Proxy string `mapstructure:"proxy"` // 可选,仅戎行请求走此代理,如 socks5://rongxing-vpn:1080
Logging RongxingLoggingConfig `mapstructure:"logging"`
}

View File

@@ -233,6 +233,13 @@ type JRZQH6M3Req struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
}
// JRZQ5J5CReq 无间司南-纯黑版C10 金融黑名单
type JRZQ5J5CReq struct {
Name string `json:"name" validate:"required,min=1,validName"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
}
// JRZQW3L8Req 信用司南
type JRZQW3L8Req struct {
Name string `json:"name" validate:"required,min=1,validName"`
@@ -1350,6 +1357,10 @@ type FLXGC4CTReq struct {
AuthPDFBase64 string `json:"auth_pdf_base64" validate:"required,validAuthPDFBase64"`
}
type FLXLLD77Req struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type QYGLLUCMReq struct {
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
AuthPDFBase64 string `json:"auth_pdf_base64" validate:"required,validAuthPDFBase64"`
@@ -1360,3 +1371,37 @@ type JRZQ0OO1Req struct {
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type JRZQT2C1Req struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
Name string `json:"name" validate:"required,min=1,validName"`
}
type JRZQ2PV2Req struct {
MobileNo string `json:"mobile_no" validate:"required,min=11,max=11,validMobileNo"`
IDCard string `json:"id_card" validate:"required,validIDCard"`
}
type QCXGV20OReq struct {
VinCode string `json:"vin_code" validate:"required"`
}
type QCXGVP00Req struct {
VinCode string `json:"vin_code" validate:"required"`
}
type QCXGVJ70Req struct {
PlateNo string `json:"plate_no" validate:"required"`
}
type QCXG2Y8XReq struct {
PlateNo string `json:"plate_no" validate:"omitempty"`
IDCard string `json:"id_card" validate:"omitempty,validIDCard"`
VinCode string `json:"vin_code" validate:"omitempty"`
}
type QCXG74YTReq struct {
PlateNo string `json:"plate_no" validate:"omitempty"`
IDCard string `json:"id_card" validate:"omitempty,validIDCard"`
VinCode string `json:"vin_code" validate:"omitempty"`
}

View File

@@ -176,6 +176,7 @@ func registerAllProcessors(combService *comb.CombService) {
"FLXGJI17": flxg.ProcessFLXGJI17Request, //董监高司法综合信息核验
"FLXGMMG7": flxg.ProcessFLXGMMG7Request, //个人诉讼定制版
"FLXGC4CT": flxg.ProcessFLXGC4CTRequest, //个人涉诉案件查询汇博
"FLXLLD77": flxg.ProcessFLXLLD77Request, //劳动仲裁查询天远
// JRZQ系列处理器
"JRZQOICN": jrzq.ProcessJRZQOICNRequest, // 银行卡四要素
"JRZQMDQ1": jrzq.ProcessJRZQMDQ1Request, // 银行卡OCR数卖
@@ -209,6 +210,12 @@ func registerAllProcessors(combService *comb.CombService) {
"JRZQW3L8": jrzq.ProcessJRZQW3L8Request, //信用司南
"JRZQP8D2": jrzq.ProcessJRZQP8D2Request, //全景雷达BH
"JRZQ0OO1": jrzq.ProcessJRZQ0OO1Request, //戎行贷后信息
"JRZQT2C1": jrzq.ProcessJRZQT2C1Request, //探针C
"JRZQT2A1": jrzq.ProcessJRZQT2A1Request, //探针A
"JRZQS74D": jrzq.ProcessJRZQS74DRequest, //申请借贷
"JRZQ2F4W": jrzq.ProcessJRZQ2F4WRequest, //支付行为
"JRZQ2PV2": jrzq.ProcessJRZQ2PV2Request, //租赁通用版v2
"JRZQ5J5C": jrzq.ProcessJRZQ5J5CRequest, //无间司南-纯黑版C10 金融黑名单
// QYGL系列处理器
"QYGL7HBN": qygl.ProcessQYGL7HBNRequest, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I
@@ -223,6 +230,11 @@ func registerAllProcessors(combService *comb.CombService) {
"QCXGCP77": qcxg.ProcessQCXGCP77Request, //全国车辆配置查验(车辆详情)
"QCXGX2X6": qcxg.ProcessQCXGX2X6Request, //行驶证信息核验V2
"QCXG1S2L": qcxg.ProcessQCXG1S2LRequest, //汽车车辆五项
"QCXGV20O": qcxg.ProcessQCXGV20ORequest, //车VIN查询估值
"QCXGVP00": qcxg.ProcessQCXGVP00Request, //车VIN查车牌号
"QCXGVJ70": qcxg.ProcessQCXGVJ70Request, //车牌号查vin
"QCXG2Y8X": qcxg.ProcessQCXG2Y8XRequest, //商业险有效性
"QCXG74YT": qcxg.ProcessQCXG74YTRequest, //交强险有效性
// YYSY系列处理器
"YYSY0YYV": yysy.ProcessYYSY0YYVRequest, //运营商二要素查询

View File

@@ -320,7 +320,18 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
"IVYZX7J9": &dto.IVYZX7J9Req{}, //学籍核验
"QCXG6U5G": &dto.QCXG6U5GReq{}, //车辆核验
"JRZQ0OO1": &dto.JRZQ0OO1Req{}, //戎行贷后信息 Info360
"JRZQT2C1": &dto.JRZQT2C1Req{}, //探针C
"JRZQT2A1": &dto.JRZQT2C1Req{}, //探针A
"JRZQS74D": &dto.JRZQT2C1Req{}, //申请借贷
"JRZQ2F4W": &dto.JRZQT2C1Req{}, //支付行为
"JRZQ2PV2": &dto.JRZQ2PV2Req{}, //租赁通用版v2
"FLXLLD77": &dto.FLXLLD77Req{}, //劳动仲裁查询天远
"QCXGV20O": &dto.QCXGV20OReq{}, //车VIN查询估值
"QCXGVP00": &dto.QCXGVP00Req{}, //车VIN查车牌号
"QCXGVJ70": &dto.QCXGVJ70Req{}, //车牌号查vin
"QCXG2Y8X": &dto.QCXG2Y8XReq{}, //商业险有效性
"QCXG74YT": &dto.QCXG74YTReq{}, //交强险有效性
"JRZQ5J5C": &dto.JRZQ5J5CReq{}, //无间司南-纯黑版C10 金融黑名单
}
// 优先返回已配置的DTO

View File

@@ -0,0 +1,33 @@
package flxg
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
)
// ProcessFLXLLD77Request FLXLLD77 API处理方法 - 劳动仲裁查询天远
func ProcessFLXLLD77Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.FLXLLD77Req
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)
}
reqdata := map[string]interface{}{
"name": paramsDto.Name,
"id_card": paramsDto.IDCard,
}
respBytes, err := deps.TianyuanapiService.CallAPI(ctx, "IVYZ0S0D", reqdata)
if err != nil {
return nil, errors.Join(processors.ErrDatasource, err)
}
return respBytes, nil
}

View File

@@ -42,5 +42,9 @@ func ProcessJRZQ0OO1Request(ctx context.Context, params []byte, deps *processors
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
transformed, err := transformJRZQ0OO1Response(respBytes)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return transformed, nil
}

View File

@@ -0,0 +1,70 @@
package jrzq
import (
"encoding/json"
"fmt"
"strings"
)
// JRZQ0OO1 字段命名约定:{窗口}_{机构类型}_{事件}_{指标}
// 窗口: m1/m3/m6/m12
// 机构类型: 省略=全部贷款bank=银行cfc=消金sloan=小贷
// 事件: total/severe_overdue/overdue/repay_fail/repay_ok/repay_remind/
//
// disburse_fail/disburse_ok/audit_fail/audit_ok/apply/register/verify_code/marketing/other
//
// 指标: cnt=次数org=机构数
var (
jrzq0OO1Periods = []string{"m1", "m3", "m6", "m12"}
jrzq0OO1InstTypes = []string{"", "bank", "cfc", "sloan"}
jrzq0OO1Events = []string{
"total", "severe_overdue", "overdue", "repay_fail", "repay_ok",
"repay_remind", "disburse_fail", "disburse_ok", "audit_fail", "audit_ok",
"apply", "register", "verify_code", "marketing", "other",
}
jrzq0OO1Metrics = []string{"cnt", "org"}
// jrzq0OO1FieldMap B001-B480 -> 语义化扁平字段名
jrzq0OO1FieldMap = buildJRZQ0OO1FieldMap()
)
func buildJRZQ0OO1FieldMap() map[string]string {
m := make(map[string]string, 480)
n := 1
for _, period := range jrzq0OO1Periods {
for _, inst := range jrzq0OO1InstTypes {
for _, event := range jrzq0OO1Events {
for _, metric := range jrzq0OO1Metrics {
key := fmt.Sprintf("B%03d", n)
parts := []string{period}
if inst != "" {
parts = append(parts, inst)
}
parts = append(parts, event, metric)
m[key] = strings.Join(parts, "_")
n++
}
}
}
}
return m
}
// transformJRZQ0OO1Response 将 Info360 的 Bxxx 字段重命名为语义化扁平字段。
// 未在映射表中的键原样保留。
func transformJRZQ0OO1Response(respBytes []byte) ([]byte, error) {
var raw map[string]interface{}
if err := json.Unmarshal(respBytes, &raw); err != nil {
return nil, err
}
out := make(map[string]interface{}, len(raw))
for k, v := range raw {
if renamed, ok := jrzq0OO1FieldMap[k]; ok {
out[renamed] = v
continue
}
out[k] = v
}
return json.Marshal(out)
}

View File

@@ -0,0 +1,156 @@
package jrzq
import (
"encoding/json"
"testing"
)
func TestBuildJRZQ0OO1FieldMap_CountAndBounds(t *testing.T) {
if len(jrzq0OO1FieldMap) != 480 {
t.Fatalf("expected 480 mappings, got %d", len(jrzq0OO1FieldMap))
}
if got, want := jrzq0OO1FieldMap["B001"], "m1_total_cnt"; got != want {
t.Fatalf("B001: got %q want %q", got, want)
}
if got, want := jrzq0OO1FieldMap["B480"], "m12_sloan_other_org"; got != want {
t.Fatalf("B480: got %q want %q", got, want)
}
}
func TestJRZQ0OO1FieldMap_DocSpotChecks(t *testing.T) {
cases := map[string]string{
"B001": "m1_total_cnt", // 最近1个月贷款类总次数
"B009": "m1_repay_ok_cnt", // 最近1个月还款成功次数
"B031": "m1_bank_total_cnt", // 最近1个月银行类贷款总次数
"B129": "m3_repay_ok_cnt", // 最近3个月还款成功次数
"B130": "m3_repay_ok_org", // 最近3个月还款成功机构数
"B133": "m3_disburse_fail_cnt", // 最近3个月放款失败次数
"B134": "m3_disburse_fail_org", // 最近3个月放款失败机构数
"B157": "m3_bank_repay_fail_cnt", // 最近3个月银行类还款失败次数
"B158": "m3_bank_repay_fail_org", // 最近3个月银行类还款失败机构数
"B219": "m3_sloan_repay_ok_cnt", // 最近3个月小贷类还款成功次数
"B220": "m3_sloan_repay_ok_org", // 最近3个月小贷类还款成功机构数
"B223": "m3_sloan_disburse_fail_cnt", // 最近3个月小贷类放款失败次数
"B224": "m3_sloan_disburse_fail_org", // 最近3个月小贷类放款失败机构数
"B247": "m6_repay_fail_cnt", // 最近6个月还款失败次数
"B248": "m6_repay_fail_org", // 最近6个月还款失败机构数
"B257": "m6_audit_fail_cnt", // 最近6个月审核失败次数
"B258": "m6_audit_fail_org", // 最近6个月审核失败机构数
"B261": "m6_apply_cnt", // 最近6个月贷款申请次数
"B262": "m6_apply_org", // 最近6个月贷款申请机构数
"B285": "m6_bank_disburse_ok_cnt", // 最近6个月银行类放款成功次数
"B286": "m6_bank_disburse_ok_org", // 最近6个月银行类放款成功机构数
"B347": "m6_sloan_audit_fail_cnt", // 最近6个月小贷类审核失败次数
"B348": "m6_sloan_audit_fail_org", // 最近6个月小贷类审核失败机构数
"B351": "m6_sloan_apply_cnt", // 最近6个月小贷类贷款申请次数
"B352": "m6_sloan_apply_org", // 最近6个月小贷类贷款申请机构数
"B375": "m12_disburse_ok_cnt", // 最近12个月放款成功次数
"B376": "m12_disburse_ok_org", // 最近12个月放款成功机构数
"B061": "m1_cfc_total_cnt", // 最近1个月持牌消金类贷款总次数
"B301": "m6_cfc_total_cnt", // 最近6个月消金类贷款总次数
}
for src, want := range cases {
if got := jrzq0OO1FieldMap[src]; got != want {
t.Errorf("%s: got %q want %q", src, got, want)
}
}
}
func TestTransformJRZQ0OO1Response_Sample(t *testing.T) {
in := []byte(`{
"B129": 6,
"B130": 3,
"B133": 4,
"B134": 2,
"B157": 2,
"B158": 1,
"B219": 6,
"B220": 3,
"B223": 4,
"B224": 2,
"B247": 2,
"B248": 1,
"B257": 6,
"B258": 3,
"B261": 4,
"B262": 2,
"B285": 2,
"B286": 1,
"B347": 6,
"B348": 3,
"B351": 4,
"B352": 2,
"B375": 2,
"B376": 1
}`)
outBytes, err := transformJRZQ0OO1Response(in)
if err != nil {
t.Fatal(err)
}
var out map[string]interface{}
if err := json.Unmarshal(outBytes, &out); err != nil {
t.Fatal(err)
}
want := map[string]float64{
"m3_repay_ok_cnt": 6,
"m3_repay_ok_org": 3,
"m3_disburse_fail_cnt": 4,
"m3_disburse_fail_org": 2,
"m3_bank_repay_fail_cnt": 2,
"m3_bank_repay_fail_org": 1,
"m3_sloan_repay_ok_cnt": 6,
"m3_sloan_repay_ok_org": 3,
"m3_sloan_disburse_fail_cnt": 4,
"m3_sloan_disburse_fail_org": 2,
"m6_repay_fail_cnt": 2,
"m6_repay_fail_org": 1,
"m6_audit_fail_cnt": 6,
"m6_audit_fail_org": 3,
"m6_apply_cnt": 4,
"m6_apply_org": 2,
"m6_bank_disburse_ok_cnt": 2,
"m6_bank_disburse_ok_org": 1,
"m6_sloan_audit_fail_cnt": 6,
"m6_sloan_audit_fail_org": 3,
"m6_sloan_apply_cnt": 4,
"m6_sloan_apply_org": 2,
"m12_disburse_ok_cnt": 2,
"m12_disburse_ok_org": 1,
}
if len(out) != len(want) {
t.Fatalf("field count: got %d want %d, out=%v", len(out), len(want), out)
}
for k, w := range want {
got, ok := out[k]
if !ok {
t.Errorf("missing key %s", k)
continue
}
num, ok := got.(float64)
if !ok || num != w {
t.Errorf("%s: got %#v want %v", k, got, w)
}
}
}
func TestTransformJRZQ0OO1Response_KeepUnknown(t *testing.T) {
in := []byte(`{"B001":1,"extra":"x"}`)
outBytes, err := transformJRZQ0OO1Response(in)
if err != nil {
t.Fatal(err)
}
var out map[string]interface{}
if err := json.Unmarshal(outBytes, &out); err != nil {
t.Fatal(err)
}
if _, ok := out["m1_total_cnt"]; !ok {
t.Fatal("expected m1_total_cnt")
}
if out["extra"] != "x" {
t.Fatalf("expected extra kept, got %#v", out["extra"])
}
}

View File

@@ -0,0 +1,50 @@
package jrzq
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessJRZQ2F4WRequest 支付行为(上游 jy000017body encryptType=1 MD5 + timestamp
func ProcessJRZQ2F4WRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.JRZQT2C1Req
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)
}
body := map[string]string{
"name": paramsDto.Name,
"idNo": paramsDto.IDCard,
"mobile": paramsDto.MobileNo,
}
apiKey := "jy000039"
apiPath := "/api/v1/payment/index"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,49 @@
package jrzq
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessJRZQ2PV2Request 租赁通用版v2
func ProcessJRZQ2PV2Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.JRZQ2PV2Req
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)
}
body := map[string]string{
"idNo": paramsDto.IDCard,
"mobile": paramsDto.MobileNo,
}
apiKey := "jy000049"
apiPath := "/api/v1/generlea/agreev2"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,50 @@
package jrzq
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessJRZQ5J5CRequest 无间司南-纯黑A版上游 jy000052
func ProcessJRZQ5J5CRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.JRZQ5J5CReq
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)
}
body := map[string]string{
"name": paramsDto.Name,
"idCard": paramsDto.IDCard,
"mobile": paramsDto.MobileNo,
}
apiKey := "jy000055"
apiPath := "/api/v1/blacklist/c10/verify"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.TopEncryptionOptions(1))
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,50 @@
package jrzq
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessJRZQS74DRequest 申请借贷(上游 jy000017body encryptType=1 MD5 + timestamp
func ProcessJRZQS74DRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.JRZQT2C1Req
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)
}
body := map[string]string{
"name": paramsDto.Name,
"idNo": paramsDto.IDCard,
"mobile": paramsDto.MobileNo,
}
apiKey := "jy000048"
apiPath := "/api/v1/pd/loan/behavior"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,50 @@
package jrzq
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessJRZQT2A1Request 探针A上游 jy000017body encryptType=1 MD5 + timestamp
func ProcessJRZQT2A1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.JRZQT2C1Req
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)
}
body := map[string]string{
"name": jiyi.MD5Encrypt(paramsDto.Name),
"idCard": jiyi.MD5Encrypt(paramsDto.IDCard),
"mobile": jiyi.MD5Encrypt(paramsDto.MobileNo),
}
apiKey := "jy000017"
apiPath := "/api/v1/probe/a/verify/encrypt"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.CaveInvestOptions(1))
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,50 @@
package jrzq
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessJRZQT2C1Request 探针C上游 jy000008顶层 encryptionType=2 MD5
func ProcessJRZQT2C1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.JRZQT2C1Req
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)
}
body := map[string]string{
"name": jiyi.MD5Encrypt(paramsDto.Name),
"idCard": jiyi.MD5Encrypt(paramsDto.IDCard),
"mobile": jiyi.MD5Encrypt(paramsDto.MobileNo),
}
apiKey := "jy000008"
apiPath := "/api/v1/probe/c/verify"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.TopEncryptionOptions(2))
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,57 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
"github.com/google/uuid"
)
// ProcessQCXG2Y8XRequest 商业险有效性
func ProcessQCXG2Y8XRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG2Y8XReq
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)
}
if paramsDto.PlateNo == "" && paramsDto.IDCard == "" && paramsDto.VinCode == "" {
return nil, errors.Join(processors.ErrInvalidParam, errors.New("plate_no、id_card、vin_code 至少需要传其中一个"))
}
body := map[string]string{
"licenseNo": paramsDto.PlateNo,
"idNo": paramsDto.IDCard,
"vin": paramsDto.VinCode,
"nonce": uuid.New().String(),
}
apiKey := "jy000019"
apiPath := "/api/v1/commercial/validity"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,57 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
"github.com/google/uuid"
)
// ProcessQCXG74YTRequest 交强险有效性
func ProcessQCXG74YTRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXG74YTReq
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)
}
if paramsDto.PlateNo == "" && paramsDto.IDCard == "" && paramsDto.VinCode == "" {
return nil, errors.Join(processors.ErrInvalidParam, errors.New("plate_no、id_card、vin_code 至少需要传其中一个"))
}
body := map[string]string{
"licenseNo": paramsDto.PlateNo,
"idNo": paramsDto.IDCard,
"vin": paramsDto.VinCode,
"nonce": uuid.New().String(),
}
apiKey := "jy000020"
apiPath := "/api/v1/compulsory/validity"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,48 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessQCXGV20ORequest 车VIN查询估值
func ProcessQCXGV20ORequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGV20OReq
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)
}
body := map[string]string{
"vin": paramsDto.VinCode,
}
apiKey := "jy000025"
apiPath := "/api/v1/vehicle/valuation"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,48 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessQCXGVJ70Request 车牌号查vin
func ProcessQCXGVJ70Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGVJ70Req
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)
}
body := map[string]string{
"plateNo": paramsDto.PlateNo,
}
apiKey := "jy000018"
apiPath := "/api/v1/car/vin"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -0,0 +1,48 @@
package qcxg
import (
"context"
"encoding/json"
"errors"
"hyapi-server/internal/domains/api/dto"
"hyapi-server/internal/domains/api/services/processors"
"hyapi-server/internal/infrastructure/external/jiyi"
)
// ProcessQCXGVP00Request 车VIN查车牌号
func ProcessQCXGVP00Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
var paramsDto dto.QCXGVP00Req
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)
}
body := map[string]string{
"vin": paramsDto.VinCode,
}
apiKey := "jy000028"
apiPath := "/api/v1/plateNumber/vin"
resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.DefaultCallOptions())
if err != nil {
if errors.Is(err, jiyi.ErrDatasource) {
return nil, errors.Join(processors.ErrDatasource, err)
}
if errors.Is(err, jiyi.ErrNotFound) {
return nil, errors.Join(processors.ErrNotFound, err)
}
return nil, errors.Join(processors.ErrSystem, err)
}
respBytes, err := json.Marshal(resp.Data)
if err != nil {
return nil, errors.Join(processors.ErrSystem, err)
}
return respBytes, nil
}

View File

@@ -19,8 +19,14 @@ 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版
}

View File

@@ -5,14 +5,21 @@ import (
)
// generateCurlCommand 生成可直接复现的 curl 命令,便于联调排查。
func generateCurlCommand(method, url string, headers map[string]string, body string) string {
// proxyURL 非空时附加 -x便于确认线上是否应走 SOCKS。
func generateCurlCommand(method, requestURL string, headers map[string]string, body, proxyURL string) string {
var cmd strings.Builder
cmd.WriteString("curl -X ")
cmd.WriteString(method)
cmd.WriteString(" '")
cmd.WriteString(url)
cmd.WriteString(requestURL)
cmd.WriteString("'")
if p := strings.TrimSpace(proxyURL); p != "" {
cmd.WriteString(" \\\n -x '")
cmd.WriteString(escapeSingleQuotes(p))
cmd.WriteString("'")
}
for key, value := range headers {
cmd.WriteString(" \\\n -H '")
cmd.WriteString(key)

View File

@@ -0,0 +1,47 @@
package rongxing
import (
"net/http"
"testing"
"time"
)
func TestNewHTTPClient_Direct(t *testing.T) {
client, err := newHTTPClient(5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if client.Timeout != 5*time.Second {
t.Fatalf("timeout = %v", client.Timeout)
}
if client.Transport != nil && client.Transport != http.DefaultTransport {
// 直连允许 Transport 为 nil使用 DefaultTransport
}
}
func TestNewHTTPClient_Socks5(t *testing.T) {
client, err := newHTTPClient(3*time.Second, "socks5://rongxing-vpn:1080")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if client.Transport == nil {
t.Fatal("expected custom transport for socks5")
}
}
func TestNewHTTPClient_HTTPProxy(t *testing.T) {
client, err := newHTTPClient(3*time.Second, "http://127.0.0.1:8888")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if client.Transport == nil {
t.Fatal("expected custom transport for http proxy")
}
}
func TestNewHTTPClient_UnsupportedScheme(t *testing.T) {
_, err := newHTTPClient(time.Second, "ftp://x")
if err == nil {
t.Fatal("expected error for unsupported scheme")
}
}

View File

@@ -46,5 +46,6 @@ func NewRongxingServiceWithConfig(cfg *config.Config) (*RongxingService, error)
AppID: cfg.Rongxing.AppID,
PrivateKey: cfg.Rongxing.PrivateKey,
Timeout: timeout,
}, logger), nil
Proxy: cfg.Rongxing.Proxy,
}, logger)
}

View File

@@ -7,7 +7,10 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
@@ -15,6 +18,7 @@ import (
"hyapi-server/internal/shared/external_logger"
"go.uber.org/zap"
"golang.org/x/net/proxy"
)
const (
@@ -32,15 +36,17 @@ type serviceConfig struct {
AppID string
PrivateKey string
Timeout time.Duration
Proxy string
}
// RongxingService 戎行数据源服务
type RongxingService struct {
config serviceConfig
logger *external_logger.ExternalServiceLogger
client *http.Client
tokenMu sync.RWMutex
cachedToken string
// loginMu 避免并发登录打爆对方;不缓存 token每次业务调用重新登录。
loginMu sync.Mutex
}
// apiResponse 戎行统一响应。code 可能是数字或字符串;扣费以 consumeFlag 为准。
@@ -65,12 +71,52 @@ func (r apiResponse) code() string {
}
// NewRongxingService 创建戎行服务实例
func NewRongxingService(cfg serviceConfig, logger *external_logger.ExternalServiceLogger) *RongxingService {
func NewRongxingService(cfg serviceConfig, logger *external_logger.ExternalServiceLogger) (*RongxingService, error) {
if cfg.Timeout <= 0 {
cfg.Timeout = defaultRequestTimeout
}
cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
return &RongxingService{config: cfg, logger: logger}
client, err := newHTTPClient(cfg.Timeout, cfg.Proxy)
if err != nil {
return nil, err
}
return &RongxingService{config: cfg, logger: logger, client: client}, nil
}
// newHTTPClient 构建 HTTP 客户端proxyURL 支持 socks5/socks5h/http/https空则直连。
func newHTTPClient(timeout time.Duration, proxyURL string) (*http.Client, error) {
client := &http.Client{Timeout: timeout}
proxyURL = strings.TrimSpace(proxyURL)
if proxyURL == "" {
return client, nil
}
u, err := url.Parse(proxyURL)
if err != nil {
return nil, fmt.Errorf("解析 proxy 失败: %w", err)
}
switch strings.ToLower(u.Scheme) {
case "socks5", "socks5h":
dialer, err := proxy.FromURL(u, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("创建 SOCKS 代理失败: %w", err)
}
transport := &http.Transport{}
if cd, ok := dialer.(proxy.ContextDialer); ok {
transport.DialContext = cd.DialContext
} else {
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
}
}
client.Transport = transport
case "http", "https":
client.Transport = &http.Transport{Proxy: http.ProxyURL(u)}
default:
return nil, fmt.Errorf("不支持的 proxy scheme: %s支持 socks5/socks5h/http/https", u.Scheme)
}
return client, nil
}
// GetConfig 获取运行时配置
@@ -80,7 +126,7 @@ func (s *RongxingService) GetConfig() serviceConfig {
// CallAPI 通用业务接口调用。
// apiPath 为相对路径(如 /third/loan/info360reqData 为已组装好的请求体。
// Token 获取与 Header 注入由服务内部处理401/403 时自动刷新 Token 并重试一次。
// 每次调用重新登录取 Token不本地缓存若业务码/HTTP 仍为 401/403再登录重试一次。
func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData map[string]interface{}) ([]byte, error) {
apiKey := strings.Trim(apiPath, "/")
@@ -118,7 +164,7 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
"Content-Type": "application/json",
headerDmsToken: token,
}
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr)
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr, s.config.Proxy)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
if err != nil {
@@ -139,7 +185,6 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
respStr := string(respBody)
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
s.clearToken()
if attempt == 0 {
continue
}
@@ -162,6 +207,11 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
}
code := resp.code()
// 对方常以 HTTP 200 + body.code=401 表示 token 无效(非 HTTP 401
if isTokenInvalidCode(code) && attempt == 0 {
continue
}
payload := extractBusinessPayload(resp.Data)
// 扣费只看 consumeFlag1 扣费按成功返回0 不扣费
@@ -180,32 +230,20 @@ func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData m
return nil, errors.Join(ErrDatasource, errors.New("请求失败"))
}
// getToken 每次重新登录,不缓存 token。
func (s *RongxingService) getToken(ctx context.Context, transactionID string) (string, error) {
s.tokenMu.RLock()
token := s.cachedToken
s.tokenMu.RUnlock()
if token != "" {
return token, nil
}
s.tokenMu.Lock()
defer s.tokenMu.Unlock()
if s.cachedToken != "" {
return s.cachedToken, nil
}
token, err := s.login(ctx, transactionID)
if err != nil {
return "", err
}
s.cachedToken = token
return token, nil
s.loginMu.Lock()
defer s.loginMu.Unlock()
return s.login(ctx, transactionID)
}
func (s *RongxingService) clearToken() {
s.tokenMu.Lock()
s.cachedToken = ""
s.tokenMu.Unlock()
func isTokenInvalidCode(code string) bool {
switch strings.TrimSpace(code) {
case "401", "403":
return true
default:
return false
}
}
func (s *RongxingService) login(ctx context.Context, transactionID string) (string, error) {
@@ -253,7 +291,7 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri
bodyStr := string(bodyBytes)
headers := map[string]string{"Content-Type": "application/json"}
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr)
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr, s.config.Proxy)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
if err != nil {
@@ -309,10 +347,9 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri
}
func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) {
client := &http.Client{Timeout: s.config.Timeout}
resp, err := client.Do(req)
resp, err := s.client.Do(req)
if err != nil {
return nil, 0, err
return nil, 0, s.wrapTransportError(req.URL.String(), err)
}
defer resp.Body.Close()
@@ -323,6 +360,49 @@ func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) {
return body, resp.StatusCode, nil
}
// wrapTransportError 把超时/拒连/代理失败等包装成可读诊断,便于区分「联不通」原因。
func (s *RongxingService) wrapTransportError(targetURL string, err error) error {
diag := classifyTransportError(err)
proxyMode := strings.TrimSpace(s.config.Proxy)
if proxyMode == "" {
proxyMode = "(直连,未配置 proxy)"
}
return fmt.Errorf(
"戎行 HTTP 失败 target=%s proxy=%s diagnosis=%s cause=%w",
targetURL, proxyMode, diag, err,
)
}
func classifyTransportError(err error) string {
if err == nil {
return "unknown"
}
msg := err.Error()
switch {
case os.IsTimeout(err) || errors.Is(err, context.DeadlineExceeded) ||
strings.Contains(msg, "Client.Timeout") || strings.Contains(msg, "deadline exceeded"):
return "请求超时(未在 timeout 内收到响应头;可能:目标 192.168.3.43:7007 不可达、VPN/SOCKS 未转发、或服务无响应)"
case strings.Contains(msg, "connection refused"):
return "连接被拒绝(端口未监听或代理/目标拒绝)"
case strings.Contains(msg, "no such host") || strings.Contains(msg, "lookup"):
return "DNS/主机名解析失败(检查 rongxing-vpn 服务名或目标域名)"
case strings.Contains(msg, "network is unreachable") || strings.Contains(msg, "no route to host"):
return "网络不可达(无路由;直连内网 IP 时常见于未走 VPN/代理)"
case strings.Contains(msg, "i/o timeout") || strings.Contains(msg, "TLS handshake timeout"):
return "传输层超时(链路通但握手/读写超时)"
case strings.Contains(msg, "proxy") || strings.Contains(msg, "socks"):
return "代理链路异常(检查 socks5://rongxing-vpn:1080 与 VPN 容器)"
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return "网络超时"
}
// 完整原始错误在同一条日志的 error/cause 字段中,不在别的文件
return "其他网络错误(见本条日志 error 全文)"
}
func (s *RongxingService) validateConfig() error {
if s.config.BaseURL == "" {
return errors.New("戎行 url 未配置")
@@ -385,12 +465,35 @@ func (s *RongxingService) logErrorWithCurl(transactionID, apiKey string, err err
if s.logger == nil {
return
}
proxyMode := strings.TrimSpace(s.config.Proxy)
if proxyMode == "" {
proxyMode = "(直连,未配置 proxy)"
}
s.logger.LogErrorWithFields("rongxing API错误",
zap.String("transaction_id", transactionID),
zap.String("api_code", apiKey),
zap.String("base_url", s.config.BaseURL),
zap.String("proxy", proxyMode),
zap.String("diagnosis", extractDiagnosis(err)),
zap.Error(err),
zap.Any("params", payload),
zap.String("curl", curlCmd),
zap.String("response_body", respBody),
)
}
func extractDiagnosis(err error) string {
if err == nil {
return ""
}
const marker = "diagnosis="
msg := err.Error()
if i := strings.Index(msg, marker); i >= 0 {
rest := msg[i+len(marker):]
if j := strings.Index(rest, " cause="); j >= 0 {
return rest[:j]
}
return rest
}
return classifyTransportError(err)
}

View File

@@ -0,0 +1,43 @@
package rongxing
import (
"context"
"errors"
"strings"
"testing"
"time"
)
func TestClassifyTransportError_Timeout(t *testing.T) {
err := errors.New(`Post "http://192.168.3.43:7007/auth/login": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`)
got := classifyTransportError(err)
if !strings.Contains(got, "请求超时") {
t.Fatalf("got %q", got)
}
}
func TestClassifyTransportError_Deadline(t *testing.T) {
got := classifyTransportError(context.DeadlineExceeded)
if !strings.Contains(got, "请求超时") {
t.Fatalf("got %q", got)
}
}
func TestWrapTransportError_IncludesProxy(t *testing.T) {
svc, err := NewRongxingService(serviceConfig{
BaseURL: "http://192.168.3.43:7007",
Timeout: time.Second,
Proxy: "socks5://rongxing-vpn:1080",
}, nil)
if err != nil {
t.Fatal(err)
}
wrapped := svc.wrapTransportError("http://192.168.3.43:7007/auth/login", context.DeadlineExceeded)
msg := wrapped.Error()
if !strings.Contains(msg, "proxy=socks5://rongxing-vpn:1080") {
t.Fatalf("missing proxy in error: %s", msg)
}
if !strings.Contains(msg, "diagnosis=") {
t.Fatalf("missing diagnosis in error: %s", msg)
}
}