From 507a4fd4d4a10818bc3322d6619408071ed79643 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Thu, 23 Jul 2026 13:39:55 +0800 Subject: [PATCH 01/13] fadd --- internal/domains/api/dto/api_request_dto.go | 11 ++++ .../api/services/api_request_service.go | 5 ++ .../api/services/form_config_service.go | 6 ++- .../processors/jrzq/jrzq2f4w_processor.go | 50 +++++++++++++++++++ .../processors/jrzq/jrzq2pv2_processor.go | 49 ++++++++++++++++++ .../processors/jrzq/jrzqs74d_processor.go | 50 +++++++++++++++++++ .../processors/jrzq/jrzqt2a1_processor.go | 50 +++++++++++++++++++ .../processors/jrzq/jrzqt2c1_processor.go | 50 +++++++++++++++++++ 8 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 internal/domains/api/services/processors/jrzq/jrzq2f4w_processor.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzq2pv2_processor.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzqs74d_processor.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzqt2a1_processor.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzqt2c1_processor.go diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 9ac1d3e..6f74699 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -1360,3 +1360,14 @@ 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"` +} diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index c9682d4..01afc40 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -209,6 +209,11 @@ 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 // QYGL系列处理器 "QYGL7HBN": qygl.ProcessQYGL7HBNRequest, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I) diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index b232ffe..7d9fb3e 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -320,7 +320,11 @@ 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 } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/jrzq/jrzq2f4w_processor.go b/internal/domains/api/services/processors/jrzq/jrzq2f4w_processor.go new file mode 100644 index 0000000..8a1569f --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq2f4w_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/jrzq/jrzq2pv2_processor.go b/internal/domains/api/services/processors/jrzq/jrzq2pv2_processor.go new file mode 100644 index 0000000..23d96d2 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq2pv2_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/jrzq/jrzqs74d_processor.go b/internal/domains/api/services/processors/jrzq/jrzqs74d_processor.go new file mode 100644 index 0000000..005c329 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzqs74d_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/jrzq/jrzqt2a1_processor.go b/internal/domains/api/services/processors/jrzq/jrzqt2a1_processor.go new file mode 100644 index 0000000..fa8d2ea --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzqt2a1_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/jrzq/jrzqt2c1_processor.go b/internal/domains/api/services/processors/jrzq/jrzqt2c1_processor.go new file mode 100644 index 0000000..49e76ac --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzqt2c1_processor.go @@ -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 +} From 1eefe41bac31322371ca2198a5675783f8e34c75 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Thu, 23 Jul 2026 15:13:49 +0800 Subject: [PATCH 02/13] test --- .gitignore | 6 +- config.yaml | 2 + configs/env.development.yaml | 2 + configs/env.production.yaml | 2 + deployments/openvpn/Dockerfile | 25 ++ deployments/openvpn/README.md | 13 + .../docker-compose.rongxing-vpn.snippet.yml | 31 +++ deployments/openvpn/entrypoint.sh | 69 +++++ deployments/openvpn/env.example | 6 + deployments/openvpn/tiany0721.ovpn.example | 23 ++ deployments/openvpn/upload-checklist.txt | 17 ++ docker-compose.prod.yml | 43 +++ docs/戎行OpenVPN接入方案.md | 262 ++++++++++++++++++ internal/config/config.go | 1 + .../external/rongxing/http_client_test.go | 47 ++++ .../external/rongxing/rongxing_factory.go | 3 +- .../external/rongxing/rongxing_service.go | 52 +++- secrets/openvpn/.gitkeep | 3 + 18 files changed, 601 insertions(+), 6 deletions(-) create mode 100644 deployments/openvpn/Dockerfile create mode 100644 deployments/openvpn/README.md create mode 100644 deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml create mode 100644 deployments/openvpn/entrypoint.sh create mode 100644 deployments/openvpn/env.example create mode 100644 deployments/openvpn/tiany0721.ovpn.example create mode 100644 deployments/openvpn/upload-checklist.txt create mode 100644 docs/戎行OpenVPN接入方案.md create mode 100644 internal/infrastructure/external/rongxing/http_client_test.go create mode 100644 secrets/openvpn/.gitkeep diff --git a/.gitignore b/.gitignore index db5acfd..d3773b8 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,8 @@ resources/Pure_Component/ *.dll *.so *.dylib -cmd/api/__debug_bin* \ No newline at end of file +cmd/api/__debug_bin* + +# 戎行 OpenVPN 机密(仅保留 secrets/openvpn/.gitkeep) +secrets/openvpn/* +!secrets/openvpn/.gitkeep \ No newline at end of file diff --git a/config.yaml b/config.yaml index a8855de..59106fe 100644 --- a/config.yaml +++ b/config.yaml @@ -860,6 +860,8 @@ rongxing: app_id: "hykj" private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw==" timeout: 10s + # 仅戎行请求走 VPN SOCKS;本机已连 OpenVPN GUI 时可留空。生产 Docker:socks5://rongxing-vpn:1080 + proxy: "" logging: enabled: true log_dir: "logs/external_services" diff --git a/configs/env.development.yaml b/configs/env.development.yaml index e3e9812..68e935b 100644 --- a/configs/env.development.yaml +++ b/configs/env.development.yaml @@ -279,6 +279,8 @@ rongxing: app_id: "hykj" private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw==" timeout: 10s + # 开发机用 OpenVPN GUI 直连时可留空;若走本地 Docker VPN 可填 socks5://rongxing-vpn:1080 + proxy: "" logging: enabled: true log_dir: "logs/external_services" diff --git a/configs/env.production.yaml b/configs/env.production.yaml index d3c7815..1c8306e 100644 --- a/configs/env.production.yaml +++ b/configs/env.production.yaml @@ -245,6 +245,8 @@ rongxing: app_id: "hykj" private_key: "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAI1eWDx325U/DCpaw+394555voDNwKyfg6rAIBYGSmJDmUyCi6c8LeqVtYnmtUMJrTDrfUWFG7jQ+mQy65rXMY7zrZ0Cc+orP7uRgrvCBsH1775KuSji8TkVbEBw+Upro7FuNkutBItNxZCvcpFpNqNBwCCkCK1xscVN8gErxc7/AgMBAAECgYAL/ZVHWU7Ni5TyrLmTBwAjWD+RX9V4iGkb3QLiGCayZS05NGNq/ytrCvqxMSY6HIKAZ6Du+hmXvr+JXll/slvyGs1ETOgDi7563RAT/2TVicZF16IM2d7nhK6eTJffmiG2ZZC8n043F0QNposleEIMhM251iT1xiwZfg+QqHB0EQJBAI+BFcbYl+Vxpxdouvuwq11gYMTNepcGdY3OaPZzW4sQA41s+6aZhZ7tAk65Tk7PgLfdU2yimBKNkJstlSUT1w8CQQD8MKjAi5wngnDO+04n5mt5EotpNy6xpvkQP2izL0FWKmvwZtc0ihJpxa64vfo4+1jE8YXx1/qGe16A15fmywkRAkAbImVjvAC8ucjGfF8eyEEe3uJtVA0iEW6Y6bafIyDkIpsJWtoanlzNuDL/f7p23HWSTp8/o17t4ya8sNnKsP2xAkEArIQc7JqUl/KDeRQwwtq9anVlKPS23JB8kMDPvsP0zhz2+d1gGnDZZ8HzZC2RnqlScGdIWciFeLmsTDcvkpISAQJANaQU0uKBPvgMx+uedbCbn9MfYmpEiONHblrINH8WAa3Z1pr7R0wBUog1fEWimnxOX5wGwsGmDOMw5B+4xJ4Zfw==" timeout: 10s + # 生产:戎行请求经 Docker 内 rongxing-vpn 的 SOCKS5 进企业内网 + proxy: "socks5://rongxing-vpn:1080" logging: enabled: true log_dir: "logs/external_services" diff --git a/deployments/openvpn/Dockerfile b/deployments/openvpn/Dockerfile new file mode 100644 index 0000000..ae3c004 --- /dev/null +++ b/deployments/openvpn/Dockerfile @@ -0,0 +1,25 @@ +# 戎行专用:OpenVPN 客户端 + SOCKS5(仅内网代理,不接管默认路由) +FROM alpine:3.21 + +RUN apk add --no-cache \ + openvpn \ + bash \ + iproute2 \ + microsocks \ + curl + +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENV VPN_DIR=/vpn \ + SOCKS_HOST=0.0.0.0 \ + SOCKS_PORT=1080 \ + RONGXING_HOST=192.168.3.43 + +VOLUME ["/vpn"] +EXPOSE 1080 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=45s --retries=5 \ + CMD ip link show tun0 >/dev/null 2>&1 || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/deployments/openvpn/README.md b/deployments/openvpn/README.md new file mode 100644 index 0000000..e253e96 --- /dev/null +++ b/deployments/openvpn/README.md @@ -0,0 +1,13 @@ +# Rongxing OpenVPN(部署附件) + +完整方案见:[docs/戎行OpenVPN接入方案.md](../../docs/戎行OpenVPN接入方案.md) + +| 文件 | 用途 | +|------|------| +| `Dockerfile` | OpenVPN + microsocks 自建镜像 | +| `entrypoint.sh` | 启动 VPN(仅路由戎行 IP)并监听 SOCKS5 `:1080` | +| `tiany0721.ovpn.example` | ovpn 模板 | +| `docker-compose.rongxing-vpn.snippet.yml` | 独立编排参考(prod 已内置) | +| `upload-checklist.txt` | 上传清单 | + +**真实证书放到服务器 secrets 目录或 `./secrets/openvpn/`,不要提交 Git。** diff --git a/deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml b/deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml new file mode 100644 index 0000000..a704e63 --- /dev/null +++ b/deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml @@ -0,0 +1,31 @@ +# 已并入 docker-compose.prod.yml 的 rongxing-vpn 服务。 +# 保留本文件便于 1Panel 单独编排时参考。 +# +# 1Panel 机密目录示例: +# RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn +# +# 独立栈时需 external 网络(名称以 docker network ls 为准): +# networks: +# hyapi-network: +# external: true +# name: hyapi-server_hyapi-network + +services: + rongxing-vpn: + build: + context: ./deployments/openvpn + dockerfile: Dockerfile + image: hyapi-rongxing-vpn:local + container_name: hyapi-rongxing-vpn + environment: + TZ: Asia/Shanghai + RONGXING_HOST: "192.168.3.43" + volumes: + - ${RONGXING_VPN_CONFIG:-./secrets/openvpn}:/vpn:ro + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + networks: + - hyapi-network + restart: unless-stopped diff --git a/deployments/openvpn/entrypoint.sh b/deployments/openvpn/entrypoint.sh new file mode 100644 index 0000000..4190986 --- /dev/null +++ b/deployments/openvpn/entrypoint.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# OpenVPN 连通后拉起 SOCKS5;仅服务戎行内网代理。 +set -euo pipefail + +VPN_DIR="${VPN_DIR:-/vpn}" +SOCKS_HOST="${SOCKS_HOST:-0.0.0.0}" +SOCKS_PORT="${SOCKS_PORT:-1080}" +RONGXING_HOST="${RONGXING_HOST:-192.168.3.43}" + +cd "$VPN_DIR" + +CONF="" +for f in *.conf *.ovpn; do + if [ -f "$f" ]; then + CONF="$f" + break + fi +done + +if [ -z "$CONF" ]; then + echo "[rongxing-vpn] 未在 ${VPN_DIR} 找到 .ovpn/.conf,请挂载证书目录" + exit 1 +fi + +echo "[rongxing-vpn] 使用配置: ${CONF}" +echo "[rongxing-vpn] 仅路由 ${RONGXING_HOST}/32,SOCKS5 ${SOCKS_HOST}:${SOCKS_PORT}" + +openvpn \ + --config "$CONF" \ + --cd "$VPN_DIR" \ + --route-nopull \ + --route "$RONGXING_HOST" 255.255.255.255 \ + --daemon \ + --writepid /var/run/openvpn.pid \ + --log /var/log/openvpn.log \ + --verb 3 + +for i in $(seq 1 90); do + if ip link show tun0 >/dev/null 2>&1; then + echo "[rongxing-vpn] tun0 已就绪 (${i}s)" + break + fi + if [ "$i" -eq 90 ]; then + echo "[rongxing-vpn] 等待 tun0 超时,OpenVPN 日志:" + cat /var/log/openvpn.log || true + exit 1 + fi + sleep 1 +done + +# 前台跑 microsocks,进程退出则容器退出;同时监控 openvpn +microsocks -i "$SOCKS_HOST" -p "$SOCKS_PORT" & +SOCKS_PID=$! + +cleanup() { + kill "$SOCKS_PID" 2>/dev/null || true + if [ -f /var/run/openvpn.pid ]; then + kill "$(cat /var/run/openvpn.pid)" 2>/dev/null || true + fi +} +trap cleanup EXIT INT TERM + +while kill -0 "$(cat /var/run/openvpn.pid)" 2>/dev/null && kill -0 "$SOCKS_PID" 2>/dev/null; do + sleep 5 +done + +echo "[rongxing-vpn] OpenVPN 或 SOCKS 已退出" +cat /var/log/openvpn.log || true +exit 1 diff --git a/deployments/openvpn/env.example b/deployments/openvpn/env.example new file mode 100644 index 0000000..baab3e7 --- /dev/null +++ b/deployments/openvpn/env.example @@ -0,0 +1,6 @@ +# 可选:复制为 .env 供 docker compose 读取 +# 1Panel 推荐: +RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn + +# 本地默认(不设则用 compose 内 ./secrets/openvpn): +# RONGXING_VPN_CONFIG=./secrets/openvpn diff --git a/deployments/openvpn/tiany0721.ovpn.example b/deployments/openvpn/tiany0721.ovpn.example new file mode 100644 index 0000000..c18e024 --- /dev/null +++ b/deployments/openvpn/tiany0721.ovpn.example @@ -0,0 +1,23 @@ +# 复制为服务器上的 tiany0721.ovpn,并与 ca.crt / 证书 / 密钥同目录放置。 +# 勿将真实证书提交到 Git。 + +client +dev tun +proto tcp +remote 111.31.14.226 32199 +resolv-retry infinite +nobind +persist-key +persist-tun +ca ca.crt +cert tiany0721.crt +key tiany0721.key +remote-cert-tls server +tls-auth ta.key 1 +cipher AES-256-CBC +compress lz4-v2 +verb 3 + +# 仅路由戎行主机,不接管默认网关(其它流量仍走容器原出口) +route-nopull +route 192.168.3.43 255.255.255.255 diff --git a/deployments/openvpn/upload-checklist.txt b/deployments/openvpn/upload-checklist.txt new file mode 100644 index 0000000..e671b8c --- /dev/null +++ b/deployments/openvpn/upload-checklist.txt @@ -0,0 +1,17 @@ +# 从本机 C:\Users\a1726\Desktop\11\OpenVPN\config 上传到服务器: +# /opt/1panel/apps/hyapi-secrets/openvpn/ +# +# 需要: +# [ ] tiany0721.ovpn (上传后按 example 加上 route-nopull + route) +# [ ] ca.crt +# [ ] tiany0721.crt +# [ ] tiany0721.key +# [ ] ta.key +# +# 不需要: +# [x] bin/、doc/、Uninstall.exe、openvpn-gui.exe 等 Windows 安装文件 +# +# 权限: +# chmod 700 /opt/1panel/apps/hyapi-secrets /opt/1panel/apps/hyapi-secrets/openvpn +# chmod 600 *.key +# chmod 644 *.crt *.ovpn diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 8162020..2c5e8d2 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -38,6 +38,45 @@ services: ports: - "25010:5432" + # 戎行 OpenVPN 客户端 + SOCKS5(仅 192.168.3.43 走隧道) + # 证书目录:默认 ./secrets/openvpn;1Panel 建议: + # export RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn + rongxing-vpn: + build: + context: ./deployments/openvpn + dockerfile: Dockerfile + image: hyapi-rongxing-vpn:local + container_name: hyapi-rongxing-vpn + environment: + TZ: Asia/Shanghai + RONGXING_HOST: "192.168.3.43" + SOCKS_HOST: "0.0.0.0" + SOCKS_PORT: "1080" + volumes: + - ${RONGXING_VPN_CONFIG:-./secrets/openvpn}:/vpn:ro + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + networks: + - hyapi-network + # 禁止把 1080 暴露到宿主机公网 + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "ip link show tun0 >/dev/null 2>&1 || exit 1"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 60s + deploy: + resources: + limits: + memory: 256M + cpus: "0.3" + reservations: + memory: 64M + cpus: "0.1" + # Redis 缓存 (生产环境) redis: image: redis:8.0.2 @@ -101,6 +140,8 @@ services: condition: service_healthy redis: condition: service_healthy + rongxing-vpn: + condition: service_started healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s @@ -134,6 +175,8 @@ services: condition: service_healthy redis: condition: service_healthy + rongxing-vpn: + condition: service_started healthcheck: test: ["CMD", "ps", "aux", "|", "grep", "worker"] interval: 30s diff --git a/docs/戎行OpenVPN接入方案.md b/docs/戎行OpenVPN接入方案.md new file mode 100644 index 0000000..109b110 --- /dev/null +++ b/docs/戎行OpenVPN接入方案.md @@ -0,0 +1,262 @@ +# 戎行数据源 OpenVPN 接入方案(1Panel) + +> 目标:**仅** `rongxing`(`http://192.168.3.43:7007`)走企业 VPN;Postgres / Redis / 其它外部 API 仍走原出口。 +> 方案:**OpenVPN 容器 + SOCKS5 代理 + 戎行 HTTP 客户端单独走代理**。 + +--- + +## 1. 架构 + +``` + hyapi-network(现有 bridge) +┌─────────────────────────────────────────────────────────────┐ +│ hyapi-app / hyapi-worker │ +│ ├─ DB / Redis / 其它 API ──► 默认出口(不经 VPN) │ +│ └─ 戎行 Client ──► socks5://rongxing-vpn:1080 │ +│ │ │ +│ rongxing-vpn(openvpn + microsocks)│ │ +│ TUN ──► OpenVPN Server 111.31.14.226:32199 │ +│ ──► 企业内网 192.168.3.43:7007 │ +└─────────────────────────────────────────────────────────────┘ +``` + +要点: + +- 业务容器网络身份不变(端口、健康检查、服务发现保持现状)。 +- VPN 只负责进企业网;对外暴露的是 Docker 内网 SOCKS5,不映射到公网。 +- OpenVPN 使用 `route-nopull`,只加 `192.168.3.43`,避免抢走默认路由。 + +--- + +## 2. VPN 材料要不要上传? + +**要上传,但只上传「客户端配置与证书」,不要上传整个 Windows OpenVPN 安装目录。** + +| 本地路径(你机器) | 是否上传 | 说明 | +|--------------------|----------|------| +| `OpenVPN\config\tiany0721.ovpn` | ✅ | 连接配置(服务器上会再加路由控制指令) | +| `OpenVPN\config\ca.crt` | ✅ | CA | +| `OpenVPN\config\tiany0721.crt` | ✅ | 客户端证书 | +| `OpenVPN\config\tiany0721.key` | ✅ | 客户端私钥(敏感) | +| `OpenVPN\config\ta.key` | ✅ | tls-auth | +| `OpenVPN\bin\`、`doc\`、`Uninstall.exe` 等 | ❌ | Windows 安装包,容器不用 | + +服务器上使用 **Linux 版 OpenVPN 镜像**,复用上述 5 个文件即可。 + +--- + +## 3. 1Panel 推荐目录(最优落点) + +假设 1Panel 数据盘常用根为 `/opt/1panel`(若你改过安装目录,把前缀换成实际路径)。 + +**推荐:密钥与业务代码分离。** + +```text +/opt/1panel/apps/hyapi/ # 业务 Compose / 代码(可随发布更新) +├── docker-compose.prod.yml +├── config.yaml / configs/... +├── ... +└── (不要把 .key 放进 git 仓库目录) + +/opt/1panel/apps/hyapi-secrets/openvpn/ # ★ VPN 机密目录(推荐) +├── tiany0721.ovpn # 服务器侧改过的版本(见下文) +├── ca.crt +├── tiany0721.crt +├── tiany0721.key # 权限 600 +└── ta.key # 权限 600 +``` + +备选(同样可以): + +```text +/data/hyapi/secrets/openvpn/ # 若 1Panel 数据盘挂在 /data +``` + +**不要放:** + +- 网站静态目录 `/opt/1panel/www/...`(易被误暴露) +- 仓库内 `hyapi-server/config/` 或会随 CI 同步的路径(防泄漏) + +**权限建议(SSH 上执行):** + +```bash +sudo mkdir -p /opt/1panel/apps/hyapi-secrets/openvpn +sudo chmod 700 /opt/1panel/apps/hyapi-secrets +sudo chmod 700 /opt/1panel/apps/hyapi-secrets/openvpn +# 上传文件后: +sudo chmod 600 /opt/1panel/apps/hyapi-secrets/openvpn/*.key +sudo chmod 644 /opt/1panel/apps/hyapi-secrets/openvpn/*.{crt,ovpn} +``` + +--- + +## 4. 服务器侧 OpenVPN 配置(准备好再挂载) + +把本地 `tiany0721.ovpn` 拷到服务器后,**追加**以下行(保留原有 `remote` / 证书引用): + +```conf +# --- 仅路由戎行内网,不接管默认网关 --- +route-nopull +route 192.168.3.43 255.255.255.255 +``` + +完整示例见仓库:`deployments/openvpn/tiany0721.ovpn.example`。 + +证书相对路径保持与 `.ovpn` 同目录即可(`ca ca.crt` 等),挂载整个目录到容器 `/vpn`。 + +--- + +## 5. Compose 片段(接入现有 prod) + +参考文件:`deployments/openvpn/docker-compose.rongxing-vpn.snippet.yml` + +核心服务示意: + +```yaml + rongxing-vpn: + image: ghcr.io/linuxserver/openvpn-as:latest # 不推荐 AS;见下方推荐镜像 +``` + +**推荐用轻量客户端镜像**(文档配套 snippet 已按此写): + +- 镜像:`dperson/openvpn-client` **或** 自建 `alpine + openvpn + microsocks` +- 能力:`cap_add: [NET_ADMIN]`,`devices: [/dev/net/tun]` +- 挂载:`/opt/1panel/apps/hyapi-secrets/openvpn:/vpn:ro` +- 网络:加入现有 `hyapi-network` +- **不要**把 `1080` 映射到宿主机公网端口 + +业务侧: + +- `hyapi-app` /(若异步也会调戎行)`hyapi-worker` 保持原 `networks: [hyapi-network]` +- 配置增加代理地址:`socks5://rongxing-vpn:1080` + +--- + +## 6. 应用配置(仅戎行) + +在 `rongxing` 段增加代理字段(实现代码时再接线;配置先预留): + +```yaml +rongxing: + url: "http://192.168.3.43:7007" + account: "..." + password: "..." + app_id: "..." + private_key: "..." + timeout: 10s + # 仅戎行走 VPN 旁路代理;留空则直连(开发机已连 VPN 时可留空) + proxy: "socks5://rongxing-vpn:1080" + logging: + # ... +``` + +开发机若已用 GUI 连上 VPN,`proxy` 留空即可,无需起 Docker VPN。 + +--- + +## 7. 代码改动清单(后续开发) + +当前 `RongxingService` 使用裸 `http.Client`,需: + +1. `RongxingConfig` / `serviceConfig` 增加 `Proxy string` +2. `NewRongxingServiceWithConfig` 传入 `cfg.Rongxing.Proxy` +3. 创建 `http.Client` 时:若 `Proxy` 非空,用 `http.ProxyURL` 或 SOCKS5 dialer(`golang.org/x/net/proxy`) +4. **只改戎行包**,其它外部服务不动 + +验证点:未配 `proxy` 时行为与现在一致。 + +--- + +## 8. 1Panel 上操作清单 + +### 8.1 一次性准备 + +1. SSH 登录服务器,创建 `/opt/1panel/apps/hyapi-secrets/openvpn/` +2. 用 SFTP / `scp` 上传 5 个文件(见第 2 节) +3. 编辑服务器上的 `tiany0721.ovpn`,加上 `route-nopull` + `route 192.168.3.43 ...` +4. 设好目录/文件权限(第 3 节) +5. 确认内核允许 TUN:多数 1Panel/Linux 默认有 `/dev/net/tun`;若容器报错再装 `tun` 模块 + +### 8.2 接入 Compose + +任选其一: + +- **A. 1Panel「编排」**:打开现有 hyapi 的 Compose,粘贴 `rongxing-vpn` 服务片段,挂载 secrets 目录,重新部署 +- **B. 独立编排**:单独建一个 `rongxing-vpn` 栈,但必须加入与 hyapi **同一个** Docker network(`hyapi-network` 的外部网络名以 `docker network ls` 为准) + +查现有网络名: + +```bash +docker network ls | grep hyapi +docker inspect hyapi-app-prod --format '{{json .NetworkSettings.Networks}}' +``` + +若网络是 compose 项目前缀(如 `hyapi-server_hyapi-network`),独立栈里要: + +```yaml +networks: + hyapi-network: + external: true + name: hyapi-server_hyapi-network # 以实际名为准 +``` + +### 8.3 发布后验证 + +```bash +# 1) VPN 容器是否拿到 tun / 路由 +docker exec -it ip route +# 应能看到 192.168.3.43 经 tun0 + +# 2) 在 VPN 容器内测内网(镜像若有 curl) +docker exec -it wget -qO- --timeout=5 http://192.168.3.43:7007/ || true + +# 3) 从业务容器经 SOCKS5 测(需业务镜像有 curl,或临时 alpine) +docker run --rm --network curlimages/curl:8.5.0 \ + -x socks5h://rongxing-vpn:1080 -m 10 -v http://192.168.3.43:7007/ + +# 4) 确认「不走代理」时业务仍能访问公网/库(对照现有 health) +curl -f http://127.0.0.1:25000/health +``` + +业务侧再打一笔戎行真实查询,看 `logs/external_services` 下戎行日志是否成功。 + +--- + +## 9. 安全与运维 + +- `.key` / `ta.key` / 账号密码:**不进 Git**;secrets 目录不挂到可下载的 Web 根。 +- SOCKS5 **仅**监听容器网络,不 `-p 1080:1080` 到宿主机。 +- VPN 证书到期前找对方续期,替换 secrets 目录文件后 `docker compose restart rongxing-vpn`。 +- 断线策略:容器 `restart: unless-stopped`;OpenVPN 侧已有 `resolv-retry infinite` / `persist-tun`。 +- 监控:可对 `rongxing-vpn` 做 1Panel 容器存活告警;业务侧戎行错误率告警。 + +--- + +## 10. 本地文件 → 服务器对照表 + +| 本地 | 服务器推荐路径 | +|------|----------------| +| `...\OpenVPN\config\tiany0721.ovpn` | `/opt/1panel/apps/hyapi-secrets/openvpn/tiany0721.ovpn`(改过路由) | +| `...\OpenVPN\config\ca.crt` | 同目录 | +| `...\OpenVPN\config\tiany0721.crt` | 同目录 | +| `...\OpenVPN\config\tiany0721.key` | 同目录(600) | +| `...\OpenVPN\config\ta.key` | 同目录(600) | +| Windows `bin/` 整包 | **不上传** | + +--- + +## 11. 实施状态与顺序 + +**代码与 Compose 已落地**(见仓库当前变更): + +- `rongxing` 支持 `proxy`(SOCKS5/HTTP) +- `docker-compose.prod.yml` 已加入 `rongxing-vpn` 服务 +- `deployments/openvpn/` 自建镜像(OpenVPN + microsocks) +- 生产配置 `configs/env.production.yaml`:`proxy: "socks5://rongxing-vpn:1080"` + +**1Panel 服务器仍需人工完成:** + +1. 创建 `/opt/1panel/apps/hyapi-secrets/openvpn/`,上传 5 个证书/配置文件(可用本机已备好的 `secrets/openvpn/` 内容) +2. 设置环境变量:`RONGXING_VPN_CONFIG=/opt/1panel/apps/hyapi-secrets/openvpn` +3. 拉取/构建并重新部署 Compose(确保服务器能访问 Docker Hub / 镜像源以下载 `alpine`) +4. 验证:`docker exec hyapi-rongxing-vpn ip route` 含 `192.168.3.43`;业务侧打一笔戎行查询 diff --git a/internal/config/config.go b/internal/config/config.go index b263956..251d959 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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"` } diff --git a/internal/infrastructure/external/rongxing/http_client_test.go b/internal/infrastructure/external/rongxing/http_client_test.go new file mode 100644 index 0000000..0068c22 --- /dev/null +++ b/internal/infrastructure/external/rongxing/http_client_test.go @@ -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") + } +} diff --git a/internal/infrastructure/external/rongxing/rongxing_factory.go b/internal/infrastructure/external/rongxing/rongxing_factory.go index 795777a..fdd7dab 100644 --- a/internal/infrastructure/external/rongxing/rongxing_factory.go +++ b/internal/infrastructure/external/rongxing/rongxing_factory.go @@ -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) } diff --git a/internal/infrastructure/external/rongxing/rongxing_service.go b/internal/infrastructure/external/rongxing/rongxing_service.go index 2713ebb..229681d 100644 --- a/internal/infrastructure/external/rongxing/rongxing_service.go +++ b/internal/infrastructure/external/rongxing/rongxing_service.go @@ -7,7 +7,9 @@ import ( "errors" "fmt" "io" + "net" "net/http" + "net/url" "strings" "sync" "time" @@ -15,6 +17,7 @@ import ( "hyapi-server/internal/shared/external_logger" "go.uber.org/zap" + "golang.org/x/net/proxy" ) const ( @@ -32,12 +35,14 @@ 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 @@ -65,12 +70,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 获取运行时配置 @@ -309,8 +354,7 @@ 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 } diff --git a/secrets/openvpn/.gitkeep b/secrets/openvpn/.gitkeep new file mode 100644 index 0000000..7ab5b85 --- /dev/null +++ b/secrets/openvpn/.gitkeep @@ -0,0 +1,3 @@ +# 本地/仓库内占位:真实证书请放到此目录(勿提交 *.key / *.crt / *.ovpn) +# 1Panel 推荐改用绝对路径:/opt/1panel/apps/hyapi-secrets/openvpn +# Compose 通过环境变量 RONGXING_VPN_CONFIG 覆盖挂载源目录。 From 9ab7ba66f3f846487344e3c9e59280d330e692fa Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Thu, 23 Jul 2026 15:22:10 +0800 Subject: [PATCH 03/13] f --- deployments/openvpn/Dockerfile | 19 +++++++++++-------- deployments/openvpn/README.md | 3 ++- deployments/openvpn/entrypoint.sh | 14 ++++++++++---- deployments/openvpn/sockd.conf.template | 14 ++++++++++++++ 4 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 deployments/openvpn/sockd.conf.template diff --git a/deployments/openvpn/Dockerfile b/deployments/openvpn/Dockerfile index ae3c004..e48987b 100644 --- a/deployments/openvpn/Dockerfile +++ b/deployments/openvpn/Dockerfile @@ -1,14 +1,17 @@ -# 戎行专用:OpenVPN 客户端 + SOCKS5(仅内网代理,不接管默认路由) -FROM alpine:3.21 +# 戎行专用:OpenVPN 客户端 + Dante SOCKS5(仅内网代理) +FROM alpine:3.19 -RUN apk add --no-cache \ - openvpn \ - bash \ - iproute2 \ - microsocks \ - curl +# 与业务镜像一致,使用国内 apk 源 +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ + && apk add --no-cache \ + openvpn \ + bash \ + iproute2 \ + dante-server \ + curl COPY entrypoint.sh /entrypoint.sh +COPY sockd.conf.template /etc/sockd.conf.template RUN chmod +x /entrypoint.sh ENV VPN_DIR=/vpn \ diff --git a/deployments/openvpn/README.md b/deployments/openvpn/README.md index e253e96..6a88074 100644 --- a/deployments/openvpn/README.md +++ b/deployments/openvpn/README.md @@ -4,8 +4,9 @@ | 文件 | 用途 | |------|------| -| `Dockerfile` | OpenVPN + microsocks 自建镜像 | +| `Dockerfile` | OpenVPN + dante-server(sockd)自建镜像 | | `entrypoint.sh` | 启动 VPN(仅路由戎行 IP)并监听 SOCKS5 `:1080` | +| `sockd.conf.template` | Dante SOCKS 配置模板 | | `tiany0721.ovpn.example` | ovpn 模板 | | `docker-compose.rongxing-vpn.snippet.yml` | 独立编排参考(prod 已内置) | | `upload-checklist.txt` | 上传清单 | diff --git a/deployments/openvpn/entrypoint.sh b/deployments/openvpn/entrypoint.sh index 4190986..fcacefe 100644 --- a/deployments/openvpn/entrypoint.sh +++ b/deployments/openvpn/entrypoint.sh @@ -1,5 +1,5 @@ #!/bin/bash -# OpenVPN 连通后拉起 SOCKS5;仅服务戎行内网代理。 +# OpenVPN 连通后拉起 SOCKS5(dante/sockd);仅服务戎行内网代理。 set -euo pipefail VPN_DIR="${VPN_DIR:-/vpn}" @@ -48,9 +48,15 @@ for i in $(seq 1 90); do sleep 1 done -# 前台跑 microsocks,进程退出则容器退出;同时监控 openvpn -microsocks -i "$SOCKS_HOST" -p "$SOCKS_PORT" & -SOCKS_PID=$! +# 生成 dante 配置并后台启动 sockd +sed "s/__SOCKS_PORT__/${SOCKS_PORT}/g" /etc/sockd.conf.template > /etc/sockd.conf +sockd -f /etc/sockd.conf -D +SOCKS_PID="$(pidof sockd | awk '{print $1}')" +if [ -z "${SOCKS_PID}" ]; then + echo "[rongxing-vpn] sockd 启动失败" + exit 1 +fi +echo "[rongxing-vpn] SOCKS5 已监听 :${SOCKS_PORT} (pid=${SOCKS_PID})" cleanup() { kill "$SOCKS_PID" 2>/dev/null || true diff --git a/deployments/openvpn/sockd.conf.template b/deployments/openvpn/sockd.conf.template new file mode 100644 index 0000000..3539b3b --- /dev/null +++ b/deployments/openvpn/sockd.conf.template @@ -0,0 +1,14 @@ +# Dante SOCKS5 — entrypoint 会替换 __SOCKS_PORT__ +logoutput: stderr +internal: 0.0.0.0 port = __SOCKS_PORT__ +external: eth0 +socksmethod: none +clientmethod: none + +client pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 +} + +socks pass { + from: 0.0.0.0/0 to: 0.0.0.0/0 +} From 3bf04be3478121c5cf0f611cf54c9e3201541a34 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Thu, 23 Jul 2026 15:59:00 +0800 Subject: [PATCH 04/13] fadd --- deployments/openvpn/Dockerfile | 17 +- deployments/openvpn/README.md | 4 +- deployments/openvpn/entrypoint.sh | 26 +- deployments/openvpn/sockd.conf.template | 14 - .../openvpn/third_party/microsocks/.gitignore | 3 + .../openvpn/third_party/microsocks/COPYING | 24 + .../openvpn/third_party/microsocks/Makefile | 35 ++ .../openvpn/third_party/microsocks/README.md | 65 +++ .../third_party/microsocks/create-dist.sh | 28 + .../openvpn/third_party/microsocks/install.sh | 67 +++ .../openvpn/third_party/microsocks/sblist.c | 73 +++ .../openvpn/third_party/microsocks/sblist.h | 92 ++++ .../third_party/microsocks/sblist_delete.c | 9 + .../openvpn/third_party/microsocks/server.c | 64 +++ .../openvpn/third_party/microsocks/server.h | 49 ++ .../openvpn/third_party/microsocks/sockssrv.c | 482 ++++++++++++++++++ .../external/rongxing/curl_helper.go | 11 +- .../external/rongxing/rongxing_service.go | 72 ++- .../external/rongxing/transport_error_test.go | 43 ++ 19 files changed, 1144 insertions(+), 34 deletions(-) delete mode 100644 deployments/openvpn/sockd.conf.template create mode 100644 deployments/openvpn/third_party/microsocks/.gitignore create mode 100644 deployments/openvpn/third_party/microsocks/COPYING create mode 100644 deployments/openvpn/third_party/microsocks/Makefile create mode 100644 deployments/openvpn/third_party/microsocks/README.md create mode 100644 deployments/openvpn/third_party/microsocks/create-dist.sh create mode 100644 deployments/openvpn/third_party/microsocks/install.sh create mode 100644 deployments/openvpn/third_party/microsocks/sblist.c create mode 100644 deployments/openvpn/third_party/microsocks/sblist.h create mode 100644 deployments/openvpn/third_party/microsocks/sblist_delete.c create mode 100644 deployments/openvpn/third_party/microsocks/server.c create mode 100644 deployments/openvpn/third_party/microsocks/server.h create mode 100644 deployments/openvpn/third_party/microsocks/sockssrv.c create mode 100644 internal/infrastructure/external/rongxing/transport_error_test.go diff --git a/deployments/openvpn/Dockerfile b/deployments/openvpn/Dockerfile index e48987b..b852efb 100644 --- a/deployments/openvpn/Dockerfile +++ b/deployments/openvpn/Dockerfile @@ -1,18 +1,25 @@ -# 戎行专用:OpenVPN 客户端 + Dante SOCKS5(仅内网代理) +# 戎行专用:OpenVPN 客户端 + microsocks SOCKS5(走系统路由/tun0) +FROM alpine:3.19 AS socks-builder + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ + && apk add --no-cache build-base + +WORKDIR /src +COPY third_party/microsocks/ ./ +RUN make && strip microsocks + FROM alpine:3.19 -# 与业务镜像一致,使用国内 apk 源 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ && apk add --no-cache \ openvpn \ bash \ iproute2 \ - dante-server \ curl +COPY --from=socks-builder /src/microsocks /usr/local/bin/microsocks COPY entrypoint.sh /entrypoint.sh -COPY sockd.conf.template /etc/sockd.conf.template -RUN chmod +x /entrypoint.sh +RUN chmod +x /entrypoint.sh /usr/local/bin/microsocks ENV VPN_DIR=/vpn \ SOCKS_HOST=0.0.0.0 \ diff --git a/deployments/openvpn/README.md b/deployments/openvpn/README.md index 6a88074..7b432b1 100644 --- a/deployments/openvpn/README.md +++ b/deployments/openvpn/README.md @@ -4,9 +4,9 @@ | 文件 | 用途 | |------|------| -| `Dockerfile` | OpenVPN + dante-server(sockd)自建镜像 | +| `Dockerfile` | OpenVPN + 自编译 microsocks | | `entrypoint.sh` | 启动 VPN(仅路由戎行 IP)并监听 SOCKS5 `:1080` | -| `sockd.conf.template` | Dante SOCKS 配置模板 | +| `third_party/microsocks/` | SOCKS5 源码(避免 Alpine 无包 / Dante 绑错网卡) | | `tiany0721.ovpn.example` | ovpn 模板 | | `docker-compose.rongxing-vpn.snippet.yml` | 独立编排参考(prod 已内置) | | `upload-checklist.txt` | 上传清单 | diff --git a/deployments/openvpn/entrypoint.sh b/deployments/openvpn/entrypoint.sh index fcacefe..ee34455 100644 --- a/deployments/openvpn/entrypoint.sh +++ b/deployments/openvpn/entrypoint.sh @@ -1,5 +1,5 @@ #!/bin/bash -# OpenVPN 连通后拉起 SOCKS5(dante/sockd);仅服务戎行内网代理。 +# OpenVPN 连通后拉起 microsocks;出口跟随系统路由(含 tun0),避免 Dante 绑错网卡。 set -euo pipefail VPN_DIR="${VPN_DIR:-/vpn}" @@ -48,15 +48,25 @@ for i in $(seq 1 90); do sleep 1 done -# 生成 dante 配置并后台启动 sockd -sed "s/__SOCKS_PORT__/${SOCKS_PORT}/g" /etc/sockd.conf.template > /etc/sockd.conf -sockd -f /etc/sockd.conf -D -SOCKS_PID="$(pidof sockd | awk '{print $1}')" -if [ -z "${SOCKS_PID}" ]; then - echo "[rongxing-vpn] sockd 启动失败" +echo "[rongxing-vpn] 路由表:" +ip route || true + +# 直连自检(不阻断) +if curl -sS -m 5 -o /tmp/rx_probe.out -w "[rongxing-vpn] 直连探测 HTTP %{http_code}\n" "http://${RONGXING_HOST}:7007/" ; then + head -c 200 /tmp/rx_probe.out 2>/dev/null || true + echo +else + echo "[rongxing-vpn] 直连探测失败: http://${RONGXING_HOST}:7007/" +fi + +microsocks -i "$SOCKS_HOST" -p "$SOCKS_PORT" & +SOCKS_PID=$! +sleep 1 +if ! kill -0 "$SOCKS_PID" 2>/dev/null; then + echo "[rongxing-vpn] microsocks 启动失败" exit 1 fi -echo "[rongxing-vpn] SOCKS5 已监听 :${SOCKS_PORT} (pid=${SOCKS_PID})" +echo "[rongxing-vpn] SOCKS5 已监听 :${SOCKS_PORT} (microsocks pid=${SOCKS_PID})" cleanup() { kill "$SOCKS_PID" 2>/dev/null || true diff --git a/deployments/openvpn/sockd.conf.template b/deployments/openvpn/sockd.conf.template deleted file mode 100644 index 3539b3b..0000000 --- a/deployments/openvpn/sockd.conf.template +++ /dev/null @@ -1,14 +0,0 @@ -# Dante SOCKS5 — entrypoint 会替换 __SOCKS_PORT__ -logoutput: stderr -internal: 0.0.0.0 port = __SOCKS_PORT__ -external: eth0 -socksmethod: none -clientmethod: none - -client pass { - from: 0.0.0.0/0 to: 0.0.0.0/0 -} - -socks pass { - from: 0.0.0.0/0 to: 0.0.0.0/0 -} diff --git a/deployments/openvpn/third_party/microsocks/.gitignore b/deployments/openvpn/third_party/microsocks/.gitignore new file mode 100644 index 0000000..c9c6b85 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/.gitignore @@ -0,0 +1,3 @@ +*.o +*.out + diff --git a/deployments/openvpn/third_party/microsocks/COPYING b/deployments/openvpn/third_party/microsocks/COPYING new file mode 100644 index 0000000..bfb6c9c --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/COPYING @@ -0,0 +1,24 @@ +microSocks is licensed under the following standard MIT license: + +---------------------------------------------------------------------- +Copyright © 2017 rofl0r. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +---------------------------------------------------------------------- diff --git a/deployments/openvpn/third_party/microsocks/Makefile b/deployments/openvpn/third_party/microsocks/Makefile new file mode 100644 index 0000000..ecb3265 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/Makefile @@ -0,0 +1,35 @@ +# if you want to change/override some variables, do so in a file called +# config.mak, which is gets included automatically if it exists. + +prefix = /usr/local +bindir = $(prefix)/bin + +PROG = microsocks +SRCS = sockssrv.c server.c sblist.c sblist_delete.c +OBJS = $(SRCS:.c=.o) + +LIBS = -lpthread + +CFLAGS += -Wall -std=c99 + +INSTALL = ./install.sh + +-include config.mak + +all: $(PROG) + +install: $(PROG) + $(INSTALL) -D -m 755 $(PROG) $(DESTDIR)$(bindir)/$(PROG) + +clean: + rm -f $(PROG) + rm -f $(OBJS) + +%.o: %.c + $(CC) $(CPPFLAGS) $(CFLAGS) $(INC) $(PIC) -c -o $@ $< + +$(PROG): $(OBJS) + $(CC) $(LDFLAGS) $(OBJS) $(LIBS) -o $@ + +.PHONY: all clean install + diff --git a/deployments/openvpn/third_party/microsocks/README.md b/deployments/openvpn/third_party/microsocks/README.md new file mode 100644 index 0000000..cc5b9a3 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/README.md @@ -0,0 +1,65 @@ +MicroSocks - multithreaded, small, efficient SOCKS5 server. +=========================================================== + +a SOCKS5 service that you can run on your remote boxes to tunnel connections +through them, if for some reason SSH doesn't cut it for you. + +It's very lightweight, and very light on resources too: + +for every client, a thread with a stack size of 8KB is spawned. +the main process basically doesn't consume any resources at all. + +the only limits are the amount of file descriptors and the RAM. + +It's also designed to be robust: it handles resource exhaustion +gracefully by simply denying new connections, instead of calling abort() +as most other programs do these days. + +another plus is ease-of-use: no config file necessary, everything can be +done from the command line and doesn't even need any parameters for quick +setup. + +History +------- + +This is the successor of "rocksocks5", and it was written with +different goals in mind: + +- prefer usage of standard libc functions over homegrown ones +- no artificial limits +- do not aim for minimal binary size, but for minimal source code size, + and maximal readability, reusability, and extensibility. + +as a result of that, ipv4, dns, and ipv6 is supported out of the box +and can use the same code, while rocksocks5 has several compile time +defines to bring down the size of the resulting binary to extreme values +like 10 KB static linked when only ipv4 support is enabled. + +still, if optimized for size, *this* program when static linked against musl +libc is not even 50 KB. that's easily usable even on the cheapest routers. + +command line options +-------------------- + + microsocks -1 -i listenip -p port -u user -P password -b bindaddr + +all arguments are optional. +by default listenip is 0.0.0.0 and port 1080. + +option -1 activates auth_once mode: once a specific ip address +authed successfully with user/pass, it is added to a whitelist +and may use the proxy without auth. +this is handy for programs like firefox that don't support +user/pass auth. for it to work you'd basically make one connection +with another program that supports it, and then you can use firefox too. +for example, authenticate once using curl: + + curl --socks5 user:password@listenip:port anyurl + + +Supported SOCKS5 Features +------------------------- +- authentication: none, password, one-time +- IPv4, IPv6, DNS +- TCP (no UDP at this time) + diff --git a/deployments/openvpn/third_party/microsocks/create-dist.sh b/deployments/openvpn/third_party/microsocks/create-dist.sh new file mode 100644 index 0000000..279774d --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/create-dist.sh @@ -0,0 +1,28 @@ +#!/bin/sh +if [ -z "$VER" ] ; then + echo set VER! + exit +fi +me=`pwd` + +proj=microsocks +projver=${proj}-${VER} + +tempdir=/tmp/${proj}-0000 +rm -rf "$tempdir" +mkdir -p "$tempdir" + +cd $tempdir +GITDIR=https://github.com/rofl0r/$proj +GITDIR=$me +git clone "$GITDIR" $projver + +rm -rf $projver/.git +rm -rf $projver/docs +rm -f $projver/.gitignore +rm -f $projver/create-dist.sh + +tar cf $proj.tar $projver/ +xz -z -9 -e $proj.tar +mv $proj.tar.xz $me/$projver.tar.xz +rm -rf "$tempdir" diff --git a/deployments/openvpn/third_party/microsocks/install.sh b/deployments/openvpn/third_party/microsocks/install.sh new file mode 100644 index 0000000..0410883 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/install.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# +# Written by Rich Felker, originally as part of musl libc. +# Multi-licensed under MIT, 0BSD, and CC0. +# +# This is an actually-safe install command which installs the new +# file atomically in the new location, rather than overwriting +# existing files. +# + +usage() { +printf "usage: %s [-D] [-l] [-m mode] src dest\n" "$0" 1>&2 +exit 1 +} + +mkdirp= +symlink= +mode=755 + +while getopts Dlm: name ; do +case "$name" in +D) mkdirp=yes ;; +l) symlink=yes ;; +m) mode=$OPTARG ;; +?) usage ;; +esac +done +shift $(($OPTIND - 1)) + +test "$#" -eq 2 || usage +src=$1 +dst=$2 +tmp="$dst.tmp.$$" + +case "$dst" in +*/) printf "%s: %s ends in /\n", "$0" "$dst" 1>&2 ; exit 1 ;; +esac + +set -C +set -e + +if test "$mkdirp" ; then +umask 022 +case "$2" in +*/*) mkdir -p "${dst%/*}" ;; +esac +fi + +trap 'rm -f "$tmp"' EXIT INT QUIT TERM HUP + +umask 077 + +if test "$symlink" ; then +ln -s "$1" "$tmp" +else +cat < "$1" > "$tmp" +chmod "$mode" "$tmp" +fi + +mv -f "$tmp" "$2" +test -d "$2" && { +rm -f "$2/$tmp" +printf "%s: %s is a directory\n" "$0" "$dst" 1>&2 +exit 1 +} + +exit 0 diff --git a/deployments/openvpn/third_party/microsocks/sblist.c b/deployments/openvpn/third_party/microsocks/sblist.c new file mode 100644 index 0000000..96bbebc --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/sblist.c @@ -0,0 +1,73 @@ +#undef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L +#include "sblist.h" +#include +#include +#include +#define MY_PAGE_SIZE 4096 + +sblist* sblist_new(size_t itemsize, size_t blockitems) { + sblist* ret = (sblist*) malloc(sizeof(sblist)); + sblist_init(ret, itemsize, blockitems); + return ret; +} + +static void sblist_clear(sblist* l) { + l->items = NULL; + l->capa = 0; + l->count = 0; +} + +void sblist_init(sblist* l, size_t itemsize, size_t blockitems) { + if(l) { + l->blockitems = blockitems ? blockitems : MY_PAGE_SIZE / itemsize; + l->itemsize = itemsize; + sblist_clear(l); + } +} + +void sblist_free_items(sblist* l) { + if(l) { + if(l->items) free(l->items); + sblist_clear(l); + } +} + +void sblist_free(sblist* l) { + if(l) { + sblist_free_items(l); + free(l); + } +} + +char* sblist_item_from_index(sblist* l, size_t idx) { + return l->items + (idx * l->itemsize); +} + +void* sblist_get(sblist* l, size_t item) { + if(item < l->count) return (void*) sblist_item_from_index(l, item); + return NULL; +} + +int sblist_set(sblist* l, void* item, size_t pos) { + if(pos >= l->count) return 0; + memcpy(sblist_item_from_index(l, pos), item, l->itemsize); + return 1; +} + +int sblist_grow_if_needed(sblist* l) { + char* temp; + if(l->count == l->capa) { + temp = realloc(l->items, (l->capa + l->blockitems) * l->itemsize); + if(!temp) return 0; + l->capa += l->blockitems; + l->items = temp; + } + return 1; +} + +int sblist_add(sblist* l, void* item) { + if(!sblist_grow_if_needed(l)) return 0; + l->count++; + return sblist_set(l, item, l->count - 1); +} diff --git a/deployments/openvpn/third_party/microsocks/sblist.h b/deployments/openvpn/third_party/microsocks/sblist.h new file mode 100644 index 0000000..f0d1846 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/sblist.h @@ -0,0 +1,92 @@ +#ifndef SBLIST_H +#define SBLIST_H + +/* this file is part of libulz, as of commit 8ab361a27743aaf025323ee43b8b8876dc054fdd + modified for direct inclusion in microsocks. */ + + +#ifdef __cplusplus +extern "C" { +#endif + +#include +/* + * simple buffer list. + * + * this thing here is basically a generic dynamic array + * will realloc after every blockitems inserts + * can store items of any size. + * + * so think of it as a by-value list, as opposed to a typical by-ref list. + * you typically use it by having some struct on the stack, and pass a pointer + * to sblist_add, which will copy the contents into its internal memory. + * + */ + +typedef struct { + size_t itemsize; + size_t blockitems; + size_t count; + size_t capa; + char* items; +} sblist; + +#define sblist_getsize(X) ((X)->count) +#define sblist_get_count(X) ((X)->count) +#define sblist_empty(X) ((X)->count == 0) + +// for dynamic style +sblist* sblist_new(size_t itemsize, size_t blockitems); +void sblist_free(sblist* l); + +//for static style +void sblist_init(sblist* l, size_t itemsize, size_t blockitems); +void sblist_free_items(sblist* l); + +// accessors +void* sblist_get(sblist* l, size_t item); +// returns 1 on success, 0 on OOM +int sblist_add(sblist* l, void* item); +int sblist_set(sblist* l, void* item, size_t pos); +void sblist_delete(sblist* l, size_t item); +char* sblist_item_from_index(sblist* l, size_t idx); +int sblist_grow_if_needed(sblist* l); +int sblist_insert(sblist* l, void* item, size_t pos); +/* same as sblist_add, but returns list index of new item, or -1 */ +size_t sblist_addi(sblist* l, void* item); +void sblist_sort(sblist *l, int (*compar)(const void *, const void *)); +/* insert element into presorted list, returns listindex of new entry or -1*/ +size_t sblist_insert_sorted(sblist* l, void* o, int (*compar)(const void *, const void *)); + +#ifndef __COUNTER__ +#define __COUNTER__ __LINE__ +#endif + +#define __sblist_concat_impl( x, y ) x##y +#define __sblist_macro_concat( x, y ) __sblist_concat_impl( x, y ) +#define __sblist_iterator_name __sblist_macro_concat(sblist_iterator, __COUNTER__) + +// use with custom iterator variable +#define sblist_iter_counter(LIST, ITER, PTR) \ + for(size_t ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < sblist_getsize(LIST); ITER++) + +// use with custom iterator variable, which is predeclared +#define sblist_iter_counter2(LIST, ITER, PTR) \ + for(ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < sblist_getsize(LIST); ITER++) + +// use with custom iterator variable, which is predeclared and signed +// useful for a loop which can delete items from the list, and then decrease the iterator var. +#define sblist_iter_counter2s(LIST, ITER, PTR) \ + for(ITER = 0; (PTR = sblist_get(LIST, ITER)), ITER < (ssize_t) sblist_getsize(LIST); ITER++) + + +// uses "magic" iterator variable +#define sblist_iter(LIST, PTR) sblist_iter_counter(LIST, __sblist_iterator_name, PTR) + +#ifdef __cplusplus +} +#endif + +#pragma RcB2 DEP "sblist.c" "sblist_delete.c" + +#endif diff --git a/deployments/openvpn/third_party/microsocks/sblist_delete.c b/deployments/openvpn/third_party/microsocks/sblist_delete.c new file mode 100644 index 0000000..a18209a --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/sblist_delete.c @@ -0,0 +1,9 @@ +#include "sblist.h" +#include + +void sblist_delete(sblist* l, size_t item) { + if (l->count && item < l->count) { + memmove(sblist_item_from_index(l, item), sblist_item_from_index(l, item + 1), (sblist_getsize(l) - (item + 1)) * l->itemsize); + l->count--; + } +} diff --git a/deployments/openvpn/third_party/microsocks/server.c b/deployments/openvpn/third_party/microsocks/server.c new file mode 100644 index 0000000..9f0ab9a --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/server.c @@ -0,0 +1,64 @@ +#include "server.h" +#include +#include +#include + +int resolve(const char *host, unsigned short port, struct addrinfo** addr) { + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_STREAM, + .ai_flags = AI_PASSIVE, + }; + char port_buf[8]; + snprintf(port_buf, sizeof port_buf, "%u", port); + return getaddrinfo(host, port_buf, &hints, addr); +} + +int resolve_sa(const char *host, unsigned short port, union sockaddr_union *res) { + struct addrinfo *ainfo = 0; + int ret; + SOCKADDR_UNION_AF(res) = AF_UNSPEC; + if((ret = resolve(host, port, &ainfo))) return ret; + memcpy(res, ainfo->ai_addr, ainfo->ai_addrlen); + freeaddrinfo(ainfo); + return 0; +} + +int bindtoip(int fd, union sockaddr_union *bindaddr) { + socklen_t sz = SOCKADDR_UNION_LENGTH(bindaddr); + if(sz) + return bind(fd, (struct sockaddr*) bindaddr, sz); + return 0; +} + +int server_waitclient(struct server *server, struct client* client) { + socklen_t clen = sizeof client->addr; + return ((client->fd = accept(server->fd, (void*)&client->addr, &clen)) == -1)*-1; +} + +int server_setup(struct server *server, const char* listenip, unsigned short port) { + struct addrinfo *ainfo = 0; + if(resolve(listenip, port, &ainfo)) return -1; + struct addrinfo* p; + int listenfd = -1; + for(p = ainfo; p; p = p->ai_next) { + if((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) + continue; + int yes = 1; + setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)); + if(bind(listenfd, p->ai_addr, p->ai_addrlen) < 0) { + close(listenfd); + listenfd = -1; + continue; + } + break; + } + freeaddrinfo(ainfo); + if(listenfd < 0) return -2; + if(listen(listenfd, SOMAXCONN) < 0) { + close(listenfd); + return -3; + } + server->fd = listenfd; + return 0; +} diff --git a/deployments/openvpn/third_party/microsocks/server.h b/deployments/openvpn/third_party/microsocks/server.h new file mode 100644 index 0000000..5acf664 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/server.h @@ -0,0 +1,49 @@ +#ifndef SERVER_H +#define SERVER_H + +#undef _POSIX_C_SOURCE +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include + +#pragma RcB2 DEP "server.c" + +union sockaddr_union { + struct sockaddr_in v4; + struct sockaddr_in6 v6; +}; + +#define SOCKADDR_UNION_AF(PTR) (PTR)->v4.sin_family + +#define SOCKADDR_UNION_LENGTH(PTR) ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? sizeof((PTR)->v4) : ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? sizeof((PTR)->v6) : 0 ) ) + +#define SOCKADDR_UNION_ADDRESS(PTR) ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? (void*) &(PTR)->v4.sin_addr : ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? (void*) &(PTR)->v6.sin6_addr : (void*) 0 ) ) + +#define SOCKADDR_UNION_PORT(PTR) ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET ) ? (PTR)->v4.sin_port : ( \ + ( SOCKADDR_UNION_AF(PTR) == AF_INET6 ) ? (PTR)->v6.sin6_port : 0 ) ) + +struct client { + union sockaddr_union addr; + int fd; +}; + +struct server { + int fd; +}; + +int resolve(const char *host, unsigned short port, struct addrinfo** addr); +int resolve_sa(const char *host, unsigned short port, union sockaddr_union *res); +int bindtoip(int fd, union sockaddr_union *bindaddr); + +int server_waitclient(struct server *server, struct client* client); +int server_setup(struct server *server, const char* listenip, unsigned short port); + +#endif + diff --git a/deployments/openvpn/third_party/microsocks/sockssrv.c b/deployments/openvpn/third_party/microsocks/sockssrv.c new file mode 100644 index 0000000..d2b8bb5 --- /dev/null +++ b/deployments/openvpn/third_party/microsocks/sockssrv.c @@ -0,0 +1,482 @@ +/* + MicroSocks - multithreaded, small, efficient SOCKS5 server. + + Copyright (C) 2017 rofl0r. + + This is the successor of "rocksocks5", and it was written with + different goals in mind: + + - prefer usage of standard libc functions over homegrown ones + - no artificial limits + - do not aim for minimal binary size, but for minimal source code size, + and maximal readability, reusability, and extensibility. + + as a result of that, ipv4, dns, and ipv6 is supported out of the box + and can use the same code, while rocksocks5 has several compile time + defines to bring down the size of the resulting binary to extreme values + like 10 KB static linked when only ipv4 support is enabled. + + still, if optimized for size, *this* program when static linked against musl + libc is not even 50 KB. that's easily usable even on the cheapest routers. + +*/ + +#define _GNU_SOURCE +#include +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "server.h" +#include "sblist.h" + +/* timeout in microseconds on resource exhaustion to prevent excessive + cpu usage. */ +#ifndef FAILURE_TIMEOUT +#define FAILURE_TIMEOUT 64 +#endif + +#ifndef MAX +#define MAX(x, y) ((x) > (y) ? (x) : (y)) +#endif + +#ifdef PTHREAD_STACK_MIN +#define THREAD_STACK_SIZE MAX(8*1024, PTHREAD_STACK_MIN) +#else +#define THREAD_STACK_SIZE 64*1024 +#endif + +#if defined(__APPLE__) +#undef THREAD_STACK_SIZE +#define THREAD_STACK_SIZE 64*1024 +#elif defined(__GLIBC__) || defined(__FreeBSD__) +#undef THREAD_STACK_SIZE +#define THREAD_STACK_SIZE 32*1024 +#endif + +static const char* auth_user; +static const char* auth_pass; +static sblist* auth_ips; +static pthread_rwlock_t auth_ips_lock = PTHREAD_RWLOCK_INITIALIZER; +static const struct server* server; +static union sockaddr_union bind_addr = {.v4.sin_family = AF_UNSPEC}; + +enum socksstate { + SS_1_CONNECTED, + SS_2_NEED_AUTH, /* skipped if NO_AUTH method supported */ + SS_3_AUTHED, +}; + +enum authmethod { + AM_NO_AUTH = 0, + AM_GSSAPI = 1, + AM_USERNAME = 2, + AM_INVALID = 0xFF +}; + +enum errorcode { + EC_SUCCESS = 0, + EC_GENERAL_FAILURE = 1, + EC_NOT_ALLOWED = 2, + EC_NET_UNREACHABLE = 3, + EC_HOST_UNREACHABLE = 4, + EC_CONN_REFUSED = 5, + EC_TTL_EXPIRED = 6, + EC_COMMAND_NOT_SUPPORTED = 7, + EC_ADDRESSTYPE_NOT_SUPPORTED = 8, +}; + +struct thread { + pthread_t pt; + struct client client; + enum socksstate state; + volatile int done; +}; + +#ifndef CONFIG_LOG +#define CONFIG_LOG 1 +#endif +#if CONFIG_LOG +/* we log to stderr because it's not using line buffering, i.e. malloc which would need + locking when called from different threads. for the same reason we use dprintf, + which writes directly to an fd. */ +#define dolog(...) dprintf(2, __VA_ARGS__) +#else +static void dolog(const char* fmt, ...) { } +#endif + +static struct addrinfo* addr_choose(struct addrinfo* list, union sockaddr_union* bind_addr) { + int af = SOCKADDR_UNION_AF(bind_addr); + if(af == AF_UNSPEC) return list; + struct addrinfo* p; + for(p=list; p; p=p->ai_next) + if(p->ai_family == af) return p; + return list; +} + +static int connect_socks_target(unsigned char *buf, size_t n, struct client *client) { + if(n < 5) return -EC_GENERAL_FAILURE; + if(buf[0] != 5) return -EC_GENERAL_FAILURE; + if(buf[1] != 1) return -EC_COMMAND_NOT_SUPPORTED; /* we support only CONNECT method */ + if(buf[2] != 0) return -EC_GENERAL_FAILURE; /* malformed packet */ + + int af = AF_INET; + size_t minlen = 4 + 4 + 2, l; + char namebuf[256]; + struct addrinfo* remote; + + switch(buf[3]) { + case 4: /* ipv6 */ + af = AF_INET6; + minlen = 4 + 2 + 16; + /* fall through */ + case 1: /* ipv4 */ + if(n < minlen) return -EC_GENERAL_FAILURE; + if(namebuf != inet_ntop(af, buf+4, namebuf, sizeof namebuf)) + return -EC_GENERAL_FAILURE; /* malformed or too long addr */ + break; + case 3: /* dns name */ + l = buf[4]; + minlen = 4 + 2 + l + 1; + if(n < 4 + 2 + l + 1) return -EC_GENERAL_FAILURE; + memcpy(namebuf, buf+4+1, l); + namebuf[l] = 0; + break; + default: + return -EC_ADDRESSTYPE_NOT_SUPPORTED; + } + unsigned short port; + port = (buf[minlen-2] << 8) | buf[minlen-1]; + /* there's no suitable errorcode in rfc1928 for dns lookup failure */ + if(resolve(namebuf, port, &remote)) return -EC_GENERAL_FAILURE; + struct addrinfo* raddr = addr_choose(remote, &bind_addr); + int fd = socket(raddr->ai_family, SOCK_STREAM, 0); + if(fd == -1) { + eval_errno: + if(fd != -1) close(fd); + freeaddrinfo(remote); + switch(errno) { + case ETIMEDOUT: + return -EC_TTL_EXPIRED; + case EPROTOTYPE: + case EPROTONOSUPPORT: + case EAFNOSUPPORT: + return -EC_ADDRESSTYPE_NOT_SUPPORTED; + case ECONNREFUSED: + return -EC_CONN_REFUSED; + case ENETDOWN: + case ENETUNREACH: + return -EC_NET_UNREACHABLE; + case EHOSTUNREACH: + return -EC_HOST_UNREACHABLE; + case EBADF: + default: + perror("socket/connect"); + return -EC_GENERAL_FAILURE; + } + } + if(SOCKADDR_UNION_AF(&bind_addr) == raddr->ai_family && + bindtoip(fd, &bind_addr) == -1) + goto eval_errno; + if(connect(fd, raddr->ai_addr, raddr->ai_addrlen) == -1) + goto eval_errno; + + freeaddrinfo(remote); + if(CONFIG_LOG) { + char clientname[256]; + af = SOCKADDR_UNION_AF(&client->addr); + void *ipdata = SOCKADDR_UNION_ADDRESS(&client->addr); + inet_ntop(af, ipdata, clientname, sizeof clientname); + dolog("client[%d] %s: connected to %s:%d\n", client->fd, clientname, namebuf, port); + } + return fd; +} + +static int is_authed(union sockaddr_union *client, union sockaddr_union *authedip) { + int af = SOCKADDR_UNION_AF(authedip); + if(af == SOCKADDR_UNION_AF(client)) { + size_t cmpbytes = af == AF_INET ? 4 : 16; + void *cmp1 = SOCKADDR_UNION_ADDRESS(client); + void *cmp2 = SOCKADDR_UNION_ADDRESS(authedip); + if(!memcmp(cmp1, cmp2, cmpbytes)) return 1; + } + return 0; +} + +static int is_in_authed_list(union sockaddr_union *caddr) { + size_t i; + for(i=0;i= n ) return AM_INVALID; + int n_methods = buf[idx]; + idx++; + while(idx < n && n_methods > 0) { + if(buf[idx] == AM_NO_AUTH) { + if(!auth_user) return AM_NO_AUTH; + else if(auth_ips) { + int authed = 0; + if(pthread_rwlock_rdlock(&auth_ips_lock) == 0) { + authed = is_in_authed_list(&client->addr); + pthread_rwlock_unlock(&auth_ips_lock); + } + if(authed) return AM_NO_AUTH; + } + } else if(buf[idx] == AM_USERNAME) { + if(auth_user) return AM_USERNAME; + } + idx++; + n_methods--; + } + return AM_INVALID; +} + +static void send_auth_response(int fd, int version, enum authmethod meth) { + unsigned char buf[2]; + buf[0] = version; + buf[1] = meth; + write(fd, buf, 2); +} + +static void send_error(int fd, enum errorcode ec) { + /* position 4 contains ATYP, the address type, which is the same as used in the connect + request. we're lazy and return always IPV4 address type in errors. */ + char buf[10] = { 5, ec, 0, 1 /*AT_IPV4*/, 0,0,0,0, 0,0 }; + write(fd, buf, 10); +} + +static void copyloop(int fd1, int fd2) { + struct pollfd fds[2] = { + [0] = {.fd = fd1, .events = POLLIN}, + [1] = {.fd = fd2, .events = POLLIN}, + }; + + while(1) { + /* inactive connections are reaped after 15 min to free resources. + usually programs send keep-alive packets so this should only happen + when a connection is really unused. */ + switch(poll(fds, 2, 60*15*1000)) { + case 0: + return; + case -1: + if(errno == EINTR || errno == EAGAIN) continue; + else perror("poll"); + return; + } + int infd = (fds[0].revents & POLLIN) ? fd1 : fd2; + int outfd = infd == fd2 ? fd1 : fd2; + char buf[1024]; + ssize_t sent = 0, n = read(infd, buf, sizeof buf); + if(n <= 0) return; + while(sent < n) { + ssize_t m = write(outfd, buf+sent, n-sent); + if(m < 0) return; + sent += m; + } + } +} + +static enum errorcode check_credentials(unsigned char* buf, size_t n) { + if(n < 5) return EC_GENERAL_FAILURE; + if(buf[0] != 1) return EC_GENERAL_FAILURE; + unsigned ulen, plen; + ulen=buf[1]; + if(n < 2 + ulen + 2) return EC_GENERAL_FAILURE; + plen=buf[2+ulen]; + if(n < 2 + ulen + 1 + plen) return EC_GENERAL_FAILURE; + char user[256], pass[256]; + memcpy(user, buf+2, ulen); + memcpy(pass, buf+2+ulen+1, plen); + user[ulen] = 0; + pass[plen] = 0; + if(!strcmp(user, auth_user) && !strcmp(pass, auth_pass)) return EC_SUCCESS; + return EC_NOT_ALLOWED; +} + +static void* clientthread(void *data) { + struct thread *t = data; + t->state = SS_1_CONNECTED; + unsigned char buf[1024]; + ssize_t n; + int ret; + int remotefd = -1; + enum authmethod am; + while((n = recv(t->client.fd, buf, sizeof buf, 0)) > 0) { + switch(t->state) { + case SS_1_CONNECTED: + am = check_auth_method(buf, n, &t->client); + if(am == AM_NO_AUTH) t->state = SS_3_AUTHED; + else if (am == AM_USERNAME) t->state = SS_2_NEED_AUTH; + send_auth_response(t->client.fd, 5, am); + if(am == AM_INVALID) goto breakloop; + break; + case SS_2_NEED_AUTH: + ret = check_credentials(buf, n); + send_auth_response(t->client.fd, 1, ret); + if(ret != EC_SUCCESS) + goto breakloop; + t->state = SS_3_AUTHED; + if(auth_ips && !pthread_rwlock_wrlock(&auth_ips_lock)) { + if(!is_in_authed_list(&t->client.addr)) + add_auth_ip(&t->client.addr); + pthread_rwlock_unlock(&auth_ips_lock); + } + break; + case SS_3_AUTHED: + ret = connect_socks_target(buf, n, &t->client); + if(ret < 0) { + send_error(t->client.fd, ret*-1); + goto breakloop; + } + remotefd = ret; + send_error(t->client.fd, EC_SUCCESS); + copyloop(t->client.fd, remotefd); + goto breakloop; + + } + } +breakloop: + + if(remotefd != -1) + close(remotefd); + + close(t->client.fd); + t->done = 1; + + return 0; +} + +static void collect(sblist *threads) { + size_t i; + for(i=0;idone) { + pthread_join(thread->pt, 0); + sblist_delete(threads, i); + free(thread); + } else + i++; + } +} + +static int usage(void) { + dprintf(2, + "MicroSocks SOCKS5 Server\n" + "------------------------\n" + "usage: microsocks -1 -i listenip -p port -u user -P password -b bindaddr\n" + "all arguments are optional.\n" + "by default listenip is 0.0.0.0 and port 1080.\n\n" + "option -b specifies which ip outgoing connections are bound to\n" + "option -1 activates auth_once mode: once a specific ip address\n" + "authed successfully with user/pass, it is added to a whitelist\n" + "and may use the proxy without auth.\n" + "this is handy for programs like firefox that don't support\n" + "user/pass auth. for it to work you'd basically make one connection\n" + "with another program that supports it, and then you can use firefox too.\n" + ); + return 1; +} + +/* prevent username and password from showing up in top. */ +static void zero_arg(char *s) { + size_t i, l = strlen(s); + for(i=0;idone = 0; + if(server_waitclient(&s, &c)) { + dolog("failed to accept connection\n"); + free(curr); + usleep(FAILURE_TIMEOUT); + continue; + } + curr->client = c; + if(!sblist_add(threads, &curr)) { + close(curr->client.fd); + free(curr); + oom: + dolog("rejecting connection due to OOM\n"); + usleep(FAILURE_TIMEOUT); /* prevent 100% CPU usage in OOM situation */ + continue; + } + pthread_attr_t *a = 0, attr; + if(pthread_attr_init(&attr) == 0) { + a = &attr; + pthread_attr_setstacksize(a, THREAD_STACK_SIZE); + } + if(pthread_create(&curr->pt, a, clientthread, curr) != 0) + dolog("pthread_create failed. OOM?\n"); + if(a) pthread_attr_destroy(&attr); + } +} diff --git a/internal/infrastructure/external/rongxing/curl_helper.go b/internal/infrastructure/external/rongxing/curl_helper.go index 49c3832..172a043 100644 --- a/internal/infrastructure/external/rongxing/curl_helper.go +++ b/internal/infrastructure/external/rongxing/curl_helper.go @@ -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) diff --git a/internal/infrastructure/external/rongxing/rongxing_service.go b/internal/infrastructure/external/rongxing/rongxing_service.go index 229681d..8fe0d82 100644 --- a/internal/infrastructure/external/rongxing/rongxing_service.go +++ b/internal/infrastructure/external/rongxing/rongxing_service.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "net/url" + "os" "strings" "sync" "time" @@ -163,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 { @@ -298,7 +299,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 { @@ -356,7 +357,7 @@ func (s *RongxingService) login(ctx context.Context, transactionID string) (stri func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) { 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() @@ -367,6 +368,48 @@ 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 "网络超时" + } + return "其他网络错误(详见 cause)" +} + func (s *RongxingService) validateConfig() error { if s.config.BaseURL == "" { return errors.New("戎行 url 未配置") @@ -429,12 +472,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) +} diff --git a/internal/infrastructure/external/rongxing/transport_error_test.go b/internal/infrastructure/external/rongxing/transport_error_test.go new file mode 100644 index 0000000..985c44b --- /dev/null +++ b/internal/infrastructure/external/rongxing/transport_error_test.go @@ -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) + } +} From 61f95aeedb4fe238fd851e005c5d29c47f7228b3 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Fri, 24 Jul 2026 10:19:32 +0800 Subject: [PATCH 05/13] fadd --- .../external/rongxing/rongxing_service.go | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/internal/infrastructure/external/rongxing/rongxing_service.go b/internal/infrastructure/external/rongxing/rongxing_service.go index 8fe0d82..5948135 100644 --- a/internal/infrastructure/external/rongxing/rongxing_service.go +++ b/internal/infrastructure/external/rongxing/rongxing_service.go @@ -45,8 +45,8 @@ type RongxingService struct { logger *external_logger.ExternalServiceLogger client *http.Client - tokenMu sync.RWMutex - cachedToken string + // loginMu 避免并发登录打爆对方;不缓存 token,每次业务调用重新登录。 + loginMu sync.Mutex } // apiResponse 戎行统一响应。code 可能是数字或字符串;扣费以 consumeFlag 为准。 @@ -126,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, "/") @@ -185,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 } @@ -208,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 不扣费 @@ -226,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) { @@ -407,7 +399,8 @@ func classifyTransportError(err error) string { if errors.As(err, &netErr) && netErr.Timeout() { return "网络超时" } - return "其他网络错误(详见 cause)" + // 完整原始错误在同一条日志的 error/cause 字段中,不在别的文件 + return "其他网络错误(见本条日志 error 全文)" } func (s *RongxingService) validateConfig() error { From 40f6bc5bc189200d357cda90c78bd10eb30258cd Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Fri, 24 Jul 2026 10:38:57 +0800 Subject: [PATCH 06/13] f add --- internal/domains/api/dto/api_request_dto.go | 4 +++ .../api/services/api_request_service.go | 1 + .../api/services/form_config_service.go | 1 + .../processors/flxg/flxgld77_processor.go | 33 +++++++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 internal/domains/api/services/processors/flxg/flxgld77_processor.go diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 6f74699..66194b7 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -1350,6 +1350,10 @@ type FLXGC4CTReq struct { AuthPDFBase64 string `json:"auth_pdf_base64" validate:"required,validAuthPDFBase64"` } +type FLXLD77Req 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"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 01afc40..0795bd2 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -176,6 +176,7 @@ func registerAllProcessors(combService *comb.CombService) { "FLXGJI17": flxg.ProcessFLXGJI17Request, //董监高司法综合信息核验 "FLXGMMG7": flxg.ProcessFLXGMMG7Request, //个人诉讼定制版 "FLXGC4CT": flxg.ProcessFLXGC4CTRequest, //个人涉诉案件查询汇博 + "FLXLD77": flxg.ProcessFLXLD77Request, //劳动仲裁查询天远 // JRZQ系列处理器 "JRZQOICN": jrzq.ProcessJRZQOICNRequest, // 银行卡四要素 "JRZQMDQ1": jrzq.ProcessJRZQMDQ1Request, // 银行卡OCR数卖 diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 7d9fb3e..bf59d64 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -325,6 +325,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "JRZQS74D": &dto.JRZQT2C1Req{}, //申请借贷 "JRZQ2F4W": &dto.JRZQT2C1Req{}, //支付行为 "JRZQ2PV2": &dto.JRZQ2PV2Req{}, //租赁通用版v2 + "FLXLD77": &dto.FLXLD77Req{}, //劳动仲裁查询天远 } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/flxg/flxgld77_processor.go b/internal/domains/api/services/processors/flxg/flxgld77_processor.go new file mode 100644 index 0000000..27068ef --- /dev/null +++ b/internal/domains/api/services/processors/flxg/flxgld77_processor.go @@ -0,0 +1,33 @@ +package flxg + +import ( + "context" + "encoding/json" + "errors" + + "hyapi-server/internal/domains/api/dto" + "hyapi-server/internal/domains/api/services/processors" +) + +// ProcessFLXLD77Request FLXLD77 API处理方法 - 劳动仲裁查询天远 +func ProcessFLXLD77Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { + var paramsDto dto.FLXLD77Req + 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 +} From 991f7c16f6d38adb91712d43b2fefc92cfccf766 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Fri, 24 Jul 2026 10:44:33 +0800 Subject: [PATCH 07/13] fad --- internal/domains/api/dto/api_request_dto.go | 2 +- internal/domains/api/services/api_request_service.go | 2 +- internal/domains/api/services/form_config_service.go | 2 +- .../api/services/processors/flxg/flxgld77_processor.go | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 66194b7..5fb18ee 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -1350,7 +1350,7 @@ type FLXGC4CTReq struct { AuthPDFBase64 string `json:"auth_pdf_base64" validate:"required,validAuthPDFBase64"` } -type FLXLD77Req struct { +type FLXLLD77Req struct { IDCard string `json:"id_card" validate:"required,validIDCard"` Name string `json:"name" validate:"required,min=1,validName"` } diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 0795bd2..391d2ba 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -176,7 +176,7 @@ func registerAllProcessors(combService *comb.CombService) { "FLXGJI17": flxg.ProcessFLXGJI17Request, //董监高司法综合信息核验 "FLXGMMG7": flxg.ProcessFLXGMMG7Request, //个人诉讼定制版 "FLXGC4CT": flxg.ProcessFLXGC4CTRequest, //个人涉诉案件查询汇博 - "FLXLD77": flxg.ProcessFLXLD77Request, //劳动仲裁查询天远 + "FLXLLD77": flxg.ProcessFLXLLD77Request, //劳动仲裁查询天远 // JRZQ系列处理器 "JRZQOICN": jrzq.ProcessJRZQOICNRequest, // 银行卡四要素 "JRZQMDQ1": jrzq.ProcessJRZQMDQ1Request, // 银行卡OCR数卖 diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index bf59d64..341dc6e 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -325,7 +325,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "JRZQS74D": &dto.JRZQT2C1Req{}, //申请借贷 "JRZQ2F4W": &dto.JRZQT2C1Req{}, //支付行为 "JRZQ2PV2": &dto.JRZQ2PV2Req{}, //租赁通用版v2 - "FLXLD77": &dto.FLXLD77Req{}, //劳动仲裁查询天远 + "FLXLLD77": &dto.FLXLLD77Req{}, //劳动仲裁查询天远 } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/flxg/flxgld77_processor.go b/internal/domains/api/services/processors/flxg/flxgld77_processor.go index 27068ef..068ae34 100644 --- a/internal/domains/api/services/processors/flxg/flxgld77_processor.go +++ b/internal/domains/api/services/processors/flxg/flxgld77_processor.go @@ -9,9 +9,9 @@ import ( "hyapi-server/internal/domains/api/services/processors" ) -// ProcessFLXLD77Request FLXLD77 API处理方法 - 劳动仲裁查询天远 -func ProcessFLXLD77Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) { - var paramsDto dto.FLXLD77Req +// 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) } From edbdc4eafe40cb73400770625f487cff5bf724c1 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Fri, 24 Jul 2026 11:46:53 +0800 Subject: [PATCH 08/13] fadd --- internal/domains/api/dto/api_request_dto.go | 23 ++++++++ .../api/services/api_request_service.go | 5 ++ .../api/services/form_config_service.go | 5 ++ .../processors/qcxg/qcxg2y8x_processor.go | 57 +++++++++++++++++++ .../processors/qcxg/qcxg74yt_processor.go | 57 +++++++++++++++++++ .../processors/qcxg/qcxgv20o_processor.go | 48 ++++++++++++++++ .../processors/qcxg/qcxgvj70_processor.go | 48 ++++++++++++++++ .../processors/qcxg/qcxgvp00_processor.go | 48 ++++++++++++++++ 8 files changed, 291 insertions(+) create mode 100644 internal/domains/api/services/processors/qcxg/qcxg2y8x_processor.go create mode 100644 internal/domains/api/services/processors/qcxg/qcxg74yt_processor.go create mode 100644 internal/domains/api/services/processors/qcxg/qcxgv20o_processor.go create mode 100644 internal/domains/api/services/processors/qcxg/qcxgvj70_processor.go create mode 100644 internal/domains/api/services/processors/qcxg/qcxgvp00_processor.go diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 5fb18ee..4739679 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -1375,3 +1375,26 @@ 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"` +} diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index 391d2ba..e91f2c2 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -229,6 +229,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, //运营商二要素查询 diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 341dc6e..4593f97 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -326,6 +326,11 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "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{}, //交强险有效性 } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/qcxg/qcxg2y8x_processor.go b/internal/domains/api/services/processors/qcxg/qcxg2y8x_processor.go new file mode 100644 index 0000000..1b229b4 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxg2y8x_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxg74yt_processor.go b/internal/domains/api/services/processors/qcxg/qcxg74yt_processor.go new file mode 100644 index 0000000..bc22b83 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxg74yt_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxgv20o_processor.go b/internal/domains/api/services/processors/qcxg/qcxgv20o_processor.go new file mode 100644 index 0000000..06557b3 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxgv20o_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxgvj70_processor.go b/internal/domains/api/services/processors/qcxg/qcxgvj70_processor.go new file mode 100644 index 0000000..b383cd0 --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxgvj70_processor.go @@ -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 +} diff --git a/internal/domains/api/services/processors/qcxg/qcxgvp00_processor.go b/internal/domains/api/services/processors/qcxg/qcxgvp00_processor.go new file mode 100644 index 0000000..657216b --- /dev/null +++ b/internal/domains/api/services/processors/qcxg/qcxgvp00_processor.go @@ -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 +} From 903835ede160993061a069f86376b58e564e80d8 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Fri, 24 Jul 2026 12:05:48 +0800 Subject: [PATCH 09/13] f --- .../processors/jrzq/jrzq0oo1_processor.go | 6 +- .../processors/jrzq/jrzq0oo1_transform.go | 70 ++++++++ .../jrzq/jrzq0oo1_transform_test.go | 156 ++++++++++++++++++ 3 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 internal/domains/api/services/processors/jrzq/jrzq0oo1_transform.go create mode 100644 internal/domains/api/services/processors/jrzq/jrzq0oo1_transform_test.go diff --git a/internal/domains/api/services/processors/jrzq/jrzq0oo1_processor.go b/internal/domains/api/services/processors/jrzq/jrzq0oo1_processor.go index d37c02d..48b0601 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq0oo1_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq0oo1_processor.go @@ -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 } diff --git a/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform.go b/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform.go new file mode 100644 index 0000000..eda1d38 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform.go @@ -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) +} diff --git a/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform_test.go b/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform_test.go new file mode 100644 index 0000000..4e45051 --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq0oo1_transform_test.go @@ -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"]) + } +} From e6a91b0b0fcd1b77a1efb22e9da8f48b7227b2f2 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Fri, 24 Jul 2026 12:33:42 +0800 Subject: [PATCH 10/13] add --- internal/infrastructure/external/jiyi/jiyi_service.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/infrastructure/external/jiyi/jiyi_service.go b/internal/infrastructure/external/jiyi/jiyi_service.go index a93608f..dd5477e 100644 --- a/internal/infrastructure/external/jiyi/jiyi_service.go +++ b/internal/infrastructure/external/jiyi/jiyi_service.go @@ -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版 } From 7c4cbd2bee896c56922d9e355fae01be7c347fbc Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Fri, 24 Jul 2026 13:15:02 +0800 Subject: [PATCH 11/13] add --- internal/domains/api/dto/api_request_dto.go | 7 +++ .../api/services/api_request_service.go | 1 + .../api/services/form_config_service.go | 1 + .../processors/jrzq/jrzq5j5c_processor.go | 50 +++++++++++++++++++ 4 files changed, 59 insertions(+) create mode 100644 internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go diff --git a/internal/domains/api/dto/api_request_dto.go b/internal/domains/api/dto/api_request_dto.go index 4739679..d788fcf 100644 --- a/internal/domains/api/dto/api_request_dto.go +++ b/internal/domains/api/dto/api_request_dto.go @@ -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"` diff --git a/internal/domains/api/services/api_request_service.go b/internal/domains/api/services/api_request_service.go index e91f2c2..c89573e 100644 --- a/internal/domains/api/services/api_request_service.go +++ b/internal/domains/api/services/api_request_service.go @@ -215,6 +215,7 @@ func registerAllProcessors(combService *comb.CombService) { "JRZQS74D": jrzq.ProcessJRZQS74DRequest, //申请借贷 "JRZQ2F4W": jrzq.ProcessJRZQ2F4WRequest, //支付行为 "JRZQ2PV2": jrzq.ProcessJRZQ2PV2Request, //租赁通用版v2 + "JRZQ5J5C": jrzq.ProcessJRZQ5J5CRequest, //无间司南-纯黑版C10 金融黑名单 // QYGL系列处理器 "QYGL7HBN": qygl.ProcessQYGL7HBNRequest, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I) diff --git a/internal/domains/api/services/form_config_service.go b/internal/domains/api/services/form_config_service.go index 4593f97..5d752f2 100644 --- a/internal/domains/api/services/form_config_service.go +++ b/internal/domains/api/services/form_config_service.go @@ -331,6 +331,7 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string "QCXGVJ70": &dto.QCXGVJ70Req{}, //车牌号查vin "QCXG2Y8X": &dto.QCXG2Y8XReq{}, //商业险有效性 "QCXG74YT": &dto.QCXG74YTReq{}, //交强险有效性 + "JRZQ5J5C": &dto.JRZQ5J5CReq{}, //无间司南-纯黑版C10 金融黑名单 } // 优先返回已配置的DTO diff --git a/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go b/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go new file mode 100644 index 0000000..d76e63b --- /dev/null +++ b/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go @@ -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, + "idNo": paramsDto.IDCard, + "mobile": paramsDto.MobileNo, + } + + apiKey := "jy000052" + apiPath := "/api/v1/wujsinan/blacka" + + 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 +} From 65fe68f176f1f355b224c1a1a270ae9f4695d6a9 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Fri, 24 Jul 2026 13:19:45 +0800 Subject: [PATCH 12/13] f --- .../api/services/processors/jrzq/jrzq5j5c_processor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go b/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go index d76e63b..039c750 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go @@ -27,8 +27,8 @@ func ProcessJRZQ5J5CRequest(ctx context.Context, params []byte, deps *processors "mobile": paramsDto.MobileNo, } - apiKey := "jy000052" - apiPath := "/api/v1/wujsinan/blacka" + apiKey := "jy000055" + apiPath := "/api/v1/blacklist/c10/verify" resp, err := deps.JiyiService.CallAPI(ctx, apiKey, apiPath, body, jiyi.TopEncryptionOptions(1)) if err != nil { From 7c1ca7aaddc7b8d4d690983efa904e7c9cba4ee8 Mon Sep 17 00:00:00 2001 From: Mrxs <18278715334@163.com> Date: Fri, 24 Jul 2026 13:30:29 +0800 Subject: [PATCH 13/13] f --- .../domains/api/services/processors/jrzq/jrzq5j5c_processor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go b/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go index 039c750..ed8ba59 100644 --- a/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go +++ b/internal/domains/api/services/processors/jrzq/jrzq5j5c_processor.go @@ -23,7 +23,7 @@ func ProcessJRZQ5J5CRequest(ctx context.Context, params []byte, deps *processors body := map[string]string{ "name": paramsDto.Name, - "idNo": paramsDto.IDCard, + "idCard": paramsDto.IDCard, "mobile": paramsDto.MobileNo, }