f
This commit is contained in:
47
internal/infrastructure/external/shujubao/crypto.go
vendored
Normal file
47
internal/infrastructure/external/shujubao/crypto.go
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SignMethod 签名方法
|
||||
type SignMethod string
|
||||
|
||||
const (
|
||||
SignMethodMD5 SignMethod = "md5"
|
||||
SignMethodHMACMD5 SignMethod = "hmac"
|
||||
)
|
||||
|
||||
// GenerateSignMD5 使用 MD5 生成签名:md5(app_secret + timestamp),32 位小写
|
||||
func GenerateSignMD5(appSecret, timestamp string) string {
|
||||
h := md5.Sum([]byte(appSecret + timestamp))
|
||||
sign := strings.ToLower(hex.EncodeToString(h[:]))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignHMAC 使用 HMAC-MD5 生成签名(仅 timestamp,兼容旧逻辑)
|
||||
func GenerateSignHMAC(appSecret, timestamp string) string {
|
||||
mac := hmac.New(md5.New, []byte(appSecret))
|
||||
mac.Write([]byte(timestamp))
|
||||
sign := strings.ToLower(hex.EncodeToString(mac.Sum(nil)))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignFromParamsMD5 根据入参生成签名:入参按 ASCII 排序组合后与 app_secret 做 MD5。
|
||||
// sortedParamStr 格式为 key1=value1&key2=value2&...(key 按字母序)。
|
||||
func GenerateSignFromParamsMD5(appSecret, sortedParamStr string) string {
|
||||
h := md5.Sum([]byte(appSecret + sortedParamStr))
|
||||
sign := strings.ToLower(hex.EncodeToString(h[:]))
|
||||
return sign
|
||||
}
|
||||
|
||||
// GenerateSignFromParamsHMAC 根据入参生成签名:入参按 ASCII 排序组合后与 app_secret 做 HMAC-MD5。
|
||||
func GenerateSignFromParamsHMAC(appSecret, sortedParamStr string) string {
|
||||
mac := hmac.New(md5.New, []byte(appSecret))
|
||||
mac.Write([]byte(sortedParamStr))
|
||||
sign := strings.ToLower(hex.EncodeToString(mac.Sum(nil)))
|
||||
return sign
|
||||
}
|
||||
121
internal/infrastructure/external/shujubao/shujubao_errors.go
vendored
Normal file
121
internal/infrastructure/external/shujubao/shujubao_errors.go
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ShujubaoError 数据宝服务错误
|
||||
type ShujubaoError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error 实现 error 接口
|
||||
func (e *ShujubaoError) Error() string {
|
||||
return fmt.Sprintf("数据宝错误 [%s]: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否成功
|
||||
func (e *ShujubaoError) IsSuccess() bool {
|
||||
return e.Code == "200" || e.Code == "0" || e.Code == "10000"
|
||||
}
|
||||
|
||||
// NewShujubaoError 创建新的数据宝错误
|
||||
func NewShujubaoError(code, message string) *ShujubaoError {
|
||||
return &ShujubaoError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// 数据宝全系统错误码与描述映射(Code -> Desc)
|
||||
var systemErrorCodeDesc = map[string]string{
|
||||
"10000": "成功",
|
||||
"10001": "参数传入有误",
|
||||
"10002": "查询失败",
|
||||
"10003": "系统处理异常",
|
||||
"10004": "系统处理超时",
|
||||
"10005": "服务异常",
|
||||
"10006": "查无",
|
||||
"10020": "同一参数请求次数超限",
|
||||
"99999": "其他错误",
|
||||
"999": "接口处理异常",
|
||||
"000": "key参数不能为空",
|
||||
"001": "找不到这个key",
|
||||
"002": "调用次数已用完",
|
||||
"003": "用户该接口状态不可用",
|
||||
"004": "接口信息不存在",
|
||||
"005": "你没有认证信息",
|
||||
"008": "当前接口只允许“企业认证”通过的账户进行调用,请在数据宝官网个人中心进行企业认证后再进行调用,谢谢!",
|
||||
"009": "触发风控",
|
||||
"011": "接口缺少参数",
|
||||
"012": "没有ip访问权限",
|
||||
"013": "接口模板不存在",
|
||||
"015": "该接口已下架",
|
||||
"020": "调用第三方产生异常",
|
||||
"022": "调用第三方返回的数据格式错误",
|
||||
"025": "你没有购买此接口",
|
||||
"026": "用户信息不存在",
|
||||
"027": "请求第三方地址超时,请稍后再试",
|
||||
"028": "请求第三方地址被拒绝,请稍后再试",
|
||||
"034": "签名不合法",
|
||||
"035": "请求参数加密有误",
|
||||
"036": "验签失败",
|
||||
"037": "timestamp不能为空",
|
||||
"038": "请求繁忙,请稍后联系管理员再试",
|
||||
"039": "请在个人中心接口设置加密状态",
|
||||
"040": "timestamp不合法",
|
||||
"041": "timestamp已过期",
|
||||
"042": "身份证手机号姓名银行卡等不符合规则",
|
||||
"043": "该号段不支持验证",
|
||||
"047": "请在个人中心获取密钥",
|
||||
"048": "找不到这个secretKey",
|
||||
"049": "用户还未申购该产品",
|
||||
"050": "请联系客服开启验签",
|
||||
"051": "超过当日调用次数",
|
||||
"052": "机房限制调用,请联系客服切换其他机房",
|
||||
"053": "系统错误",
|
||||
"054": "token无效",
|
||||
"055": "配置信息未完善,请联系数据宝工作人员",
|
||||
"056": "apiName参数不能为空",
|
||||
"057": "并发量超过限制,请联系客服",
|
||||
"058": "撞库风控预警,请联系客服",
|
||||
}
|
||||
|
||||
// GetSystemErrorDesc 根据错误码获取系统错误描述(支持带 SYSTEM_ 前缀或纯数字)
|
||||
func GetSystemErrorDesc(code string) string {
|
||||
// 去掉 SYSTEM_ 前缀
|
||||
key := code
|
||||
if len(code) > 7 && code[:7] == "SYSTEM_" {
|
||||
key = code[7:]
|
||||
}
|
||||
if desc, ok := systemErrorCodeDesc[key]; ok {
|
||||
return desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// NewShujubaoErrorFromCode 根据状态码创建错误
|
||||
func NewShujubaoErrorFromCode(code, message string) *ShujubaoError {
|
||||
if message != "" {
|
||||
return NewShujubaoError(code, message)
|
||||
}
|
||||
if desc := GetSystemErrorDesc(code); desc != "" {
|
||||
return NewShujubaoError(code, desc)
|
||||
}
|
||||
return NewShujubaoError(code, "未知错误")
|
||||
}
|
||||
|
||||
// IsShujubaoError 检查是否是数据宝错误
|
||||
func IsShujubaoError(err error) bool {
|
||||
_, ok := err.(*ShujubaoError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetShujubaoError 获取数据宝错误
|
||||
func GetShujubaoError(err error) *ShujubaoError {
|
||||
if shujubaoErr, ok := err.(*ShujubaoError); ok {
|
||||
return shujubaoErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
66
internal/infrastructure/external/shujubao/shujubao_factory.go
vendored
Normal file
66
internal/infrastructure/external/shujubao/shujubao_factory.go
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewShujubaoServiceWithConfig 使用配置创建数据宝服务
|
||||
func NewShujubaoServiceWithConfig(cfg *config.Config) (*ShujubaoService, error) {
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Shujubao.Logging.Enabled,
|
||||
LogDir: cfg.Shujubao.Logging.LogDir,
|
||||
ServiceName: "shujubao",
|
||||
UseDaily: cfg.Shujubao.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Shujubao.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
for k, v := range cfg.Shujubao.Logging.LevelConfigs {
|
||||
loggingConfig.LevelConfigs[k] = external_logger.ExternalServiceLevelFileConfig{
|
||||
MaxSize: v.MaxSize,
|
||||
MaxBackups: v.MaxBackups,
|
||||
MaxAge: v.MaxAge,
|
||||
Compress: v.Compress,
|
||||
}
|
||||
}
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var signMethod SignMethod
|
||||
if cfg.Shujubao.SignMethod == "md5" {
|
||||
signMethod = SignMethodMD5
|
||||
} else {
|
||||
signMethod = SignMethodHMACMD5
|
||||
}
|
||||
timeout := 60 * time.Second
|
||||
if cfg.Shujubao.Timeout > 0 {
|
||||
timeout = cfg.Shujubao.Timeout
|
||||
}
|
||||
|
||||
return NewShujubaoService(
|
||||
cfg.Shujubao.URL,
|
||||
cfg.Shujubao.AppSecret,
|
||||
signMethod,
|
||||
timeout,
|
||||
logger,
|
||||
), nil
|
||||
}
|
||||
|
||||
// NewShujubaoServiceWithLogging 使用自定义日志配置创建数据宝服务
|
||||
func NewShujubaoServiceWithLogging(url, appSecret string, signMethod SignMethod, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*ShujubaoService, error) {
|
||||
loggingConfig.ServiceName = "shujubao"
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewShujubaoService(url, appSecret, signMethod, timeout, logger), nil
|
||||
}
|
||||
|
||||
// NewShujubaoServiceSimple 创建无日志的数据宝服务
|
||||
func NewShujubaoServiceSimple(url, appSecret string, signMethod SignMethod, timeout time.Duration) *ShujubaoService {
|
||||
return NewShujubaoService(url, appSecret, signMethod, timeout, nil)
|
||||
}
|
||||
265
internal/infrastructure/external/shujubao/shujubao_service.go
vendored
Normal file
265
internal/infrastructure/external/shujubao/shujubao_service.go
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
package shujubao
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
)
|
||||
|
||||
// ShujubaoResp 数据宝 API 通用响应(按实际文档调整)
|
||||
type ShujubaoResp struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
// ShujubaoConfig 数据宝服务配置
|
||||
type ShujubaoConfig struct {
|
||||
URL string
|
||||
AppSecret string
|
||||
SignMethod SignMethod
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// ShujubaoService 数据宝服务
|
||||
type ShujubaoService struct {
|
||||
config ShujubaoConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewShujubaoService 创建数据宝服务实例
|
||||
func NewShujubaoService(url, appSecret string, signMethod SignMethod, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *ShujubaoService {
|
||||
if signMethod == "" {
|
||||
signMethod = SignMethodHMACMD5
|
||||
}
|
||||
if timeout == 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
return &ShujubaoService{
|
||||
config: ShujubaoConfig{
|
||||
URL: url,
|
||||
AppSecret: appSecret,
|
||||
SignMethod: signMethod,
|
||||
Timeout: timeout,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求 ID
|
||||
func (s *ShujubaoService) generateRequestID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, s.config.AppSecret)))
|
||||
return fmt.Sprintf("shujubao_%x", hash[:8])
|
||||
}
|
||||
|
||||
// buildSortedParamStr 将入参按 key 的 ASCII 排序组合为 key1=value1&key2=value2&...
|
||||
func buildSortedParamStr(params map[string]interface{}) string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte('&')
|
||||
}
|
||||
v := params[k]
|
||||
var vs string
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
vs = val
|
||||
case nil:
|
||||
vs = ""
|
||||
default:
|
||||
vs = fmt.Sprint(val)
|
||||
}
|
||||
b.WriteString(k)
|
||||
b.WriteByte('=')
|
||||
b.WriteString(vs)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// buildFormUrlEncodedBody 按 key 的 ASCII 排序构建 application/x-www-form-urlencoded 请求体(键与值均已 URL 编码)
|
||||
func buildFormUrlEncodedBody(params map[string]interface{}) string {
|
||||
if len(params) == 0 {
|
||||
return ""
|
||||
}
|
||||
keys := make([]string, 0, len(params))
|
||||
for k := range params {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var b strings.Builder
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
b.WriteByte('&')
|
||||
}
|
||||
v := params[k]
|
||||
var vs string
|
||||
switch val := v.(type) {
|
||||
case string:
|
||||
vs = val
|
||||
case nil:
|
||||
vs = ""
|
||||
default:
|
||||
vs = fmt.Sprint(val)
|
||||
}
|
||||
b.WriteString(url.QueryEscape(k))
|
||||
b.WriteByte('=')
|
||||
b.WriteString(url.QueryEscape(vs))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// generateSign 根据入参与时间戳生成签名。入参按 ASCII 排序组合后与 app_secret 做 MD5/HMAC。
|
||||
// 对于开启了加密的接口需传 sign 与 timestamp;明文传输的接口则无需传这两个参数。
|
||||
func (s *ShujubaoService) generateSign(timestamp string, params map[string]interface{}) string {
|
||||
// 合并 timestamp 到入参后参与排序
|
||||
merged := make(map[string]interface{}, len(params)+1)
|
||||
for k, v := range params {
|
||||
merged[k] = v
|
||||
}
|
||||
merged["timestamp"] = timestamp
|
||||
sortedStr := buildSortedParamStr(merged)
|
||||
switch s.config.SignMethod {
|
||||
case SignMethodMD5:
|
||||
return GenerateSignFromParamsMD5(s.config.AppSecret, sortedStr)
|
||||
default:
|
||||
return GenerateSignFromParamsHMAC(s.config.AppSecret, sortedStr)
|
||||
}
|
||||
}
|
||||
|
||||
// buildRequestURL 拼接接口地址得到最终请求 URL,如 https://api.chinadatapay.com/communication/personal/197
|
||||
func (s *ShujubaoService) buildRequestURL(apiPath string) string {
|
||||
base := strings.TrimSuffix(s.config.URL, "/")
|
||||
if apiPath == "" {
|
||||
return base
|
||||
}
|
||||
return base + "/" + strings.TrimPrefix(apiPath, "/")
|
||||
}
|
||||
|
||||
// CallAPI 调用数据宝 API(POST)。最终请求地址 = url + 拼接接口地址值;body 为业务参数;sign、timestamp 按原样传 header。
|
||||
func (s *ShujubaoService) CallAPI(ctx context.Context, apiPath string, params map[string]interface{}) (data interface{}, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := s.generateRequestID()
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
|
||||
// 最终请求 URL = https://api.chinadatapay.com/communication + 拼接接口地址值,如 /personal/197
|
||||
requestURL := s.buildRequestURL(apiPath)
|
||||
|
||||
var transactionID string
|
||||
if id, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = id
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.LogRequest(requestID, transactionID, apiPath, requestURL)
|
||||
}
|
||||
|
||||
// 使用 application/x-www-form-urlencoded,贵司接口暂不支持 JSON 入参
|
||||
formBody := buildFormUrlEncodedBody(params)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", requestURL, strings.NewReader(formBody))
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("timestamp", timestamp)
|
||||
req.Header.Set("sign", s.generateSign(timestamp, params))
|
||||
|
||||
client := &http.Client{Timeout: s.config.Timeout}
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
isTimeout := false
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
isTimeout = true
|
||||
} else if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
isTimeout = true
|
||||
} else if errStr := err.Error(); errStr == "context deadline exceeded" ||
|
||||
errStr == "timeout" ||
|
||||
errStr == "Client.Timeout exceeded" ||
|
||||
errStr == "net/http: request canceled" {
|
||||
isTimeout = true
|
||||
}
|
||||
if isTimeout {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", err))
|
||||
} else {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
}
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
duration := time.Since(startTime)
|
||||
s.logger.LogResponse(requestID, transactionID, apiPath, response.StatusCode, duration)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", response.StatusCode))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var shujubaoResp ShujubaoResp
|
||||
if err := json.Unmarshal(respBody, &shujubaoResp); err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
code := shujubaoResp.Code
|
||||
if code != "10000" && code != "10006" {
|
||||
shujubaoErr := NewShujubaoErrorFromCode(code, shujubaoResp.Message)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, shujubaoErr, params)
|
||||
}
|
||||
return nil, errors.Join(ErrDatasource, shujubaoErr)
|
||||
}
|
||||
|
||||
return shujubaoResp.Data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user