f
This commit is contained in:
24
internal/infrastructure/external/haiyuapi/crypto.go
vendored
Normal file
24
internal/infrastructure/external/haiyuapi/crypto.go
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
package haiyuapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"tyapi-server/internal/shared/crypto"
|
||||
)
|
||||
|
||||
// EncryptParams 将业务参数序列化为 JSON 后,使用 Access Key(16进制)AES-128-CBC 加密并 Base64 编码
|
||||
func EncryptParams(params map[string]interface{}, accessKey string) (string, error) {
|
||||
plainJSON, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return crypto.AesEncrypt(plainJSON, accessKey)
|
||||
}
|
||||
|
||||
// DecryptData 解密响应 data 字段(IV+密文 Base64);空字符串视为无数据,返回 {}
|
||||
func DecryptData(encrypted, accessKey string) ([]byte, error) {
|
||||
if encrypted == "" {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
return crypto.AesDecrypt(encrypted, accessKey)
|
||||
}
|
||||
93
internal/infrastructure/external/haiyuapi/haiyuapi_errors.go
vendored
Normal file
93
internal/infrastructure/external/haiyuapi/haiyuapi_errors.go
vendored
Normal file
@@ -0,0 +1,93 @@
|
||||
package haiyuapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 海宇API平台返回码
|
||||
const (
|
||||
CodeSuccess = 0 // 业务成功
|
||||
CodeQueryEmpty = 1000 // 查询为空
|
||||
CodeSystem = 1001 // 接口异常
|
||||
CodeDecryptFail = 1002 // 参数解密失败
|
||||
CodeRequestParam = 1003 // 基础参数校验不正确
|
||||
CodeInvalidIP = 1004 // 未经授权的IP
|
||||
CodeMissingAccessID = 1005 // 缺少Access-Id
|
||||
CodeInvalidAccessID = 1006 // 未经授权的AccessId
|
||||
CodeInsufficientBalance = 1007 // 账户余额不足,无法请求
|
||||
CodeProductNotSubscribed = 1008 // 未开通此产品
|
||||
CodeBusiness = 2001 // 业务失败
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrNotFound = errors.New("查询为空")
|
||||
)
|
||||
|
||||
// haiyuapiAPIError 海宇API平台错误
|
||||
type haiyuapiAPIError struct {
|
||||
Code int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *haiyuapiAPIError) Error() string {
|
||||
return fmt.Sprintf("海宇API返回错误,code: %d,message: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// NewHaiyuapiAPIError 创建平台错误
|
||||
func NewHaiyuapiAPIError(code int, message string) *haiyuapiAPIError {
|
||||
if message == "" {
|
||||
if desc := GetPlatformCodeDesc(code); desc != "" {
|
||||
message = desc
|
||||
} else {
|
||||
message = "海宇API返回未知错误"
|
||||
}
|
||||
}
|
||||
return &haiyuapiAPIError{Code: code, Message: message}
|
||||
}
|
||||
|
||||
// platformCodeDesc 平台 code -> 官方 message
|
||||
var platformCodeDesc = map[int]string{
|
||||
CodeSuccess: "业务成功",
|
||||
CodeQueryEmpty: "查询为空",
|
||||
CodeSystem: "接口异常",
|
||||
CodeDecryptFail: "参数解密失败",
|
||||
CodeRequestParam: "基础参数校验不正确",
|
||||
CodeInvalidIP: "未经授权的IP",
|
||||
CodeMissingAccessID: "缺少Access-Id",
|
||||
CodeInvalidAccessID: "未经授权的AccessId",
|
||||
CodeInsufficientBalance: "账户余额不足,无法请求",
|
||||
CodeProductNotSubscribed: "未开通此产品",
|
||||
CodeBusiness: "业务失败",
|
||||
}
|
||||
|
||||
// GetPlatformCodeDesc 根据平台 code 获取描述
|
||||
func GetPlatformCodeDesc(code int) string {
|
||||
if desc, ok := platformCodeDesc[code]; ok {
|
||||
return desc
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetErrByCode 将海宇API code 映射为内部哨兵错误,供处理器 errors.Is 判断
|
||||
//
|
||||
// 1000 -> ErrNotFound(查询为空,可按产品约定当成功处理)
|
||||
// 1001, 1002, 1003 -> ErrSystem(接口/解密/参数校验异常)
|
||||
// 1004~1008, 2001 -> ErrDatasource(鉴权、余额、产品、业务类上游错误)
|
||||
func GetErrByCode(code int) error {
|
||||
switch code {
|
||||
case CodeSuccess:
|
||||
return nil
|
||||
case CodeQueryEmpty:
|
||||
return ErrNotFound
|
||||
case CodeSystem, CodeDecryptFail, CodeRequestParam:
|
||||
return ErrSystem
|
||||
case CodeInvalidIP, CodeMissingAccessID, CodeInvalidAccessID,
|
||||
CodeInsufficientBalance, CodeProductNotSubscribed, CodeBusiness:
|
||||
return ErrDatasource
|
||||
default:
|
||||
return ErrDatasource
|
||||
}
|
||||
}
|
||||
64
internal/infrastructure/external/haiyuapi/haiyuapi_factory.go
vendored
Normal file
64
internal/infrastructure/external/haiyuapi/haiyuapi_factory.go
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
package haiyuapi
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewHaiyuapiServiceWithConfig 使用配置创建海宇API服务
|
||||
func NewHaiyuapiServiceWithConfig(cfg *config.Config) (*HaiyuapiService, error) {
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Haiyuapi.Logging.Enabled,
|
||||
LogDir: cfg.Haiyuapi.Logging.LogDir,
|
||||
ServiceName: "haiyuapi",
|
||||
UseDaily: cfg.Haiyuapi.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Haiyuapi.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
|
||||
for level, levelCfg := range cfg.Haiyuapi.Logging.LevelConfigs {
|
||||
loggingConfig.LevelConfigs[level] = external_logger.ExternalServiceLevelFileConfig{
|
||||
MaxSize: levelCfg.MaxSize,
|
||||
MaxBackups: levelCfg.MaxBackups,
|
||||
MaxAge: levelCfg.MaxAge,
|
||||
Compress: levelCfg.Compress,
|
||||
}
|
||||
}
|
||||
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
timeout := cfg.Haiyuapi.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = defaultRequestTimeout
|
||||
}
|
||||
|
||||
return NewHaiyuapiService(
|
||||
cfg.Haiyuapi.BaseURL,
|
||||
cfg.Haiyuapi.AccessID,
|
||||
cfg.Haiyuapi.SecretKey,
|
||||
timeout,
|
||||
logger,
|
||||
), nil
|
||||
}
|
||||
|
||||
// NewHaiyuapiServiceWithLogging 使用自定义日志配置创建海宇API服务
|
||||
func NewHaiyuapiServiceWithLogging(baseURL, accessID, secretKey string, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*HaiyuapiService, error) {
|
||||
loggingConfig.ServiceName = "haiyuapi"
|
||||
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewHaiyuapiService(baseURL, accessID, secretKey, timeout, logger), nil
|
||||
}
|
||||
|
||||
// NewHaiyuapiServiceSimple 创建无日志的海宇API服务
|
||||
func NewHaiyuapiServiceSimple(baseURL, accessID, secretKey string, timeout time.Duration) *HaiyuapiService {
|
||||
return NewHaiyuapiService(baseURL, accessID, secretKey, timeout, nil)
|
||||
}
|
||||
178
internal/infrastructure/external/haiyuapi/haiyuapi_service.go
vendored
Normal file
178
internal/infrastructure/external/haiyuapi/haiyuapi_service.go
vendored
Normal file
@@ -0,0 +1,178 @@
|
||||
package haiyuapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
const defaultRequestTimeout = 60 * time.Second
|
||||
|
||||
// serviceConfig 海宇API服务运行时配置(Access Key 为 16 进制 AES-128 密钥)
|
||||
type serviceConfig struct {
|
||||
BaseURL string
|
||||
AccessID string
|
||||
SecretKey string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// HaiyuapiService 海宇API上游服务客户端
|
||||
type HaiyuapiService struct {
|
||||
config serviceConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewHaiyuapiService 创建海宇API服务实例
|
||||
func NewHaiyuapiService(baseURL, accessID, secretKey string, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *HaiyuapiService {
|
||||
if timeout <= 0 {
|
||||
timeout = defaultRequestTimeout
|
||||
}
|
||||
return &HaiyuapiService{
|
||||
config: serviceConfig{
|
||||
BaseURL: strings.TrimRight(baseURL, "/"),
|
||||
AccessID: accessID,
|
||||
SecretKey: secretKey,
|
||||
Timeout: timeout,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CallAPI 调用海宇API:apiPath 如 /api/v1/FLXGHB4F,自动拼接 base_url 与 ?t=13位毫秒时间戳,返回解密后的明文 JSON
|
||||
func (s *HaiyuapiService) CallAPI(ctx context.Context, apiPath string, params map[string]interface{}) ([]byte, error) {
|
||||
startTime := time.Now()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", time.Now().UnixNano(), s.config.SecretKey)))
|
||||
requestID := fmt.Sprintf("haiyuapi_%x", hash[:8])
|
||||
|
||||
var transactionID string
|
||||
if id, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = id
|
||||
}
|
||||
|
||||
path := apiPath
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
reqURL := s.config.BaseURL + path + "?t=" + timestamp
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.LogRequest(requestID, transactionID, apiPath, reqURL)
|
||||
}
|
||||
|
||||
encryptedData, err := EncryptParams(params, s.config.SecretKey)
|
||||
if 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
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(RequestPayload{Data: encryptedData})
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewBuffer(bodyBytes))
|
||||
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(HeaderContentType, ContentTypeJSON)
|
||||
req.Header.Set(HeaderAccessID, s.config.AccessID)
|
||||
|
||||
resp, err := (&http.Client{Timeout: s.config.Timeout}).Do(req)
|
||||
if err != nil {
|
||||
err = wrapHTTPError(err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, err)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
duration := time.Since(startTime)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("HTTP状态码 %d", resp.StatusCode))
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResp APIResponse
|
||||
if err := json.Unmarshal(respBody, &apiResp); 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
|
||||
}
|
||||
|
||||
if apiResp.Code != CodeSuccess {
|
||||
apiErr := NewHaiyuapiAPIError(apiResp.Code, apiResp.Message)
|
||||
err = errors.Join(GetErrByCode(apiResp.Code), apiErr)
|
||||
if s.logger != nil {
|
||||
s.logger.LogError(requestID, transactionID, apiPath, apiErr, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plainResp, err := DecryptData(apiResp.Data, s.config.SecretKey)
|
||||
if 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
|
||||
}
|
||||
|
||||
if s.logger != nil {
|
||||
s.logger.LogResponse(requestID, transactionID, apiPath, resp.StatusCode, duration)
|
||||
}
|
||||
|
||||
return plainResp, nil
|
||||
}
|
||||
|
||||
func wrapHTTPError(err error) error {
|
||||
if err == context.DeadlineExceeded {
|
||||
return errors.Join(ErrDatasource, err)
|
||||
}
|
||||
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
return errors.Join(ErrDatasource, err)
|
||||
}
|
||||
switch err.Error() {
|
||||
case "context deadline exceeded", "timeout", "Client.Timeout exceeded", "net/http: request canceled":
|
||||
return errors.Join(ErrDatasource, err)
|
||||
default:
|
||||
return errors.Join(ErrSystem, err)
|
||||
}
|
||||
}
|
||||
21
internal/infrastructure/external/haiyuapi/haiyuapi_types.go
vendored
Normal file
21
internal/infrastructure/external/haiyuapi/haiyuapi_types.go
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
package haiyuapi
|
||||
|
||||
// HTTP 请求头
|
||||
const (
|
||||
HeaderAccessID = "Access-Id"
|
||||
HeaderContentType = "Content-Type"
|
||||
ContentTypeJSON = "application/json"
|
||||
)
|
||||
|
||||
// APIResponse 海宇API公共响应(data 为 AES 加密后的 Base64 字符串)
|
||||
type APIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
TransactionID string `json:"transaction_id"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// RequestPayload 海宇API请求体(业务参数加密后置于 data)
|
||||
type RequestPayload struct {
|
||||
Data string `json:"data"`
|
||||
}
|
||||
Reference in New Issue
Block a user