Merge branch 'main' of http://1.117.67.95:3000/team/hyapi-server
This commit is contained in:
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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, //运营商二要素查询
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
}
|
||||
@@ -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 支付行为(上游 jy000017;body 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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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 申请借贷(上游 jy000017;body 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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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(上游 jy000017;body 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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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, ¶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)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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版
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
47
internal/infrastructure/external/rongxing/http_client_test.go
vendored
Normal file
47
internal/infrastructure/external/rongxing/http_client_test.go
vendored
Normal 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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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/info360),reqData 为已组装好的请求体。
|
||||
// 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)
|
||||
|
||||
// 扣费只看 consumeFlag:1 扣费(按成功返回),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)
|
||||
}
|
||||
|
||||
43
internal/infrastructure/external/rongxing/transport_error_test.go
vendored
Normal file
43
internal/infrastructure/external/rongxing/transport_error_test.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user