fadd
This commit is contained in:
53
config.yaml
53
config.yaml
@@ -298,14 +298,14 @@ esign:
|
|||||||
# 应用名称:海宇数据 | AppId:80005307 | 状态:待上线
|
# 应用名称:海宇数据 | AppId:80005307 | 状态:待上线
|
||||||
# ===========================================
|
# ===========================================
|
||||||
fadada:
|
fadada:
|
||||||
app_id: "80005307"
|
app_id: "00004782"
|
||||||
app_secret: "SM0R81LFZBOAFQY6MV6SRXUJZKWHPLW7"
|
app_secret: "E9GMVOQ9NBSOUDG5VOFM0J1CZPOAP2Y8"
|
||||||
# 待上线/联调用测试环境;正式上线后改为 https://api.fadada.com/api/v5/
|
# 待上线/联调用测试环境;正式上线后改为 https://api.fadada.com/api/v5/
|
||||||
server_url: "https://uat-api.fadada.com/api/v5/"
|
server_url: "https://api.fadada.com/api/v5/"
|
||||||
open_corp_id: "cc1a281c715e4f289b681eb15d512e3f" # 海宇数据在法大大侧企业 ID
|
open_corp_id: "7118e66dfd9349d998c0395c5d0742ad" # 海宇数据在法大大侧企业 ID
|
||||||
# 免验证签场景码:仅 freeSignType=business 时需要;当前按模板免验证签(template),可留空
|
# 免验证签场景码(businessId):乙方 requestVerifyFree=true 时必传,平台校验是否已授权
|
||||||
no_auth_scene_code: ""
|
no_auth_scene_code: "c66587b4a048bba3c1cb032eb7750182"
|
||||||
template_id: "1784281786354133853" # 合作协议模板 ID(填单/签署)
|
template_id: "1784619066713193840" # 合作协议模板 ID(填单/签署;freeSignType=template)
|
||||||
# 签署模板中的参与方标识(actorId),必须与模板「签署角色」名称完全一致
|
# 签署模板中的参与方标识(actorId),必须与模板「签署角色」名称完全一致
|
||||||
party_a_actor_id: "甲方"
|
party_a_actor_id: "甲方"
|
||||||
party_b_actor_id: "乙方"
|
party_b_actor_id: "乙方"
|
||||||
@@ -344,10 +344,10 @@ fadada:
|
|||||||
retry_count: 3
|
retry_count: 3
|
||||||
auth:
|
auth:
|
||||||
# 认证完成后顶层跳回企业入驻页(勿用 callback 页留在 iframe 内)
|
# 认证完成后顶层跳回企业入驻页(勿用 callback 页留在 iframe 内)
|
||||||
redirect_url: "http://localhost:5173/profile/certification"
|
redirect_url: "https://console.haiyudata.com/profile/certification"
|
||||||
sign:
|
sign:
|
||||||
# 甲方签署完成后回跳(iframe 内会顶层跳出到入驻页)
|
# 甲方签署完成后回跳(iframe 内会顶层跳出到入驻页)
|
||||||
redirect_url: "http://localhost:5173/certification/callback/fadada/sign"
|
redirect_url: "https://console.haiyudata.com/certification/callback/fadada/sign"
|
||||||
callback:
|
callback:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
@@ -818,3 +818,38 @@ huibo:
|
|||||||
max_backups: 5
|
max_backups: 5
|
||||||
max_age: 30
|
max_age: 30
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# ✨ 戎行配置
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
rongxing:
|
||||||
|
url: "http://192.168.3.43:7007" # 基础地址,登录=/auth/login,查询=/third/loan/info360
|
||||||
|
account: "haiyukeji"
|
||||||
|
password: "haiyukeji@123" # 明文,请求时自动 Base64
|
||||||
|
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
|
||||||
|
logging:
|
||||||
|
enabled: true
|
||||||
|
log_dir: "logs/external_services"
|
||||||
|
service_name: "rongxing"
|
||||||
|
use_daily: true
|
||||||
|
enable_level_separation: true
|
||||||
|
level_configs:
|
||||||
|
info:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
error:
|
||||||
|
max_size: 200
|
||||||
|
max_backups: 10
|
||||||
|
max_age: 90
|
||||||
|
compress: true
|
||||||
|
warn:
|
||||||
|
max_size: 100
|
||||||
|
max_backups: 5
|
||||||
|
max_age: 30
|
||||||
|
compress: true
|
||||||
|
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ fadada:
|
|||||||
# 待上线/联调用测试环境;正式上线后改为 https://api.fadada.com/api/v5/
|
# 待上线/联调用测试环境;正式上线后改为 https://api.fadada.com/api/v5/
|
||||||
server_url: "https://uat-api.fadada.com/api/v5/"
|
server_url: "https://uat-api.fadada.com/api/v5/"
|
||||||
open_corp_id: "cc1a281c715e4f289b681eb15d512e3f" # 海宇数据在法大大侧企业 ID
|
open_corp_id: "cc1a281c715e4f289b681eb15d512e3f" # 海宇数据在法大大侧企业 ID
|
||||||
# 免验证签场景码:仅 freeSignType=business 时需要;当前按模板免验证签(template),可留空
|
# 免验证签场景码(businessId):乙方 requestVerifyFree=true 时必传
|
||||||
no_auth_scene_code: ""
|
no_auth_scene_code: "d2a3af30bf4fa5740bc65ffff446ab16"
|
||||||
template_id: "1784281786354133853" # 合作协议模板 ID(填单/签署)
|
template_id: "1784281786354133853" # 合作协议模板 ID(填单/签署)
|
||||||
# 签署模板中的参与方标识(actorId),必须与模板「签署角色」名称完全一致
|
# 签署模板中的参与方标识(actorId),必须与模板「签署角色」名称完全一致
|
||||||
party_a_actor_id: "甲方"
|
party_a_actor_id: "甲方"
|
||||||
|
|||||||
@@ -104,9 +104,9 @@ fadada:
|
|||||||
# 待上线/联调用测试环境;正式上线后改为 https://api.fadada.com/api/v5/
|
# 待上线/联调用测试环境;正式上线后改为 https://api.fadada.com/api/v5/
|
||||||
server_url: "https://api.fadada.com/api/v5/"
|
server_url: "https://api.fadada.com/api/v5/"
|
||||||
open_corp_id: "7118e66dfd9349d998c0395c5d0742ad" # 海宇数据在法大大侧企业 ID
|
open_corp_id: "7118e66dfd9349d998c0395c5d0742ad" # 海宇数据在法大大侧企业 ID
|
||||||
# 免验证签场景码:仅 freeSignType=business 时需要;当前按模板免验证签(template),可留空
|
# 免验证签场景码(businessId):乙方 requestVerifyFree=true 时必传,平台校验是否已授权
|
||||||
no_auth_scene_code: ""
|
no_auth_scene_code: "c66587b4a048bba3c1cb032eb7750182"
|
||||||
template_id: "1784619066713193840" # 合作协议模板 ID(填单/签署;免验证签依据此模板)
|
template_id: "1784619066713193840" # 合作协议模板 ID(填单/签署;freeSignType=template)
|
||||||
# 签署模板中的参与方标识(actorId),必须与模板「签署角色」名称完全一致
|
# 签署模板中的参与方标识(actorId),必须与模板「签署角色」名称完全一致
|
||||||
party_a_actor_id: "甲方"
|
party_a_actor_id: "甲方"
|
||||||
party_b_actor_id: "乙方"
|
party_b_actor_id: "乙方"
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type Config struct {
|
|||||||
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
Shujubao ShujubaoConfig `mapstructure:"shujubao"`
|
||||||
Nuoer NuoerConfig `mapstructure:"nuoer"`
|
Nuoer NuoerConfig `mapstructure:"nuoer"`
|
||||||
Huibo HuiboConfig `mapstructure:"huibo"`
|
Huibo HuiboConfig `mapstructure:"huibo"`
|
||||||
|
Rongxing RongxingConfig `mapstructure:"rongxing"`
|
||||||
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
PDFGen PDFGenConfig `mapstructure:"pdfgen"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +396,7 @@ type FadadaConfig struct {
|
|||||||
AppSecret string `mapstructure:"app_secret"` // 应用密钥
|
AppSecret string `mapstructure:"app_secret"` // 应用密钥
|
||||||
ServerURL string `mapstructure:"server_url"` // 服务器URL
|
ServerURL string `mapstructure:"server_url"` // 服务器URL
|
||||||
OpenCorpID string `mapstructure:"open_corp_id"` // 接入方(海宇)在法大大侧企业ID
|
OpenCorpID string `mapstructure:"open_corp_id"` // 接入方(海宇)在法大大侧企业ID
|
||||||
NoAuthSceneCode string `mapstructure:"no_auth_scene_code"` // 免验证签场景码(仅 freeSignType=business 时用;当前为 template 可留空)
|
NoAuthSceneCode string `mapstructure:"no_auth_scene_code"` // 免验证签场景码(businessId;requestVerifyFree 时必传)
|
||||||
TemplateID string `mapstructure:"template_id"` // 模板ID
|
TemplateID string `mapstructure:"template_id"` // 模板ID
|
||||||
PartyAActorID string `mapstructure:"party_a_actor_id"` // 签署模板甲方参与方标识(actorId)
|
PartyAActorID string `mapstructure:"party_a_actor_id"` // 签署模板甲方参与方标识(actorId)
|
||||||
PartyBActorID string `mapstructure:"party_b_actor_id"` // 签署模板乙方参与方标识(actorId)
|
PartyBActorID string `mapstructure:"party_b_actor_id"` // 签署模板乙方参与方标识(actorId)
|
||||||
@@ -809,6 +810,36 @@ type HuiboLevelFileConfig struct {
|
|||||||
Compress bool `mapstructure:"compress"`
|
Compress bool `mapstructure:"compress"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RongxingConfig 戎行(Info360)配置
|
||||||
|
type RongxingConfig struct {
|
||||||
|
URL string `mapstructure:"url"` // 基础地址,如 http://host:7007
|
||||||
|
Account string `mapstructure:"account"` // 登录账号
|
||||||
|
Password string `mapstructure:"password"` // 明文密码(请求时 Base64)
|
||||||
|
AppID string `mapstructure:"app_id"` // 应用 ID
|
||||||
|
PrivateKey string `mapstructure:"private_key"` // RSA 私钥(PEM 或 PKCS#8 Base64)
|
||||||
|
Timeout time.Duration `mapstructure:"timeout"`
|
||||||
|
|
||||||
|
Logging RongxingLoggingConfig `mapstructure:"logging"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RongxingLoggingConfig 戎行日志配置
|
||||||
|
type RongxingLoggingConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
LogDir string `mapstructure:"log_dir"`
|
||||||
|
ServiceName string `mapstructure:"service_name"`
|
||||||
|
UseDaily bool `mapstructure:"use_daily"`
|
||||||
|
EnableLevelSeparation bool `mapstructure:"enable_level_separation"`
|
||||||
|
LevelConfigs map[string]RongxingLevelFileConfig `mapstructure:"level_configs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RongxingLevelFileConfig 戎行日志级别配置
|
||||||
|
type RongxingLevelFileConfig struct {
|
||||||
|
MaxSize int `mapstructure:"max_size"`
|
||||||
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
|
MaxAge int `mapstructure:"max_age"`
|
||||||
|
Compress bool `mapstructure:"compress"`
|
||||||
|
}
|
||||||
|
|
||||||
// PDFGenConfig PDF生成服务配置
|
// PDFGenConfig PDF生成服务配置
|
||||||
type PDFGenConfig struct {
|
type PDFGenConfig struct {
|
||||||
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
|
DevelopmentURL string `mapstructure:"development_url"` // 开发环境服务地址
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import (
|
|||||||
infraesign "hyapi-server/internal/infrastructure/external/esign"
|
infraesign "hyapi-server/internal/infrastructure/external/esign"
|
||||||
infrafadada "hyapi-server/internal/infrastructure/external/fadada"
|
infrafadada "hyapi-server/internal/infrastructure/external/fadada"
|
||||||
"hyapi-server/internal/infrastructure/external/huibo"
|
"hyapi-server/internal/infrastructure/external/huibo"
|
||||||
|
"hyapi-server/internal/infrastructure/external/rongxing"
|
||||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||||
"hyapi-server/internal/infrastructure/external/muzi"
|
"hyapi-server/internal/infrastructure/external/muzi"
|
||||||
"hyapi-server/internal/infrastructure/external/nuoer"
|
"hyapi-server/internal/infrastructure/external/nuoer"
|
||||||
@@ -472,6 +473,10 @@ func NewContainer() *Container {
|
|||||||
func(cfg *config.Config) (*huibo.HuiboService, error) {
|
func(cfg *config.Config) (*huibo.HuiboService, error) {
|
||||||
return huibo.NewHuiboServiceWithConfig(cfg)
|
return huibo.NewHuiboServiceWithConfig(cfg)
|
||||||
},
|
},
|
||||||
|
// RongxingService - 戎行 Info360 服务
|
||||||
|
func(cfg *config.Config) (*rongxing.RongxingService, error) {
|
||||||
|
return rongxing.NewRongxingServiceWithConfig(cfg)
|
||||||
|
},
|
||||||
func(cfg *config.Config) *yushan.YushanService {
|
func(cfg *config.Config) *yushan.YushanService {
|
||||||
return yushan.NewYushanService(
|
return yushan.NewYushanService(
|
||||||
cfg.Yushan.URL,
|
cfg.Yushan.URL,
|
||||||
|
|||||||
@@ -1318,3 +1318,9 @@ type QYGLLUCMReq struct {
|
|||||||
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
EntName string `json:"ent_name" validate:"required,min=1,validEnterpriseName"`
|
||||||
AuthPDFBase64 string `json:"auth_pdf_base64" validate:"required,validAuthPDFBase64"`
|
AuthPDFBase64 string `json:"auth_pdf_base64" validate:"required,validAuthPDFBase64"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type JRZQ0OO1Req 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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||||
"hyapi-server/internal/infrastructure/external/muzi"
|
"hyapi-server/internal/infrastructure/external/muzi"
|
||||||
nuoerext "hyapi-server/internal/infrastructure/external/nuoer"
|
nuoerext "hyapi-server/internal/infrastructure/external/nuoer"
|
||||||
|
"hyapi-server/internal/infrastructure/external/rongxing"
|
||||||
"hyapi-server/internal/infrastructure/external/shujubao"
|
"hyapi-server/internal/infrastructure/external/shujubao"
|
||||||
"hyapi-server/internal/infrastructure/external/tianyancha"
|
"hyapi-server/internal/infrastructure/external/tianyancha"
|
||||||
"hyapi-server/internal/infrastructure/external/tianyuanapi"
|
"hyapi-server/internal/infrastructure/external/tianyuanapi"
|
||||||
@@ -67,6 +68,7 @@ func NewApiRequestService(
|
|||||||
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
||||||
nuoerService *nuoerext.NuoerService,
|
nuoerService *nuoerext.NuoerService,
|
||||||
huiboService *huibo.HuiboService,
|
huiboService *huibo.HuiboService,
|
||||||
|
rongxingService *rongxing.RongxingService,
|
||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
productManagementService *services.ProductManagementService,
|
productManagementService *services.ProductManagementService,
|
||||||
cfg *appconfig.Config,
|
cfg *appconfig.Config,
|
||||||
@@ -84,6 +86,7 @@ func NewApiRequestService(
|
|||||||
tianyuanapiService,
|
tianyuanapiService,
|
||||||
nuoerService,
|
nuoerService,
|
||||||
huiboService,
|
huiboService,
|
||||||
|
rongxingService,
|
||||||
validator,
|
validator,
|
||||||
productManagementService,
|
productManagementService,
|
||||||
cfg,
|
cfg,
|
||||||
@@ -106,6 +109,7 @@ func NewApiRequestServiceWithRepos(
|
|||||||
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
||||||
nuoerService *nuoerext.NuoerService,
|
nuoerService *nuoerext.NuoerService,
|
||||||
huiboService *huibo.HuiboService,
|
huiboService *huibo.HuiboService,
|
||||||
|
rongxingService *rongxing.RongxingService,
|
||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
productManagementService *services.ProductManagementService,
|
productManagementService *services.ProductManagementService,
|
||||||
cfg *appconfig.Config,
|
cfg *appconfig.Config,
|
||||||
@@ -134,6 +138,7 @@ func NewApiRequestServiceWithRepos(
|
|||||||
tianyuanapiService,
|
tianyuanapiService,
|
||||||
nuoerService,
|
nuoerService,
|
||||||
huiboService,
|
huiboService,
|
||||||
|
rongxingService,
|
||||||
validator,
|
validator,
|
||||||
combService,
|
combService,
|
||||||
reportRepo,
|
reportRepo,
|
||||||
@@ -193,6 +198,7 @@ func registerAllProcessors(combService *comb.CombService) {
|
|||||||
"JRZQTH74": jrzq.ProcessJRZQTH74Request, //团伙欺诈
|
"JRZQTH74": jrzq.ProcessJRZQTH74Request, //团伙欺诈
|
||||||
"JRZQGJ99": jrzq.ProcessJRZQGJ99Request, //公积金核验
|
"JRZQGJ99": jrzq.ProcessJRZQGJ99Request, //公积金核验
|
||||||
"JRZQFC59": jrzq.ProcessJRZQFC59Request, //房产核验
|
"JRZQFC59": jrzq.ProcessJRZQFC59Request, //房产核验
|
||||||
|
"JRZQ0OO1": jrzq.ProcessJRZQ0OO1Request, //戎行贷后信息
|
||||||
|
|
||||||
// QYGL系列处理器
|
// QYGL系列处理器
|
||||||
"QYGL7HBN": qygl.ProcessQYGL7HBNRequest, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I)
|
"QYGL7HBN": qygl.ProcessQYGL7HBNRequest, //企业全景报告(聚合 QYGLUY3S/QYGLJ0Q1/QYGL5S1I)
|
||||||
|
|||||||
@@ -314,7 +314,8 @@ func (s *FormConfigServiceImpl) getDTOStruct(ctx context.Context, apiCode string
|
|||||||
"JRZQFC59": &dto.JRZQFC59Req{}, //房产核验
|
"JRZQFC59": &dto.JRZQFC59Req{}, //房产核验
|
||||||
"IVYZX7J9": &dto.IVYZX7J9Req{}, //学籍核验
|
"IVYZX7J9": &dto.IVYZX7J9Req{}, //学籍核验
|
||||||
"QCXG6U5G": &dto.QCXG6U5GReq{}, //车辆核验
|
"QCXG6U5G": &dto.QCXG6U5GReq{}, //车辆核验
|
||||||
"QCXG7K2N": &dto.QCXG7K2NReq{}, //人车核验加强版
|
"JRZQ0OO1": &dto.JRZQ0OO1Req{}, //戎行贷后信息 Info360
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优先返回已配置的DTO
|
// 优先返回已配置的DTO
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"hyapi-server/internal/infrastructure/external/alicloud"
|
"hyapi-server/internal/infrastructure/external/alicloud"
|
||||||
"hyapi-server/internal/infrastructure/external/huibo"
|
"hyapi-server/internal/infrastructure/external/huibo"
|
||||||
"hyapi-server/internal/infrastructure/external/jiguang"
|
"hyapi-server/internal/infrastructure/external/jiguang"
|
||||||
|
"hyapi-server/internal/infrastructure/external/rongxing"
|
||||||
"hyapi-server/internal/infrastructure/external/muzi"
|
"hyapi-server/internal/infrastructure/external/muzi"
|
||||||
"hyapi-server/internal/infrastructure/external/nuoer"
|
"hyapi-server/internal/infrastructure/external/nuoer"
|
||||||
"hyapi-server/internal/infrastructure/external/shujubao"
|
"hyapi-server/internal/infrastructure/external/shujubao"
|
||||||
@@ -44,6 +45,7 @@ type ProcessorDependencies struct {
|
|||||||
TianyuanapiService *tianyuanapi.TianyuanapiService
|
TianyuanapiService *tianyuanapi.TianyuanapiService
|
||||||
NuoerService *nuoer.NuoerService
|
NuoerService *nuoer.NuoerService
|
||||||
HuiboService *huibo.HuiboService
|
HuiboService *huibo.HuiboService
|
||||||
|
RongxingService *rongxing.RongxingService
|
||||||
Validator interfaces.RequestValidator
|
Validator interfaces.RequestValidator
|
||||||
CombService CombServiceInterface // Changed to interface to break import cycle
|
CombService CombServiceInterface // Changed to interface to break import cycle
|
||||||
Options *commands.ApiCallOptions // 添加Options支持
|
Options *commands.ApiCallOptions // 添加Options支持
|
||||||
@@ -73,6 +75,7 @@ func NewProcessorDependencies(
|
|||||||
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
tianyuanapiService *tianyuanapi.TianyuanapiService,
|
||||||
nuoerService *nuoer.NuoerService,
|
nuoerService *nuoer.NuoerService,
|
||||||
huiboService *huibo.HuiboService,
|
huiboService *huibo.HuiboService,
|
||||||
|
rongxingService *rongxing.RongxingService,
|
||||||
validator interfaces.RequestValidator,
|
validator interfaces.RequestValidator,
|
||||||
combService CombServiceInterface, // Changed to interface
|
combService CombServiceInterface, // Changed to interface
|
||||||
reportRepo repositories.ReportRepository,
|
reportRepo repositories.ReportRepository,
|
||||||
@@ -92,6 +95,7 @@ func NewProcessorDependencies(
|
|||||||
TianyuanapiService: tianyuanapiService,
|
TianyuanapiService: tianyuanapiService,
|
||||||
NuoerService: nuoerService,
|
NuoerService: nuoerService,
|
||||||
HuiboService: huiboService,
|
HuiboService: huiboService,
|
||||||
|
RongxingService: rongxingService,
|
||||||
Validator: validator,
|
Validator: validator,
|
||||||
CombService: combService,
|
CombService: combService,
|
||||||
Options: nil, // 初始化为nil,在调用时设置
|
Options: nil, // 初始化为nil,在调用时设置
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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/rongxing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ProcessJRZQ0OO1Request(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||||
|
var paramsDto dto.JRZQ0OO1Req
|
||||||
|
if err := json.Unmarshal(params, ¶msDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if deps.RongxingService == nil {
|
||||||
|
return nil, errors.Join(processors.ErrSystem, errors.New("戎行服务未初始化"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deps.Validator.ValidateStruct(paramsDto); err != nil {
|
||||||
|
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 业务入参在处理器侧组装;三要素按接口要求 MD5 大写后入请求体
|
||||||
|
reqdata := map[string]interface{}{
|
||||||
|
"phone": rongxing.MD5Upper(paramsDto.MobileNo),
|
||||||
|
"idCard": rongxing.MD5Upper(paramsDto.IDCard),
|
||||||
|
"name": rongxing.MD5Upper(paramsDto.Name),
|
||||||
|
}
|
||||||
|
|
||||||
|
respBytes, err := deps.RongxingService.CallAPI(ctx, "/third/loan/info360", reqdata)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, rongxing.ErrNotFound) {
|
||||||
|
return nil, errors.Join(processors.ErrNotFound, err)
|
||||||
|
}
|
||||||
|
if errors.Is(err, rongxing.ErrDatasource) {
|
||||||
|
return nil, errors.Join(processors.ErrDatasource, err)
|
||||||
|
}
|
||||||
|
return nil, errors.Join(processors.ErrSystem, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return respBytes, nil
|
||||||
|
}
|
||||||
94
internal/infrastructure/external/rongxing/crypto.go
vendored
Normal file
94
internal/infrastructure/external/rongxing/crypto.go
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package rongxing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MD5Upper 对明文做 MD5,返回 32 位大写十六进制(入参说明要求大写)。
|
||||||
|
func MD5Upper(plaintext string) string {
|
||||||
|
sum := md5.Sum([]byte(strings.TrimSpace(plaintext)))
|
||||||
|
return strings.ToUpper(hex.EncodeToString(sum[:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodePasswordBase64 将明文密码做 Base64 编码(登录接口要求)。
|
||||||
|
func EncodePasswordBase64(password string) string {
|
||||||
|
return base64.StdEncoding.EncodeToString([]byte(password))
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildSignContent 生成登录签名原文:过滤 sign/空值 → 按 key 升序 → key=value&...
|
||||||
|
func BuildSignContent(params map[string]interface{}) string {
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for k, v := range params {
|
||||||
|
if strings.EqualFold(k, "sign") || v == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
parts := make([]string, 0, len(keys))
|
||||||
|
for _, k := range keys {
|
||||||
|
parts = append(parts, fmt.Sprintf("%s=%v", k, params[k]))
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignSHA256WithRSA 使用私钥对原文做 SHA256withRSA 签名,返回 Base64。
|
||||||
|
func SignSHA256WithRSA(content string, privateKey *rsa.PrivateKey) (string, error) {
|
||||||
|
if privateKey == nil {
|
||||||
|
return "", fmt.Errorf("私钥为空")
|
||||||
|
}
|
||||||
|
hashed := sha256.Sum256([]byte(content))
|
||||||
|
signed, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:])
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("RSA 签名失败: %w", err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(signed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePrivateKey 解析 RSA 私钥(支持 PEM / PKCS#8 / PKCS#1 原始 Base64)。
|
||||||
|
func ParsePrivateKey(raw string) (*rsa.PrivateKey, error) {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil, fmt.Errorf("私钥为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
var der []byte
|
||||||
|
if block, _ := pem.Decode([]byte(raw)); block != nil {
|
||||||
|
der = block.Bytes
|
||||||
|
} else {
|
||||||
|
normalized := strings.ReplaceAll(raw, "\n", "")
|
||||||
|
normalized = strings.ReplaceAll(normalized, "\r", "")
|
||||||
|
normalized = strings.ReplaceAll(normalized, " ", "")
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(normalized)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("私钥 Base64 解码失败: %w", err)
|
||||||
|
}
|
||||||
|
der = decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||||
|
rsaKey, ok := key.(*rsa.PrivateKey)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("私钥不是 RSA 类型")
|
||||||
|
}
|
||||||
|
return rsaKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("无法解析 RSA 私钥(需 PKCS#8 或 PKCS#1)")
|
||||||
|
}
|
||||||
66
internal/infrastructure/external/rongxing/crypto_test.go
vendored
Normal file
66
internal/infrastructure/external/rongxing/crypto_test.go
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package rongxing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMD5Upper(t *testing.T) {
|
||||||
|
if got := MD5Upper(""); got != "D41D8CD98F00B204E9800998ECF8427E" {
|
||||||
|
t.Fatalf("MD5Upper empty = %s", got)
|
||||||
|
}
|
||||||
|
if got := MD5Upper(" hello "); got != MD5Upper("hello") {
|
||||||
|
t.Fatalf("MD5Upper should trim spaces")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildSignContent(t *testing.T) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"account": "haiyukeji",
|
||||||
|
"password": "cGFzcw==",
|
||||||
|
"appId": "hykj",
|
||||||
|
"timestamp": int64(1710000000000),
|
||||||
|
"sign": "ignored",
|
||||||
|
}
|
||||||
|
got := BuildSignContent(params)
|
||||||
|
want := "account=haiyukeji&appId=hykj&password=cGFzcw==×tamp=1710000000000"
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("BuildSignContent mismatch\ngot: %s\nwant: %s", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSignSHA256WithRSA(t *testing.T) {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("generate key: %v", err)
|
||||||
|
}
|
||||||
|
sig, err := SignSHA256WithRSA("account=a&appId=b", key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("sign: %v", err)
|
||||||
|
}
|
||||||
|
if sig == "" {
|
||||||
|
t.Fatal("empty signature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParsePrivateKeyPKCS8Base64(t *testing.T) {
|
||||||
|
// 与对接文档一致的 PKCS#8 Base64(无 PEM 头)
|
||||||
|
raw := "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=="
|
||||||
|
key, err := ParsePrivateKey(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParsePrivateKey: %v", err)
|
||||||
|
}
|
||||||
|
if key == nil {
|
||||||
|
t.Fatal("nil key")
|
||||||
|
}
|
||||||
|
if _, err := SignSHA256WithRSA("account=haiyukeji&appId=hykj", key); err != nil {
|
||||||
|
t.Fatalf("sign with parsed key: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodePasswordBase64(t *testing.T) {
|
||||||
|
if got := EncodePasswordBase64("haiyukeji@123"); got != "aGFpeXVrZWppQDEyMw==" {
|
||||||
|
t.Fatalf("EncodePasswordBase64 = %s", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
internal/infrastructure/external/rongxing/curl_helper.go
vendored
Normal file
35
internal/infrastructure/external/rongxing/curl_helper.go
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package rongxing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// generateCurlCommand 生成可直接复现的 curl 命令,便于联调排查。
|
||||||
|
func generateCurlCommand(method, url string, headers map[string]string, body string) string {
|
||||||
|
var cmd strings.Builder
|
||||||
|
cmd.WriteString("curl -X ")
|
||||||
|
cmd.WriteString(method)
|
||||||
|
cmd.WriteString(" '")
|
||||||
|
cmd.WriteString(url)
|
||||||
|
cmd.WriteString("'")
|
||||||
|
|
||||||
|
for key, value := range headers {
|
||||||
|
cmd.WriteString(" \\\n -H '")
|
||||||
|
cmd.WriteString(key)
|
||||||
|
cmd.WriteString(": ")
|
||||||
|
cmd.WriteString(escapeSingleQuotes(value))
|
||||||
|
cmd.WriteString("'")
|
||||||
|
}
|
||||||
|
|
||||||
|
if body != "" {
|
||||||
|
cmd.WriteString(" \\\n -d '")
|
||||||
|
cmd.WriteString(escapeSingleQuotes(body))
|
||||||
|
cmd.WriteString("'")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeSingleQuotes(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "'", `'\''`)
|
||||||
|
}
|
||||||
50
internal/infrastructure/external/rongxing/rongxing_factory.go
vendored
Normal file
50
internal/infrastructure/external/rongxing/rongxing_factory.go
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package rongxing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"hyapi-server/internal/config"
|
||||||
|
"hyapi-server/internal/shared/external_logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewRongxingServiceWithConfig 使用配置创建戎行服务
|
||||||
|
func NewRongxingServiceWithConfig(cfg *config.Config) (*RongxingService, error) {
|
||||||
|
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||||
|
Enabled: cfg.Rongxing.Logging.Enabled,
|
||||||
|
LogDir: cfg.Rongxing.Logging.LogDir,
|
||||||
|
ServiceName: "rongxing",
|
||||||
|
UseDaily: cfg.Rongxing.Logging.UseDaily,
|
||||||
|
EnableLevelSeparation: cfg.Rongxing.Logging.EnableLevelSeparation,
|
||||||
|
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Rongxing.Logging.ServiceName != "" {
|
||||||
|
loggingConfig.ServiceName = cfg.Rongxing.Logging.ServiceName
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range cfg.Rongxing.Logging.LevelConfigs {
|
||||||
|
loggingConfig.LevelConfigs[key] = external_logger.ExternalServiceLevelFileConfig{
|
||||||
|
MaxSize: value.MaxSize,
|
||||||
|
MaxBackups: value.MaxBackups,
|
||||||
|
MaxAge: value.MaxAge,
|
||||||
|
Compress: value.Compress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := cfg.Rongxing.Timeout
|
||||||
|
if timeout <= 0 {
|
||||||
|
timeout = defaultRequestTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewRongxingService(serviceConfig{
|
||||||
|
BaseURL: cfg.Rongxing.URL,
|
||||||
|
Account: cfg.Rongxing.Account,
|
||||||
|
Password: cfg.Rongxing.Password,
|
||||||
|
AppID: cfg.Rongxing.AppID,
|
||||||
|
PrivateKey: cfg.Rongxing.PrivateKey,
|
||||||
|
Timeout: timeout,
|
||||||
|
}, logger), nil
|
||||||
|
}
|
||||||
432
internal/infrastructure/external/rongxing/rongxing_service.go
vendored
Normal file
432
internal/infrastructure/external/rongxing/rongxing_service.go
vendored
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
package rongxing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"hyapi-server/internal/shared/external_logger"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultRequestTimeout = 10 * time.Second
|
||||||
|
apiKeyLogin = "auth_login"
|
||||||
|
pathAuthLogin = "/auth/login"
|
||||||
|
headerDmsToken = "dms-token"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serviceConfig 戎行服务运行时配置
|
||||||
|
type serviceConfig struct {
|
||||||
|
BaseURL string
|
||||||
|
Account string
|
||||||
|
Password string
|
||||||
|
AppID string
|
||||||
|
PrivateKey string
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// RongxingService 戎行数据源服务
|
||||||
|
type RongxingService struct {
|
||||||
|
config serviceConfig
|
||||||
|
logger *external_logger.ExternalServiceLogger
|
||||||
|
|
||||||
|
tokenMu sync.RWMutex
|
||||||
|
cachedToken string
|
||||||
|
}
|
||||||
|
|
||||||
|
// apiResponse 戎行统一响应。code 可能是数字或字符串;扣费以 consumeFlag 为准。
|
||||||
|
type apiResponse struct {
|
||||||
|
Flag bool `json:"flag"`
|
||||||
|
Code json.RawMessage `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
ConsumeFlag int `json:"consumeFlag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r apiResponse) text() string {
|
||||||
|
if r.Msg != "" {
|
||||||
|
return r.Msg
|
||||||
|
}
|
||||||
|
return r.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r apiResponse) code() string {
|
||||||
|
return parseCode(r.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRongxingService 创建戎行服务实例
|
||||||
|
func NewRongxingService(cfg serviceConfig, logger *external_logger.ExternalServiceLogger) *RongxingService {
|
||||||
|
if cfg.Timeout <= 0 {
|
||||||
|
cfg.Timeout = defaultRequestTimeout
|
||||||
|
}
|
||||||
|
cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
|
||||||
|
return &RongxingService{config: cfg, logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConfig 获取运行时配置
|
||||||
|
func (s *RongxingService) GetConfig() serviceConfig {
|
||||||
|
return s.config
|
||||||
|
}
|
||||||
|
|
||||||
|
// CallAPI 通用业务接口调用。
|
||||||
|
// apiPath 为相对路径(如 /third/loan/info360),reqData 为已组装好的请求体。
|
||||||
|
// Token 获取与 Header 注入由服务内部处理;401/403 时自动刷新 Token 并重试一次。
|
||||||
|
func (s *RongxingService) CallAPI(ctx context.Context, apiPath string, reqData map[string]interface{}) ([]byte, error) {
|
||||||
|
apiKey := strings.Trim(apiPath, "/")
|
||||||
|
|
||||||
|
var transactionID string
|
||||||
|
if id, ok := ctx.Value("transaction_id").(string); ok {
|
||||||
|
transactionID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.validateConfig(); err != nil {
|
||||||
|
err = errors.Join(ErrSystem, err)
|
||||||
|
s.logError(transactionID, apiKey, "", err, nil)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(apiPath, "/") {
|
||||||
|
apiPath = "/" + apiPath
|
||||||
|
}
|
||||||
|
requestURL := s.config.BaseURL + apiPath
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(reqData)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(ErrSystem, err)
|
||||||
|
s.logError(transactionID, apiKey, "", err, reqData)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bodyStr := string(bodyBytes)
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 2; attempt++ {
|
||||||
|
startTime := time.Now()
|
||||||
|
|
||||||
|
token, err := s.getToken(ctx, transactionID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := map[string]string{
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
headerDmsToken: token,
|
||||||
|
}
|
||||||
|
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr)
|
||||||
|
s.logCurl(transactionID, apiKey, requestURL, curlCmd)
|
||||||
|
|
||||||
|
if s.logger != nil {
|
||||||
|
s.logger.LogRequest("", transactionID, apiKey, requestURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(ErrSystem, err)
|
||||||
|
s.logErrorWithCurl(transactionID, apiKey, err, reqData, curlCmd, "")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set(headerDmsToken, token)
|
||||||
|
|
||||||
|
respBody, statusCode, err := s.doHTTP(req)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(ErrDatasource, err)
|
||||||
|
s.logErrorWithCurl(transactionID, apiKey, err, reqData, curlCmd, "")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
respStr := string(respBody)
|
||||||
|
|
||||||
|
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
|
||||||
|
s.clearToken()
|
||||||
|
if attempt == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", statusCode))
|
||||||
|
s.logErrorWithCurl(transactionID, apiKey, err, reqData, curlCmd, respStr)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if statusCode != http.StatusOK {
|
||||||
|
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", statusCode))
|
||||||
|
s.logErrorWithCurl(transactionID, apiKey, err, reqData, curlCmd, respStr)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp apiResponse
|
||||||
|
if err := json.Unmarshal(respBody, &resp); err != nil {
|
||||||
|
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w, body=%s", err, respStr))
|
||||||
|
s.logErrorWithCurl(transactionID, apiKey, err, reqData, curlCmd, respStr)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := resp.code()
|
||||||
|
payload := extractBusinessPayload(resp.Data)
|
||||||
|
s.logResponse(transactionID, apiKey, statusCode, duration, respStr)
|
||||||
|
|
||||||
|
// 扣费只看 consumeFlag:1 扣费(按成功返回),0 不扣费
|
||||||
|
if IsBillable(resp.ConsumeFlag) {
|
||||||
|
return payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sentinel := MapNonBillableToErr(code)
|
||||||
|
err = errors.Join(sentinel, NewRongxingError(code, resp.text()))
|
||||||
|
if !errors.Is(sentinel, ErrNotFound) {
|
||||||
|
s.logErrorWithCurl(transactionID, apiKey, err, reqData, curlCmd, respStr)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Join(ErrDatasource, errors.New("请求失败"))
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RongxingService) clearToken() {
|
||||||
|
s.tokenMu.Lock()
|
||||||
|
s.cachedToken = ""
|
||||||
|
s.tokenMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RongxingService) login(ctx context.Context, transactionID string) (string, error) {
|
||||||
|
privateKey, err := ParsePrivateKey(s.config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(ErrSystem, err)
|
||||||
|
s.logError(transactionID, apiKeyLogin, "", err, nil)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordB64 := EncodePasswordBase64(s.config.Password)
|
||||||
|
timestamp := time.Now().UnixMilli()
|
||||||
|
signParams := map[string]interface{}{
|
||||||
|
"account": s.config.Account,
|
||||||
|
"password": passwordB64,
|
||||||
|
"appId": s.config.AppID,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
}
|
||||||
|
content := BuildSignContent(signParams)
|
||||||
|
sign, err := SignSHA256WithRSA(content, privateKey)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(ErrSystem, err)
|
||||||
|
s.logError(transactionID, apiKeyLogin, "", err, nil)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload := map[string]interface{}{
|
||||||
|
"account": s.config.Account,
|
||||||
|
"password": passwordB64,
|
||||||
|
"appId": s.config.AppID,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"sign": sign,
|
||||||
|
}
|
||||||
|
requestURL := s.config.BaseURL + pathAuthLogin
|
||||||
|
|
||||||
|
bodyBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(ErrSystem, err)
|
||||||
|
s.logError(transactionID, apiKeyLogin, "", err, map[string]interface{}{
|
||||||
|
"account": s.config.Account,
|
||||||
|
"appId": s.config.AppID,
|
||||||
|
})
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
bodyStr := string(bodyBytes)
|
||||||
|
|
||||||
|
headers := map[string]string{"Content-Type": "application/json"}
|
||||||
|
curlCmd := generateCurlCommand(http.MethodPost, requestURL, headers, bodyStr)
|
||||||
|
s.logCurl(transactionID, apiKeyLogin, requestURL, curlCmd)
|
||||||
|
|
||||||
|
if s.logger != nil {
|
||||||
|
s.logger.LogRequest("", transactionID, apiKeyLogin, requestURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewBuffer(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(ErrSystem, err)
|
||||||
|
s.logErrorWithCurl(transactionID, apiKeyLogin, err, nil, curlCmd, "")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
startTime := time.Now()
|
||||||
|
respBody, statusCode, err := s.doHTTP(req)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Join(ErrDatasource, err)
|
||||||
|
s.logErrorWithCurl(transactionID, apiKeyLogin, err, nil, curlCmd, "")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
duration := time.Since(startTime)
|
||||||
|
respStr := string(respBody)
|
||||||
|
|
||||||
|
if statusCode != http.StatusOK {
|
||||||
|
err = errors.Join(ErrDatasource, fmt.Errorf("登录 HTTP状态码 %d, body=%s", statusCode, respStr))
|
||||||
|
s.logErrorWithCurl(transactionID, apiKeyLogin, err, nil, curlCmd, respStr)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var loginResp apiResponse
|
||||||
|
if err := json.Unmarshal(respBody, &loginResp); err != nil {
|
||||||
|
err = errors.Join(ErrSystem, fmt.Errorf("登录响应解析失败: %w, body=%s", err, respStr))
|
||||||
|
s.logErrorWithCurl(transactionID, apiKeyLogin, err, nil, curlCmd, respStr)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
code := loginResp.code()
|
||||||
|
if code != CodeSuccess {
|
||||||
|
rxErr := NewRongxingError(code, loginResp.text())
|
||||||
|
err = errors.Join(ErrDatasource, rxErr)
|
||||||
|
s.logErrorWithCurl(transactionID, apiKeyLogin, err, nil, curlCmd, respStr)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var token string
|
||||||
|
if err := json.Unmarshal(loginResp.Data, &token); err != nil {
|
||||||
|
err = errors.Join(ErrSystem, fmt.Errorf("登录响应 Token 解析失败: %w, body=%s", err, respStr))
|
||||||
|
s.logErrorWithCurl(transactionID, apiKeyLogin, err, nil, curlCmd, respStr)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
if token == "" {
|
||||||
|
err = errors.Join(ErrSystem, fmt.Errorf("登录响应 Token 为空, body=%s", respStr))
|
||||||
|
s.logErrorWithCurl(transactionID, apiKeyLogin, err, nil, curlCmd, respStr)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logResponse(transactionID, apiKeyLogin, statusCode, duration, respStr)
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RongxingService) doHTTP(req *http.Request) ([]byte, int, error) {
|
||||||
|
client := &http.Client{Timeout: s.config.Timeout}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
return body, resp.StatusCode, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RongxingService) validateConfig() error {
|
||||||
|
if s.config.BaseURL == "" {
|
||||||
|
return errors.New("戎行 url 未配置")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.config.Account) == "" {
|
||||||
|
return errors.New("戎行 account 未配置")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.config.Password) == "" {
|
||||||
|
return errors.New("戎行 password 未配置")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.config.AppID) == "" {
|
||||||
|
return errors.New("戎行 app_id 未配置")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.config.PrivateKey) == "" {
|
||||||
|
return errors.New("戎行 private_key 未配置")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCode(raw json.RawMessage) string {
|
||||||
|
if len(raw) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(raw, &s); err == nil {
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
var n json.Number
|
||||||
|
if err := json.Unmarshal(raw, &n); err == nil {
|
||||||
|
return n.String()
|
||||||
|
}
|
||||||
|
return strings.Trim(string(raw), `"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractBusinessPayload 提取对外返回的业务 data。
|
||||||
|
// 若外层 data 内还嵌套 data(如 Info360),则取内层标签对象。
|
||||||
|
func extractBusinessPayload(data json.RawMessage) []byte {
|
||||||
|
if len(data) == 0 || string(data) == "null" {
|
||||||
|
return []byte("{}")
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrap struct {
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &wrap); err == nil &&
|
||||||
|
len(wrap.Data) > 0 && string(wrap.Data) != "null" {
|
||||||
|
return wrap.Data
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RongxingService) logCurl(transactionID, apiKey, url, curlCmd string) {
|
||||||
|
if s.logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.LogInfo("rongxing curl",
|
||||||
|
zap.String("transaction_id", transactionID),
|
||||||
|
zap.String("api_code", apiKey),
|
||||||
|
zap.String("url", url),
|
||||||
|
zap.String("curl", curlCmd),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RongxingService) logResponse(transactionID, apiKey string, statusCode int, duration time.Duration, responseBody string) {
|
||||||
|
if s.logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.LogResponseWithBody("", transactionID, apiKey, statusCode, duration, responseBody)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RongxingService) logError(transactionID, apiKey, requestID string, err error, payload interface{}) {
|
||||||
|
if s.logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.LogError(requestID, transactionID, apiKey, err, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *RongxingService) logErrorWithCurl(transactionID, apiKey string, err error, payload interface{}, curlCmd, respBody string) {
|
||||||
|
if s.logger == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.logger.LogErrorWithFields("rongxing API错误",
|
||||||
|
zap.String("transaction_id", transactionID),
|
||||||
|
zap.String("api_code", apiKey),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Any("params", payload),
|
||||||
|
zap.String("curl", curlCmd),
|
||||||
|
zap.String("response_body", respBody),
|
||||||
|
)
|
||||||
|
}
|
||||||
75
internal/infrastructure/external/rongxing/status_codes.go
vendored
Normal file
75
internal/infrastructure/external/rongxing/status_codes.go
vendored
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package rongxing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 业务返回码(仅用于错误描述/分类,不作为扣费依据)
|
||||||
|
const (
|
||||||
|
CodeSuccess = "200" // 成功
|
||||||
|
CodeNoData = "204" // 未查到数据
|
||||||
|
CodeInternalErr = "503" // 内部服务错误
|
||||||
|
)
|
||||||
|
|
||||||
|
// consumeFlag 扣费标记(对方各业务码场景下均可能返回)
|
||||||
|
const (
|
||||||
|
ConsumeFlagNoCharge = 0 // 不扣费
|
||||||
|
ConsumeFlagCharge = 1 // 扣费
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDatasource = errors.New("数据源异常")
|
||||||
|
ErrSystem = errors.New("系统异常")
|
||||||
|
ErrNotFound = errors.New("查询为空")
|
||||||
|
)
|
||||||
|
|
||||||
|
var codeMessage = map[string]string{
|
||||||
|
CodeSuccess: "成功",
|
||||||
|
CodeNoData: "未查到数据",
|
||||||
|
CodeInternalErr: "内部服务错误",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCodeMessage 返回码描述
|
||||||
|
func GetCodeMessage(code string) string {
|
||||||
|
if msg, ok := codeMessage[code]; ok {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsBillable 按 consumeFlag 判断是否扣费(不以 code 为依据)
|
||||||
|
func IsBillable(consumeFlag int) bool {
|
||||||
|
return consumeFlag == ConsumeFlagCharge
|
||||||
|
}
|
||||||
|
|
||||||
|
type rongxingError struct {
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *rongxingError) Error() string {
|
||||||
|
return fmt.Sprintf("戎行返回错误,code: %s,message: %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRongxingError 创建戎行业务错误
|
||||||
|
func NewRongxingError(code, message string) *rongxingError {
|
||||||
|
if message == "" {
|
||||||
|
if desc := GetCodeMessage(code); desc != "" {
|
||||||
|
message = desc
|
||||||
|
} else {
|
||||||
|
message = "戎行返回未知错误"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &rongxingError{Code: code, Message: message}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapNonBillableToErr 不扣费场景下,将返回码映射为内部哨兵错误
|
||||||
|
func MapNonBillableToErr(code string) error {
|
||||||
|
switch code {
|
||||||
|
case CodeSuccess, CodeNoData:
|
||||||
|
return ErrNotFound
|
||||||
|
default:
|
||||||
|
return ErrDatasource
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -327,6 +327,24 @@ func (e *ExternalServiceLogger) LogResponse(requestID, transactionID, apiCode st
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogResponseWithBody 记录响应日志(含原始响应体)
|
||||||
|
func (e *ExternalServiceLogger) LogResponseWithBody(requestID, transactionID, apiCode string, statusCode int, duration time.Duration, responseBody string) {
|
||||||
|
logger := e.responseLogger
|
||||||
|
if logger == nil {
|
||||||
|
logger = e.logger
|
||||||
|
}
|
||||||
|
logger.Info(fmt.Sprintf("%s API响应", e.serviceName),
|
||||||
|
zap.String("service", e.serviceName),
|
||||||
|
zap.String("request_id", requestID),
|
||||||
|
zap.String("transaction_id", transactionID),
|
||||||
|
zap.String("api_code", apiCode),
|
||||||
|
zap.Int("status_code", statusCode),
|
||||||
|
zap.Duration("duration", duration),
|
||||||
|
zap.String("response_body", responseBody),
|
||||||
|
zap.String("timestamp", time.Now().Format(time.RFC3339)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// LogResponseWithID 记录包含响应ID的响应日志
|
// LogResponseWithID 记录包含响应ID的响应日志
|
||||||
func (e *ExternalServiceLogger) LogResponseWithID(requestID, transactionID, apiCode string, statusCode int, duration time.Duration, responseID string) {
|
func (e *ExternalServiceLogger) LogResponseWithID(requestID, transactionID, apiCode string, statusCode int, duration time.Duration, responseID string) {
|
||||||
logger := e.responseLogger
|
logger := e.responseLogger
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ type signConfigInfo struct {
|
|||||||
OrderNo int `json:"orderNo"`
|
OrderNo int `json:"orderNo"`
|
||||||
JoinByLink bool `json:"joinByLink"`
|
JoinByLink bool `json:"joinByLink"`
|
||||||
RequestVerifyFree bool `json:"requestVerifyFree"`
|
RequestVerifyFree bool `json:"requestVerifyFree"`
|
||||||
FreeSignType string `json:"freeSignType,omitempty"` // template=按模板免验证签;business=按场景码(需 businessId)
|
FreeSignType string `json:"freeSignType,omitempty"` // template=按模板;business=按场景码
|
||||||
FreeLogin bool `json:"freeLogin"`
|
FreeLogin bool `json:"freeLogin"`
|
||||||
BlockHere bool `json:"blockHere"`
|
BlockHere bool `json:"blockHere"`
|
||||||
}
|
}
|
||||||
@@ -125,7 +125,7 @@ type createSignTaskWithTemplateReq struct {
|
|||||||
SignInOrder bool `json:"signInOrder,omitempty"`
|
SignInOrder bool `json:"signInOrder,omitempty"`
|
||||||
ExpiresTime string `json:"expiresTime,omitempty"`
|
ExpiresTime string `json:"expiresTime,omitempty"`
|
||||||
FreeSignType string `json:"freeSignType,omitempty"` // template:按模板;business:按场景码
|
FreeSignType string `json:"freeSignType,omitempty"` // template:按模板;business:按场景码
|
||||||
BusinessID string `json:"businessId,omitempty"` // 仅 freeSignType=business 时传场景码
|
BusinessID string `json:"businessId,omitempty"` // 免验证签场景码;requestVerifyFree=true 时必传
|
||||||
TransReferenceID string `json:"transReferenceId,omitempty"`
|
TransReferenceID string `json:"transReferenceId,omitempty"`
|
||||||
Actors []signTaskActor `json:"actors"`
|
Actors []signTaskActor `json:"actors"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type Config struct {
|
|||||||
AppSecret string `json:"appSecret" yaml:"app_secret"`
|
AppSecret string `json:"appSecret" yaml:"app_secret"`
|
||||||
ServerURL string `json:"serverUrl" yaml:"server_url"`
|
ServerURL string `json:"serverUrl" yaml:"server_url"`
|
||||||
OpenCorpID string `json:"openCorpId" yaml:"open_corp_id"`
|
OpenCorpID string `json:"openCorpId" yaml:"open_corp_id"`
|
||||||
// NoAuthSceneCode 免验证签场景码;仅 freeSignType=business 时需要。当前按模板免验证签,可留空。
|
// NoAuthSceneCode 免验证签场景码(businessId);requestVerifyFree=true 时必传。
|
||||||
NoAuthSceneCode string `json:"noAuthSceneCode" yaml:"no_auth_scene_code"`
|
NoAuthSceneCode string `json:"noAuthSceneCode" yaml:"no_auth_scene_code"`
|
||||||
TemplateID string `json:"templateId" yaml:"template_id"`
|
TemplateID string `json:"templateId" yaml:"template_id"`
|
||||||
// PartyAActorID / PartyBActorID 签署模板中的参与方标识(actorId),须与模板角色名一致
|
// PartyAActorID / PartyBActorID 签署模板中的参与方标识(actorId),须与模板角色名一致
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
// CreateSignFlow 基于签署任务模板创建签署任务(POST /sign-task/create-with-template)
|
// CreateSignFlow 基于签署任务模板创建签署任务(POST /sign-task/create-with-template)
|
||||||
// - autoStart:无填单时创建即自动提交;有填单时创建后填控件再 Start(完成自动提交)
|
// - autoStart:无填单时创建即自动提交;有填单时创建后填控件再 Start(完成自动提交)
|
||||||
// - 签署顺序:甲方 orderNo=1 先签(短信/验证);乙方 orderNo=2 后签,由系统按模板免验证签自动盖章
|
// - 签署顺序:甲方 orderNo=1 先签(短信/验证);乙方 orderNo=2 后签,由系统按模板免验证签自动盖章
|
||||||
// - 乙方参与方开启免验证签(requestVerifyFree + freeSignType=template)
|
// - 乙方参与方开启免验证签(requestVerifyFree=true + freeSignType=template);requestVerifyFree 时 businessId 必传
|
||||||
func (c *Client) CreateSignFlow(req *CreateSignFlowRequest) (*CreateSignFlowResult, error) {
|
func (c *Client) CreateSignFlow(req *CreateSignFlowRequest) (*CreateSignFlowResult, error) {
|
||||||
if err := validateCreateSignFlowRequest(req); err != nil {
|
if err := validateCreateSignFlowRequest(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -31,6 +31,11 @@ func (c *Client) CreateSignFlow(req *CreateSignFlowRequest) (*CreateSignFlowResu
|
|||||||
// 有填单须先填再提交,创建时不能 autoStart;无填单则创建时自动提交
|
// 有填单须先填再提交,创建时不能 autoStart;无填单则创建时自动提交
|
||||||
autoStart := !needFill
|
autoStart := !needFill
|
||||||
|
|
||||||
|
businessID, err := c.resolveBusinessID()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
subject := strings.TrimSpace(req.Subject)
|
subject := strings.TrimSpace(req.Subject)
|
||||||
if subject == "" {
|
if subject == "" {
|
||||||
if c.config.Contract != nil && c.config.Contract.Name != "" {
|
if c.config.Contract != nil && c.config.Contract.Name != "" {
|
||||||
@@ -55,7 +60,8 @@ func (c *Client) CreateSignFlow(req *CreateSignFlowRequest) (*CreateSignFlowResu
|
|||||||
AutoFinish: true,
|
AutoFinish: true,
|
||||||
SignInOrder: true,
|
SignInOrder: true,
|
||||||
ExpiresTime: expiresTime,
|
ExpiresTime: expiresTime,
|
||||||
FreeSignType: freeSignTypeTemplate, // 按模板免验证签,无需 businessId
|
FreeSignType: freeSignTypeTemplate, // 按模板免验证签
|
||||||
|
BusinessID: businessID, // requestVerifyFree=true 时必传场景码
|
||||||
TransReferenceID: strings.TrimSpace(req.TransReferenceID),
|
TransReferenceID: strings.TrimSpace(req.TransReferenceID),
|
||||||
Actors: actors,
|
Actors: actors,
|
||||||
})
|
})
|
||||||
@@ -338,7 +344,19 @@ func (c *Client) GenerateContractSigning(req *ContractSigningRequest) (*Contract
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const freeSignTypeTemplate = "template" // 根据模板免验证签(无需 businessId)
|
const freeSignTypeTemplate = "template" // 根据模板免验证签;requestVerifyFree 时仍须传 businessId
|
||||||
|
|
||||||
|
// resolveBusinessID 免验证签场景码(requestVerifyFree=true 时 businessId 必传)
|
||||||
|
func (c *Client) resolveBusinessID() (string, error) {
|
||||||
|
if c.config == nil {
|
||||||
|
return "", fmt.Errorf("法大大配置为空")
|
||||||
|
}
|
||||||
|
businessID := strings.TrimSpace(c.config.NoAuthSceneCode)
|
||||||
|
if businessID == "" {
|
||||||
|
return "", fmt.Errorf("法大大 no_auth_scene_code(免验证签场景码)不能为空:requestVerifyFree 时 businessId 必传")
|
||||||
|
}
|
||||||
|
return businessID, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) buildSignActors(req *CreateSignFlowRequest) []signTaskActor {
|
func (c *Client) buildSignActors(req *CreateSignFlowRequest) []signTaskActor {
|
||||||
partyAActorID := c.config.ResolvePartyAActorID()
|
partyAActorID := c.config.ResolvePartyAActorID()
|
||||||
@@ -366,7 +384,7 @@ func (c *Client) buildSignActors(req *CreateSignFlowRequest) []signTaskActor {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 乙方(平台)后签 OrderNo=2:甲方签完后由系统按模板免验证签自动盖章
|
// 乙方(平台)后签 OrderNo=2:甲方签完后由系统按模板免验证签自动盖章(任务级传 businessId)
|
||||||
partyB := signTaskActor{
|
partyB := signTaskActor{
|
||||||
Actor: &actor{
|
Actor: &actor{
|
||||||
ActorType: "corp",
|
ActorType: "corp",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func TestBuildSignActors(t *testing.T) {
|
|||||||
AppSecret: "secret",
|
AppSecret: "secret",
|
||||||
ServerURL: "https://uat-api.fadada.com/api/v5/",
|
ServerURL: "https://uat-api.fadada.com/api/v5/",
|
||||||
OpenCorpID: "haiyu-open-corp",
|
OpenCorpID: "haiyu-open-corp",
|
||||||
|
NoAuthSceneCode: "scene-code",
|
||||||
TemplateID: "tpl",
|
TemplateID: "tpl",
|
||||||
})
|
})
|
||||||
actors := c.buildSignActors(&CreateSignFlowRequest{
|
actors := c.buildSignActors(&CreateSignFlowRequest{
|
||||||
@@ -45,6 +46,10 @@ func TestBuildSignActors(t *testing.T) {
|
|||||||
if actors[1].SignConfigInfo.FreeSignType != freeSignTypeTemplate {
|
if actors[1].SignConfigInfo.FreeSignType != freeSignTypeTemplate {
|
||||||
t.Fatalf("party B freeSignType=%q, want %s", actors[1].SignConfigInfo.FreeSignType, freeSignTypeTemplate)
|
t.Fatalf("party B freeSignType=%q, want %s", actors[1].SignConfigInfo.FreeSignType, freeSignTypeTemplate)
|
||||||
}
|
}
|
||||||
|
businessID, err := c.resolveBusinessID()
|
||||||
|
if err != nil || businessID != "scene-code" {
|
||||||
|
t.Fatalf("businessId=%q err=%v, want scene-code", businessID, err)
|
||||||
|
}
|
||||||
if actors[0].Actor.ActorID == "party_a" || actors[1].Actor.ActorID == "party_b" {
|
if actors[0].Actor.ActorID == "party_a" || actors[1].Actor.ActorID == "party_b" {
|
||||||
t.Fatal("actorId must match sign-template role names, not party_a/party_b")
|
t.Fatal("actorId must match sign-template role names, not party_a/party_b")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user