add JRZQ09J8、FLXGDEA8、FLXGDEA9、JRZQ1D09
add external_services log
This commit is contained in:
63
internal/infrastructure/external/westdex/westdex_factory.go
vendored
Normal file
63
internal/infrastructure/external/westdex/westdex_factory.go
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
package westdex
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewWestDexServiceWithConfig 使用配置创建西部数据服务
|
||||
func NewWestDexServiceWithConfig(cfg *config.Config) (*WestDexService, error) {
|
||||
// 将配置类型转换为通用外部服务日志配置
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.WestDex.Logging.Enabled,
|
||||
LogDir: cfg.WestDex.Logging.LogDir,
|
||||
ServiceName: "westdex",
|
||||
UseDaily: cfg.WestDex.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.WestDex.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
|
||||
// 转换级别配置
|
||||
for key, value := range cfg.WestDex.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
|
||||
}
|
||||
|
||||
// 创建西部数据服务
|
||||
service := NewWestDexService(
|
||||
cfg.WestDex.URL,
|
||||
cfg.WestDex.Key,
|
||||
cfg.WestDex.SecretID,
|
||||
cfg.WestDex.SecretSecondID,
|
||||
logger,
|
||||
)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// NewWestDexServiceWithLogging 使用自定义日志配置创建西部数据服务
|
||||
func NewWestDexServiceWithLogging(url, key, secretID, secretSecondID string, loggingConfig external_logger.ExternalServiceLoggingConfig) (*WestDexService, error) {
|
||||
// 设置服务名称
|
||||
loggingConfig.ServiceName = "westdex"
|
||||
|
||||
// 创建通用外部服务日志器
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建西部数据服务
|
||||
service := NewWestDexService(url, key, secretID, secretSecondID, logger)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
@@ -2,15 +2,18 @@ package westdex
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/crypto"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -26,6 +29,7 @@ type WestResp struct {
|
||||
ErrorCode *int `json:"error_code"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type G05HZ01WestResp struct {
|
||||
Message string `json:"message"`
|
||||
Code string `json:"code"`
|
||||
@@ -38,43 +42,91 @@ type G05HZ01WestResp struct {
|
||||
type WestConfig struct {
|
||||
Url string
|
||||
Key string
|
||||
SecretId string
|
||||
SecretSecondId string
|
||||
SecretID string
|
||||
SecretSecondID string
|
||||
}
|
||||
|
||||
type WestDexService struct {
|
||||
config WestConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewWestDexService 是一个构造函数,用于初始化 WestDexService
|
||||
func NewWestDexService(url, key, secretId, secretSecondId string) *WestDexService {
|
||||
func NewWestDexService(url, key, secretID, secretSecondID string, logger *external_logger.ExternalServiceLogger) *WestDexService {
|
||||
return &WestDexService{
|
||||
config: WestConfig{
|
||||
Url: url,
|
||||
Key: key,
|
||||
SecretId: secretId,
|
||||
SecretSecondId: secretSecondId,
|
||||
SecretID: secretID,
|
||||
SecretSecondID: secretSecondID,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CallAPI 调用西部数据的 API
|
||||
func (w *WestDexService) CallAPI(code string, reqData map[string]interface{}) (resp []byte, err error) {
|
||||
// 生成当前的13位时间戳
|
||||
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
||||
// generateRequestID 生成请求ID
|
||||
func (w *WestDexService) generateRequestID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, w.config.Key)))
|
||||
return fmt.Sprintf("westdex_%x", hash[:8])
|
||||
}
|
||||
|
||||
// 构造请求URL
|
||||
reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretId, code, timestamp)
|
||||
// buildLogData 构建包含transactionId的日志数据
|
||||
func (w *WestDexService) buildLogData(data map[string]interface{}, transactionID string) map[string]interface{} {
|
||||
if transactionID == "" {
|
||||
return data
|
||||
}
|
||||
|
||||
logData := data
|
||||
if logData == nil {
|
||||
logData = make(map[string]interface{})
|
||||
}
|
||||
logData["transaction_id"] = transactionID
|
||||
return logData
|
||||
}
|
||||
|
||||
// buildRequestURL 构建请求URL
|
||||
func (w *WestDexService) buildRequestURL(code string) string {
|
||||
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
||||
return fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretID, code, timestamp)
|
||||
}
|
||||
|
||||
// CallAPI 调用西部数据的 API
|
||||
func (w *WestDexService) CallAPI(ctx context.Context, code string, reqData map[string]interface{}) (resp []byte, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := w.generateRequestID()
|
||||
|
||||
// 从ctx中获取transactionId
|
||||
var transactionID string
|
||||
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = ctxTransactionID
|
||||
}
|
||||
|
||||
// 构建请求URL
|
||||
reqUrl := w.buildRequestURL(code)
|
||||
|
||||
// 记录请求日志
|
||||
if w.logger != nil {
|
||||
w.logger.LogRequest(requestID, code, reqUrl, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
|
||||
jsonData, marshalErr := json.Marshal(reqData)
|
||||
if marshalErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建HTTP POST请求
|
||||
req, newRequestErr := http.NewRequest("POST", reqUrl, bytes.NewBuffer(jsonData))
|
||||
req, newRequestErr := http.NewRequestWithContext(ctx, "POST", reqUrl, bytes.NewBuffer(jsonData))
|
||||
if newRequestErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, newRequestErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, newRequestErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
@@ -84,70 +136,150 @@ func (w *WestDexService) CallAPI(code string, reqData map[string]interface{}) (r
|
||||
client := &http.Client{}
|
||||
httpResp, clientDoErr := client.Do(req)
|
||||
if clientDoErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, clientDoErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, clientDoErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
closeErr := Body.Close()
|
||||
if closeErr != nil {
|
||||
// 记录关闭错误
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, fmt.Errorf("关闭响应体失败: %w", closeErr), w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
}
|
||||
}(httpResp.Body)
|
||||
|
||||
// 计算请求耗时
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// 检查请求是否成功
|
||||
if httpResp.StatusCode == 200 {
|
||||
// 读取响应体
|
||||
bodyBytes, ReadErr := io.ReadAll(httpResp.Body)
|
||||
if ReadErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, ReadErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, ReadErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录响应日志
|
||||
if w.logger != nil {
|
||||
w.logger.LogResponse(requestID, code, httpResp.StatusCode, bodyBytes, duration)
|
||||
}
|
||||
|
||||
// 手动调用 json.Unmarshal 触发自定义的 UnmarshalJSON 方法
|
||||
var westDexResp WestResp
|
||||
log.Println("westDexResp.ID", westDexResp.ID)
|
||||
UnmarshalErr := json.Unmarshal(bodyBytes, &westDexResp)
|
||||
if UnmarshalErr != nil {
|
||||
return nil, UnmarshalErr
|
||||
err = UnmarshalErr
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if westDexResp.Code != "00000" && westDexResp.Code != "200" && westDexResp.Code != "0" {
|
||||
if westDexResp.Data == "" {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
decryptedData, DecryptErr := crypto.WestDexDecrypt(westDexResp.Data, w.config.Key)
|
||||
if DecryptErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, DecryptErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, DecryptErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录业务错误日志
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, fmt.Errorf("%w: %s", ErrDatasource, westDexResp.Message), w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
|
||||
// 记录性能日志(失败)
|
||||
// 注意:通用日志系统不包含性能日志功能
|
||||
|
||||
return decryptedData, fmt.Errorf("%w: %s", ErrDatasource, westDexResp.Message)
|
||||
}
|
||||
|
||||
if westDexResp.Data == "" {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decryptedData, DecryptErr := crypto.WestDexDecrypt(westDexResp.Data, w.config.Key)
|
||||
if DecryptErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, DecryptErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, DecryptErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录性能日志(成功)
|
||||
// 注意:通用日志系统不包含性能日志功能
|
||||
|
||||
return decryptedData, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("%w: 西部请求失败Code: %d", ErrSystem, httpResp.StatusCode)
|
||||
// 记录HTTP错误
|
||||
err = fmt.Errorf("%w: 西部请求失败Code: %d", ErrSystem, httpResp.StatusCode)
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
// 注意:通用日志系统不包含性能日志功能
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// G05HZ01CallAPI 调用西部数据的 G05HZ01 API
|
||||
func (w *WestDexService) G05HZ01CallAPI(code string, reqData map[string]interface{}) (resp []byte, err error) {
|
||||
// 生成当前的13位时间戳
|
||||
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
||||
func (w *WestDexService) G05HZ01CallAPI(ctx context.Context, code string, reqData map[string]interface{}) (resp []byte, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := w.generateRequestID()
|
||||
|
||||
// 从ctx中获取transactionId
|
||||
var transactionID string
|
||||
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = ctxTransactionID
|
||||
}
|
||||
|
||||
// 构造请求URL
|
||||
reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%s", w.config.Url, w.config.SecretSecondId, code, timestamp)
|
||||
// 构建请求URL
|
||||
reqUrl := fmt.Sprintf("%s/%s/%s?timestamp=%d", w.config.Url, w.config.SecretSecondID, code, time.Now().UnixNano()/int64(time.Millisecond))
|
||||
|
||||
// 记录请求日志
|
||||
if w.logger != nil {
|
||||
w.logger.LogRequest(requestID, code, reqUrl, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
|
||||
jsonData, marshalErr := json.Marshal(reqData)
|
||||
if marshalErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建HTTP POST请求
|
||||
req, newRequestErr := http.NewRequest("POST", reqUrl, bytes.NewBuffer(jsonData))
|
||||
req, newRequestErr := http.NewRequestWithContext(ctx, "POST", reqUrl, bytes.NewBuffer(jsonData))
|
||||
if newRequestErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, newRequestErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, newRequestErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
@@ -157,38 +289,90 @@ func (w *WestDexService) G05HZ01CallAPI(code string, reqData map[string]interfac
|
||||
client := &http.Client{}
|
||||
httpResp, clientDoErr := client.Do(req)
|
||||
if clientDoErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, clientDoErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, clientDoErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
closeErr := Body.Close()
|
||||
if closeErr != nil {
|
||||
// 忽略
|
||||
// 记录关闭错误
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, fmt.Errorf("关闭响应体失败: %w", closeErr), w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
}
|
||||
}(httpResp.Body)
|
||||
|
||||
// 计算请求耗时
|
||||
duration := time.Since(startTime)
|
||||
|
||||
if httpResp.StatusCode == 200 {
|
||||
bodyBytes, ReadErr := io.ReadAll(httpResp.Body)
|
||||
if ReadErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, ReadErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, ReadErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录响应日志
|
||||
if w.logger != nil {
|
||||
w.logger.LogResponse(requestID, code, httpResp.StatusCode, bodyBytes, duration)
|
||||
}
|
||||
|
||||
var westDexResp G05HZ01WestResp
|
||||
UnmarshalErr := json.Unmarshal(bodyBytes, &westDexResp)
|
||||
if UnmarshalErr != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, UnmarshalErr.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, UnmarshalErr.Error())
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if westDexResp.Code != "0000" {
|
||||
if westDexResp.Data == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
// 记录业务错误日志
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, fmt.Errorf("%w: %s", ErrSystem, string(westDexResp.Data)), w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
|
||||
// 记录性能日志(失败)
|
||||
// 注意:通用日志系统不包含性能日志功能
|
||||
|
||||
return westDexResp.Data, fmt.Errorf("%w: %s", ErrSystem, string(westDexResp.Data))
|
||||
}
|
||||
}
|
||||
|
||||
if westDexResp.Data == nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, westDexResp.Message)
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录性能日志(成功)
|
||||
// 注意:通用日志系统不包含性能日志功能
|
||||
|
||||
return westDexResp.Data, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("%w: 西部请求失败Code: %d", ErrSystem, httpResp.StatusCode)
|
||||
// 记录HTTP错误
|
||||
err = fmt.Errorf("%w: 西部请求失败Code: %d", ErrSystem, httpResp.StatusCode)
|
||||
if w.logger != nil {
|
||||
w.logger.LogError(requestID, code, err, w.buildLogData(reqData, transactionID))
|
||||
// 注意:通用日志系统不包含性能日志功能
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,12 +381,14 @@ func (w *WestDexService) Encrypt(data string) (string, error) {
|
||||
if err != nil {
|
||||
return "", ErrSystem
|
||||
}
|
||||
|
||||
return encryptedValue, nil
|
||||
}
|
||||
func (w *WestDexService) Md5Encrypt(data string) string {
|
||||
return Md5Encrypt(data)
|
||||
result := Md5Encrypt(data)
|
||||
return result
|
||||
}
|
||||
|
||||
func (w *WestDexService) GetConfig() WestConfig {
|
||||
return w.config
|
||||
}
|
||||
}
|
||||
|
||||
67
internal/infrastructure/external/yushan/yushan_factory.go
vendored
Normal file
67
internal/infrastructure/external/yushan/yushan_factory.go
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
package yushan
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewYushanServiceWithConfig 使用配置创建羽山服务
|
||||
func NewYushanServiceWithConfig(cfg *config.Config) (*YushanService, error) {
|
||||
// 将配置类型转换为通用外部服务日志配置
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Yushan.Logging.Enabled,
|
||||
LogDir: cfg.Yushan.Logging.LogDir,
|
||||
ServiceName: "yushan",
|
||||
UseDaily: cfg.Yushan.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Yushan.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
|
||||
// 转换级别配置
|
||||
for key, value := range cfg.Yushan.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
|
||||
}
|
||||
|
||||
// 创建羽山服务
|
||||
service := NewYushanService(
|
||||
cfg.Yushan.URL,
|
||||
cfg.Yushan.APIKey,
|
||||
cfg.Yushan.AcctID,
|
||||
logger,
|
||||
)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// NewYushanServiceWithLogging 使用自定义日志配置创建羽山服务
|
||||
func NewYushanServiceWithLogging(url, apiKey, acctID string, loggingConfig external_logger.ExternalServiceLoggingConfig) (*YushanService, error) {
|
||||
// 设置服务名称
|
||||
loggingConfig.ServiceName = "yushan"
|
||||
|
||||
// 创建通用外部服务日志器
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建羽山服务
|
||||
service := NewYushanService(url, apiKey, acctID, logger)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// NewYushanServiceSimple 创建简单的羽山服务(无日志)
|
||||
func NewYushanServiceSimple(url, apiKey, acctID string) *YushanService {
|
||||
return NewYushanService(url, apiKey, acctID, nil)
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package yushan
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
@@ -15,6 +17,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
@@ -32,21 +36,37 @@ type YushanConfig struct {
|
||||
|
||||
type YushanService struct {
|
||||
config YushanConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewWestDexService 是一个构造函数,用于初始化 WestDexService
|
||||
func NewYushanService(url, apiKey, acctID string) *YushanService {
|
||||
// NewYushanService 是一个构造函数,用于初始化 YushanService
|
||||
func NewYushanService(url, apiKey, acctID string, logger *external_logger.ExternalServiceLogger) *YushanService {
|
||||
return &YushanService{
|
||||
config: YushanConfig{
|
||||
URL: url,
|
||||
ApiKey: apiKey,
|
||||
AcctID: acctID,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// CallAPI 调用西部数据的 API
|
||||
func (y *YushanService) CallAPI(code string, params map[string]interface{}) (respBytes []byte, err error) {
|
||||
// CallAPI 调用羽山数据的 API
|
||||
func (y *YushanService) CallAPI(ctx context.Context, code string, params map[string]interface{}) (respBytes []byte, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := y.generateRequestID()
|
||||
|
||||
// 从ctx中获取transactionId
|
||||
var transactionID string
|
||||
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = ctxTransactionID
|
||||
}
|
||||
|
||||
// 记录请求日志
|
||||
if y.logger != nil {
|
||||
y.logger.LogRequest(requestID, code, y.config.URL, y.buildLogData(params, transactionID))
|
||||
}
|
||||
|
||||
// 获取当前时间戳
|
||||
unixMilliseconds := time.Now().UnixNano() / int64(time.Millisecond)
|
||||
|
||||
@@ -64,13 +84,21 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
|
||||
// 将请求数据转换为 JSON 字节数组
|
||||
messageBytes, err := json.Marshal(reqData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
if y.logger != nil {
|
||||
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取 API 密钥
|
||||
key, err := hex.DecodeString(y.config.ApiKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
if y.logger != nil {
|
||||
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 使用 AES CBC 加密请求数据
|
||||
@@ -80,10 +108,16 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
|
||||
content := base64.StdEncoding.EncodeToString(cipherText)
|
||||
|
||||
// 发起 HTTP 请求
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", y.config.URL, strings.NewReader(content))
|
||||
client := &http.Client{
|
||||
Timeout: 20 * time.Second,
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", y.config.URL, strings.NewReader(content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
if y.logger != nil {
|
||||
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("ACCT_ID", y.config.AcctID)
|
||||
@@ -91,13 +125,20 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
|
||||
// 执行请求
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
if y.logger != nil {
|
||||
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
if y.logger != nil {
|
||||
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -108,12 +149,22 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
|
||||
} else {
|
||||
sDec, err := base64.StdEncoding.DecodeString(string(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
if y.logger != nil {
|
||||
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
respData = y.AES_CBC_Decrypt(sDec, key)
|
||||
}
|
||||
retCode := gjson.GetBytes(respData, "retcode").String()
|
||||
|
||||
// 记录响应日志
|
||||
if y.logger != nil {
|
||||
duration := time.Since(startTime)
|
||||
y.logger.LogResponse(requestID, code, resp.StatusCode, respData, duration)
|
||||
}
|
||||
|
||||
if retCode == "100000" {
|
||||
// retcode 为 100000,表示查询为空
|
||||
return nil, ErrNotFound
|
||||
@@ -121,13 +172,41 @@ func (y *YushanService) CallAPI(code string, params map[string]interface{}) (res
|
||||
// retcode 为 000000,表示有数据,返回 retdata
|
||||
retData := gjson.GetBytes(respData, "retdata")
|
||||
if !retData.Exists() {
|
||||
return nil, fmt.Errorf("%w: %s", ErrDatasource, "羽山请求retdata为空")
|
||||
err = fmt.Errorf("%w: %s", ErrDatasource, "羽山请求retdata为空")
|
||||
if y.logger != nil {
|
||||
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return []byte(retData.Raw), nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("%w: %s", ErrDatasource, "羽山请求未知的状态码")
|
||||
err = fmt.Errorf("%w: %s", ErrDatasource, "羽山请求未知的状态码")
|
||||
if y.logger != nil {
|
||||
y.logger.LogError(requestID, code, err, y.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求ID
|
||||
func (y *YushanService) generateRequestID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, y.config.ApiKey)))
|
||||
return fmt.Sprintf("yushan_%x", hash[:8])
|
||||
}
|
||||
|
||||
// buildLogData 构建包含transactionId的日志数据
|
||||
func (y *YushanService) buildLogData(data map[string]interface{}, transactionID string) map[string]interface{} {
|
||||
if transactionID == "" {
|
||||
return data
|
||||
}
|
||||
|
||||
logData := data
|
||||
if logData == nil {
|
||||
logData = make(map[string]interface{})
|
||||
}
|
||||
logData["transaction_id"] = transactionID
|
||||
return logData
|
||||
}
|
||||
|
||||
// GenerateRandomString 生成一个32位的随机字符串订单号
|
||||
|
||||
83
internal/infrastructure/external/yushan/yushan_test.go
vendored
Normal file
83
internal/infrastructure/external/yushan/yushan_test.go
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
package yushan
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGenerateRequestID(t *testing.T) {
|
||||
service := &YushanService{
|
||||
config: YushanConfig{
|
||||
ApiKey: "test_api_key_123",
|
||||
},
|
||||
}
|
||||
|
||||
id1 := service.generateRequestID()
|
||||
|
||||
// 等待一小段时间确保时间戳不同
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
id2 := service.generateRequestID()
|
||||
|
||||
if id1 == "" || id2 == "" {
|
||||
t.Error("请求ID生成失败")
|
||||
}
|
||||
|
||||
if id1 == id2 {
|
||||
t.Error("不同时间生成的请求ID应该不同")
|
||||
}
|
||||
|
||||
// 验证ID格式
|
||||
if len(id1) < 20 { // yushan_ + 8位十六进制 + 其他
|
||||
t.Errorf("请求ID长度不足,实际: %s", id1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRandomString(t *testing.T) {
|
||||
service := &YushanService{}
|
||||
|
||||
str1, err := service.GenerateRandomString()
|
||||
if err != nil {
|
||||
t.Fatalf("生成随机字符串失败: %v", err)
|
||||
}
|
||||
|
||||
str2, err := service.GenerateRandomString()
|
||||
if err != nil {
|
||||
t.Fatalf("生成随机字符串失败: %v", err)
|
||||
}
|
||||
|
||||
if str1 == "" || str2 == "" {
|
||||
t.Error("随机字符串为空")
|
||||
}
|
||||
|
||||
if str1 == str2 {
|
||||
t.Error("两次生成的随机字符串应该不同")
|
||||
}
|
||||
|
||||
// 验证长度(16字节 = 32位十六进制字符)
|
||||
if len(str1) != 32 || len(str2) != 32 {
|
||||
t.Error("随机字符串长度应该是32位")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsJSON(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"{}", true},
|
||||
{"[]", true},
|
||||
{"{\"key\": \"value\"}", true},
|
||||
{"[1, 2, 3]", true},
|
||||
{"invalid json", false},
|
||||
{"", false},
|
||||
{"{invalid}", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := IsJSON(tc.input)
|
||||
if result != tc.expected {
|
||||
t.Errorf("输入: %s, 期望: %v, 实际: %v", tc.input, tc.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
121
internal/infrastructure/external/zhicha/crypto.go
vendored
Normal file
121
internal/infrastructure/external/zhicha/crypto.go
vendored
Normal file
@@ -0,0 +1,121 @@
|
||||
package zhicha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
KEY_SIZE = 16 // AES-128, 16 bytes
|
||||
)
|
||||
|
||||
// Encrypt 使用AES-128-CBC加密数据
|
||||
// 对应Python示例中的encrypt函数
|
||||
func Encrypt(data, key string) (string, error) {
|
||||
// 将十六进制密钥转换为字节
|
||||
binKey, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("密钥格式错误: %w", err)
|
||||
}
|
||||
|
||||
if len(binKey) < KEY_SIZE {
|
||||
return "", fmt.Errorf("密钥长度不足,需要至少%d字节", KEY_SIZE)
|
||||
}
|
||||
|
||||
// 从密钥前16个字符生成IV
|
||||
iv := []byte(key[:KEY_SIZE])
|
||||
|
||||
// 创建AES加密器
|
||||
block, err := aes.NewCipher(binKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建AES加密器失败: %w", err)
|
||||
}
|
||||
|
||||
// 对数据进行PKCS7填充
|
||||
paddedData := pkcs7Padding([]byte(data), aes.BlockSize)
|
||||
|
||||
// 创建CBC模式加密器
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
|
||||
// 加密
|
||||
ciphertext := make([]byte, len(paddedData))
|
||||
mode.CryptBlocks(ciphertext, paddedData)
|
||||
|
||||
// 返回Base64编码结果
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// Decrypt 使用AES-128-CBC解密数据
|
||||
// 对应Python示例中的decrypt函数
|
||||
func Decrypt(encryptedData, key string) (string, error) {
|
||||
// 将十六进制密钥转换为字节
|
||||
binKey, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("密钥格式错误: %w", err)
|
||||
}
|
||||
|
||||
if len(binKey) < KEY_SIZE {
|
||||
return "", fmt.Errorf("密钥长度不足,需要至少%d字节", KEY_SIZE)
|
||||
}
|
||||
|
||||
// 从密钥前16个字符生成IV
|
||||
iv := []byte(key[:KEY_SIZE])
|
||||
|
||||
// 解码Base64数据
|
||||
decodedData, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Base64解码失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查数据长度是否为AES块大小的倍数
|
||||
if len(decodedData) == 0 || len(decodedData)%aes.BlockSize != 0 {
|
||||
return "", fmt.Errorf("加密数据长度无效,必须是%d字节的倍数", aes.BlockSize)
|
||||
}
|
||||
|
||||
// 创建AES解密器
|
||||
block, err := aes.NewCipher(binKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建AES解密器失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建CBC模式解密器
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
|
||||
// 解密
|
||||
plaintext := make([]byte, len(decodedData))
|
||||
mode.CryptBlocks(plaintext, decodedData)
|
||||
|
||||
// 移除PKCS7填充
|
||||
unpadded, err := pkcs7Unpadding(plaintext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("移除填充失败: %w", err)
|
||||
}
|
||||
|
||||
return string(unpadded), nil
|
||||
}
|
||||
|
||||
// pkcs7Padding 使用PKCS7填充数据
|
||||
func pkcs7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padtext...)
|
||||
}
|
||||
|
||||
// pkcs7Unpadding 移除PKCS7填充
|
||||
func pkcs7Unpadding(src []byte) ([]byte, error) {
|
||||
length := len(src)
|
||||
if length == 0 {
|
||||
return nil, fmt.Errorf("数据为空")
|
||||
}
|
||||
|
||||
unpadding := int(src[length-1])
|
||||
if unpadding > length {
|
||||
return nil, fmt.Errorf("填充长度无效")
|
||||
}
|
||||
|
||||
return src[:length-unpadding], nil
|
||||
}
|
||||
170
internal/infrastructure/external/zhicha/zhicha_errors.go
vendored
Normal file
170
internal/infrastructure/external/zhicha/zhicha_errors.go
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
package zhicha
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ZhichaError 智查金控服务错误
|
||||
type ZhichaError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error 实现error接口
|
||||
func (e *ZhichaError) Error() string {
|
||||
return fmt.Sprintf("智查金控错误 [%s]: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否成功
|
||||
func (e *ZhichaError) IsSuccess() bool {
|
||||
return e.Code == "200"
|
||||
}
|
||||
|
||||
// IsNoRecord 检查是否查询无记录
|
||||
func (e *ZhichaError) IsNoRecord() bool {
|
||||
return e.Code == "201"
|
||||
}
|
||||
|
||||
// IsBusinessError 检查是否是业务错误(非系统错误)
|
||||
func (e *ZhichaError) IsBusinessError() bool {
|
||||
return e.Code >= "302" && e.Code <= "320"
|
||||
}
|
||||
|
||||
// IsSystemError 检查是否是系统错误
|
||||
func (e *ZhichaError) IsSystemError() bool {
|
||||
return e.Code == "500"
|
||||
}
|
||||
|
||||
// IsAuthError 检查是否是认证相关错误
|
||||
func (e *ZhichaError) IsAuthError() bool {
|
||||
return e.Code == "304" || e.Code == "318" || e.Code == "319" || e.Code == "320"
|
||||
}
|
||||
|
||||
// IsParamError 检查是否是参数相关错误
|
||||
func (e *ZhichaError) IsParamError() bool {
|
||||
return e.Code == "302" || e.Code == "303" || e.Code == "305" || e.Code == "306" || e.Code == "307" || e.Code == "316" || e.Code == "317"
|
||||
}
|
||||
|
||||
// IsServiceError 检查是否是服务相关错误
|
||||
func (e *ZhichaError) IsServiceError() bool {
|
||||
return e.Code == "308" || e.Code == "309" || e.Code == "310" || e.Code == "311"
|
||||
}
|
||||
|
||||
// IsUserError 检查是否是用户相关错误
|
||||
func (e *ZhichaError) IsUserError() bool {
|
||||
return e.Code == "312" || e.Code == "313" || e.Code == "314" || e.Code == "315"
|
||||
}
|
||||
|
||||
// 预定义错误常量
|
||||
var (
|
||||
// 成功状态
|
||||
ErrSuccess = &ZhichaError{Code: "200", Message: "请求成功"}
|
||||
ErrNoRecord = &ZhichaError{Code: "201", Message: "查询无记录"}
|
||||
|
||||
// 业务参数错误
|
||||
ErrBusinessParamMissing = &ZhichaError{Code: "302", Message: "业务参数缺失"}
|
||||
ErrParamError = &ZhichaError{Code: "303", Message: "参数错误"}
|
||||
ErrHeaderParamMissing = &ZhichaError{Code: "304", Message: "请求头参数缺失"}
|
||||
ErrNameError = &ZhichaError{Code: "305", Message: "姓名错误"}
|
||||
ErrPhoneError = &ZhichaError{Code: "306", Message: "手机号错误"}
|
||||
ErrIDCardError = &ZhichaError{Code: "307", Message: "身份证号错误"}
|
||||
|
||||
// 服务相关错误
|
||||
ErrServiceNotExist = &ZhichaError{Code: "308", Message: "服务不存在"}
|
||||
ErrServiceNotEnabled = &ZhichaError{Code: "309", Message: "服务未开通"}
|
||||
ErrInsufficientBalance = &ZhichaError{Code: "310", Message: "余额不足"}
|
||||
ErrRemoteDataError = &ZhichaError{Code: "311", Message: "调用远程数据异常"}
|
||||
|
||||
// 用户相关错误
|
||||
ErrUserNotExist = &ZhichaError{Code: "312", Message: "用户不存在"}
|
||||
ErrUserStatusError = &ZhichaError{Code: "313", Message: "用户状态异常"}
|
||||
ErrUserUnauthorized = &ZhichaError{Code: "314", Message: "用户未授权"}
|
||||
ErrWhitelistError = &ZhichaError{Code: "315", Message: "白名单错误"}
|
||||
|
||||
// 时间戳和认证错误
|
||||
ErrTimestampInvalid = &ZhichaError{Code: "316", Message: "timestamp不合法"}
|
||||
ErrTimestampExpired = &ZhichaError{Code: "317", Message: "timestamp已过期"}
|
||||
ErrSignVerifyFailed = &ZhichaError{Code: "318", Message: "验签失败"}
|
||||
ErrDecryptFailed = &ZhichaError{Code: "319", Message: "解密失败"}
|
||||
ErrUnauthorized = &ZhichaError{Code: "320", Message: "未授权"}
|
||||
|
||||
// 系统错误
|
||||
ErrSystemError = &ZhichaError{Code: "500", Message: "系统异常,请联系管理员"}
|
||||
)
|
||||
|
||||
// NewZhichaError 创建新的智查金控错误
|
||||
func NewZhichaError(code, message string) *ZhichaError {
|
||||
return &ZhichaError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// NewZhichaErrorFromCode 根据状态码创建错误
|
||||
func NewZhichaErrorFromCode(code string) *ZhichaError {
|
||||
switch code {
|
||||
case "200":
|
||||
return ErrSuccess
|
||||
case "201":
|
||||
return ErrNoRecord
|
||||
case "302":
|
||||
return ErrBusinessParamMissing
|
||||
case "303":
|
||||
return ErrParamError
|
||||
case "304":
|
||||
return ErrHeaderParamMissing
|
||||
case "305":
|
||||
return ErrNameError
|
||||
case "306":
|
||||
return ErrPhoneError
|
||||
case "307":
|
||||
return ErrIDCardError
|
||||
case "308":
|
||||
return ErrServiceNotExist
|
||||
case "309":
|
||||
return ErrServiceNotEnabled
|
||||
case "310":
|
||||
return ErrInsufficientBalance
|
||||
case "311":
|
||||
return ErrRemoteDataError
|
||||
case "312":
|
||||
return ErrUserNotExist
|
||||
case "313":
|
||||
return ErrUserStatusError
|
||||
case "314":
|
||||
return ErrUserUnauthorized
|
||||
case "315":
|
||||
return ErrWhitelistError
|
||||
case "316":
|
||||
return ErrTimestampInvalid
|
||||
case "317":
|
||||
return ErrTimestampExpired
|
||||
case "318":
|
||||
return ErrSignVerifyFailed
|
||||
case "319":
|
||||
return ErrDecryptFailed
|
||||
case "320":
|
||||
return ErrUnauthorized
|
||||
case "500":
|
||||
return ErrSystemError
|
||||
default:
|
||||
return &ZhichaError{
|
||||
Code: code,
|
||||
Message: "未知错误",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsZhichaError 检查是否是智查金控错误
|
||||
func IsZhichaError(err error) bool {
|
||||
_, ok := err.(*ZhichaError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetZhichaError 获取智查金控错误
|
||||
func GetZhichaError(err error) *ZhichaError {
|
||||
if zhichaErr, ok := err.(*ZhichaError); ok {
|
||||
return zhichaErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
68
internal/infrastructure/external/zhicha/zhicha_factory.go
vendored
Normal file
68
internal/infrastructure/external/zhicha/zhicha_factory.go
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
package zhicha
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewZhichaServiceWithConfig 使用配置创建智查金控服务
|
||||
func NewZhichaServiceWithConfig(cfg *config.Config) (*ZhichaService, error) {
|
||||
// 将配置类型转换为通用外部服务日志配置
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Zhicha.Logging.Enabled,
|
||||
LogDir: cfg.Zhicha.Logging.LogDir,
|
||||
ServiceName: "zhicha",
|
||||
UseDaily: cfg.Zhicha.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Zhicha.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
|
||||
// 转换级别配置
|
||||
for key, value := range cfg.Zhicha.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
|
||||
}
|
||||
|
||||
// 创建智查金控服务
|
||||
service := NewZhichaService(
|
||||
cfg.Zhicha.URL,
|
||||
cfg.Zhicha.AppID,
|
||||
cfg.Zhicha.AppSecret,
|
||||
cfg.Zhicha.EncryptKey,
|
||||
logger,
|
||||
)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// NewZhichaServiceWithLogging 使用自定义日志配置创建智查金控服务
|
||||
func NewZhichaServiceWithLogging(url, appID, appSecret, encryptKey string, loggingConfig external_logger.ExternalServiceLoggingConfig) (*ZhichaService, error) {
|
||||
// 设置服务名称
|
||||
loggingConfig.ServiceName = "zhicha"
|
||||
|
||||
// 创建通用外部服务日志器
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建智查金控服务
|
||||
service := NewZhichaService(url, appID, appSecret, encryptKey, logger)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// NewZhichaServiceSimple 创建简单的智查金控服务(无日志)
|
||||
func NewZhichaServiceSimple(url, appID, appSecret, encryptKey string) *ZhichaService {
|
||||
return NewZhichaService(url, appID, appSecret, encryptKey, nil)
|
||||
}
|
||||
318
internal/infrastructure/external/zhicha/zhicha_service.go
vendored
Normal file
318
internal/infrastructure/external/zhicha/zhicha_service.go
vendored
Normal file
@@ -0,0 +1,318 @@
|
||||
package zhicha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
)
|
||||
|
||||
type ZhichaResp struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type ZhichaConfig struct {
|
||||
URL string
|
||||
AppID string
|
||||
AppSecret string
|
||||
EncryptKey string
|
||||
}
|
||||
|
||||
type ZhichaService struct {
|
||||
config ZhichaConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewZhichaService 是一个构造函数,用于初始化 ZhichaService
|
||||
func NewZhichaService(url, appID, appSecret, encryptKey string, logger *external_logger.ExternalServiceLogger) *ZhichaService {
|
||||
return &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: url,
|
||||
AppID: appID,
|
||||
AppSecret: appSecret,
|
||||
EncryptKey: encryptKey,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求ID
|
||||
func (z *ZhichaService) generateRequestID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, z.config.AppID)))
|
||||
return fmt.Sprintf("zhicha_%x", hash[:8])
|
||||
}
|
||||
|
||||
// buildLogData 构建包含transactionId的日志数据
|
||||
func (z *ZhichaService) buildLogData(data map[string]interface{}, transactionID string) map[string]interface{} {
|
||||
if transactionID == "" {
|
||||
return data
|
||||
}
|
||||
|
||||
logData := data
|
||||
if logData == nil {
|
||||
logData = make(map[string]interface{})
|
||||
}
|
||||
logData["transaction_id"] = transactionID
|
||||
return logData
|
||||
}
|
||||
|
||||
// generateSign 生成签名
|
||||
func (z *ZhichaService) generateSign(timestamp int64) string {
|
||||
// 第一步:对app_secret进行MD5加密
|
||||
encryptedSecret := fmt.Sprintf("%x", md5.Sum([]byte(z.config.AppSecret)))
|
||||
|
||||
// 第二步:将加密后的密钥和时间戳拼接,再次MD5加密
|
||||
signStr := encryptedSecret + strconv.FormatInt(timestamp, 10)
|
||||
sign := fmt.Sprintf("%x", md5.Sum([]byte(signStr)))
|
||||
|
||||
return sign
|
||||
}
|
||||
|
||||
// CallAPI 调用智查金控的 API
|
||||
func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[string]interface{}) (data interface{}, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := z.generateRequestID()
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
// 从ctx中获取transactionId
|
||||
var transactionID string
|
||||
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = ctxTransactionID
|
||||
}
|
||||
|
||||
// 记录请求日志
|
||||
if z.logger != nil {
|
||||
z.logger.LogRequest(requestID, "handle", z.config.URL, z.buildLogData(params, transactionID))
|
||||
}
|
||||
|
||||
jsonData, marshalErr := json.Marshal(params)
|
||||
if marshalErr != nil {
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, marshalErr.Error())
|
||||
if z.logger != nil {
|
||||
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建HTTP POST请求
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", z.config.URL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
if z.logger != nil {
|
||||
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("appId", z.config.AppID)
|
||||
req.Header.Set("proId", proID)
|
||||
req.Header.Set("timestamp", strconv.FormatInt(timestamp, 10))
|
||||
req.Header.Set("sign", z.generateSign(timestamp))
|
||||
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: 20 * time.Second,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
response, err := client.Do(req)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
if z.logger != nil {
|
||||
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
respBody, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%w: %s", ErrSystem, err.Error())
|
||||
if z.logger != nil {
|
||||
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录响应日志
|
||||
if z.logger != nil {
|
||||
duration := time.Since(startTime)
|
||||
z.logger.LogResponse(requestID, "handle", response.StatusCode, respBody, duration)
|
||||
}
|
||||
|
||||
// 检查HTTP状态码
|
||||
if response.StatusCode != http.StatusOK {
|
||||
err = fmt.Errorf("%w: HTTP状态码 %d", ErrDatasource, response.StatusCode)
|
||||
if z.logger != nil {
|
||||
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var zhichaResp ZhichaResp
|
||||
if err := json.Unmarshal(respBody, &zhichaResp); err != nil {
|
||||
err = fmt.Errorf("%w: 响应解析失败: %s", ErrSystem, err.Error())
|
||||
if z.logger != nil {
|
||||
z.logger.LogError(requestID, "handle", err, z.buildLogData(params, transactionID))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查业务状态码
|
||||
if zhichaResp.Code != "200" && zhichaResp.Code != "201" {
|
||||
// 创建智查金控错误用于日志记录
|
||||
zhichaErr := NewZhichaErrorFromCode(zhichaResp.Code)
|
||||
if zhichaErr.Code == "未知错误" {
|
||||
zhichaErr.Message = zhichaResp.Message
|
||||
}
|
||||
|
||||
// 记录智查金控的详细错误信息到日志
|
||||
if z.logger != nil {
|
||||
z.logger.LogError(requestID, "handle", zhichaErr, z.buildLogData(params, transactionID))
|
||||
}
|
||||
|
||||
// 对外统一返回数据源异常错误
|
||||
return nil, ErrDatasource
|
||||
}
|
||||
|
||||
// 返回data字段
|
||||
return zhichaResp.Data, nil
|
||||
}
|
||||
|
||||
// Encrypt 使用配置的加密密钥对数据进行AES-128-CBC加密
|
||||
func (z *ZhichaService) Encrypt(data string) (string, error) {
|
||||
if z.config.EncryptKey == "" {
|
||||
return "", fmt.Errorf("加密密钥未配置")
|
||||
}
|
||||
|
||||
// 将十六进制密钥转换为字节
|
||||
binKey, err := hex.DecodeString(z.config.EncryptKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("密钥格式错误: %w", err)
|
||||
}
|
||||
|
||||
if len(binKey) < 16 { // AES-128, 16 bytes
|
||||
return "", fmt.Errorf("密钥长度不足,需要至少16字节")
|
||||
}
|
||||
|
||||
// 从密钥前16个字符生成IV
|
||||
iv := []byte(z.config.EncryptKey[:16])
|
||||
|
||||
// 创建AES加密器
|
||||
block, err := aes.NewCipher(binKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建AES加密器失败: %w", err)
|
||||
}
|
||||
|
||||
// 对数据进行PKCS7填充
|
||||
paddedData := z.pkcs7Padding([]byte(data), aes.BlockSize)
|
||||
|
||||
// 创建CBC模式加密器
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
|
||||
// 加密
|
||||
ciphertext := make([]byte, len(paddedData))
|
||||
mode.CryptBlocks(ciphertext, paddedData)
|
||||
|
||||
// 返回Base64编码结果
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// Decrypt 使用配置的加密密钥对数据进行AES-128-CBC解密
|
||||
func (z *ZhichaService) Decrypt(encryptedData string) (string, error) {
|
||||
if z.config.EncryptKey == "" {
|
||||
return "", fmt.Errorf("加密密钥未配置")
|
||||
}
|
||||
|
||||
// 将十六进制密钥转换为字节
|
||||
binKey, err := hex.DecodeString(z.config.EncryptKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("密钥格式错误: %w", err)
|
||||
}
|
||||
|
||||
if len(binKey) < 16 { // AES-128, 16 bytes
|
||||
return "", fmt.Errorf("密钥长度不足,需要至少16字节")
|
||||
}
|
||||
|
||||
// 从密钥前16个字符生成IV
|
||||
iv := []byte(z.config.EncryptKey[:16])
|
||||
|
||||
// 解码Base64数据
|
||||
decodedData, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Base64解码失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查数据长度是否为AES块大小的倍数
|
||||
if len(decodedData) == 0 || len(decodedData)%aes.BlockSize != 0 {
|
||||
return "", fmt.Errorf("加密数据长度无效,必须是%d字节的倍数", aes.BlockSize)
|
||||
}
|
||||
|
||||
// 创建AES解密器
|
||||
block, err := aes.NewCipher(binKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建AES解密器失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建CBC模式解密器
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
|
||||
// 解密
|
||||
plaintext := make([]byte, len(decodedData))
|
||||
mode.CryptBlocks(plaintext, decodedData)
|
||||
|
||||
// 移除PKCS7填充
|
||||
unpadded, err := z.pkcs7Unpadding(plaintext)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("移除填充失败: %w", err)
|
||||
}
|
||||
|
||||
return string(unpadded), nil
|
||||
}
|
||||
|
||||
// pkcs7Padding 使用PKCS7填充数据
|
||||
func (z *ZhichaService) pkcs7Padding(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padtext...)
|
||||
}
|
||||
|
||||
// pkcs7Unpadding 移除PKCS7填充
|
||||
func (z *ZhichaService) pkcs7Unpadding(src []byte) ([]byte, error) {
|
||||
length := len(src)
|
||||
if length == 0 {
|
||||
return nil, fmt.Errorf("数据为空")
|
||||
}
|
||||
|
||||
unpadding := int(src[length-1])
|
||||
if unpadding > length {
|
||||
return nil, fmt.Errorf("填充长度无效")
|
||||
}
|
||||
|
||||
return src[:length-unpadding], nil
|
||||
}
|
||||
698
internal/infrastructure/external/zhicha/zhicha_test.go
vendored
Normal file
698
internal/infrastructure/external/zhicha/zhicha_test.go
vendored
Normal file
@@ -0,0 +1,698 @@
|
||||
package zhicha
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGenerateSign(t *testing.T) {
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
AppSecret: "test_secret_123",
|
||||
},
|
||||
}
|
||||
|
||||
timestamp := int64(1640995200) // 2022-01-01 00:00:00
|
||||
sign := service.generateSign(timestamp)
|
||||
|
||||
if sign == "" {
|
||||
t.Error("签名生成失败,签名为空")
|
||||
}
|
||||
|
||||
// 验证签名长度(MD5是32位十六进制)
|
||||
if len(sign) != 32 {
|
||||
t.Errorf("签名长度错误,期望32位,实际%d位", len(sign))
|
||||
}
|
||||
|
||||
// 验证相同参数生成相同签名
|
||||
sign2 := service.generateSign(timestamp)
|
||||
if sign != sign2 {
|
||||
t.Error("相同参数生成的签名不一致")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecrypt(t *testing.T) {
|
||||
// 测试密钥(32位十六进制)
|
||||
key := "1234567890abcdef1234567890abcdef"
|
||||
|
||||
// 测试数据
|
||||
testData := "这是一个测试数据,包含中文和English"
|
||||
|
||||
// 加密
|
||||
encrypted, err := Encrypt(testData, key)
|
||||
if err != nil {
|
||||
t.Fatalf("加密失败: %v", err)
|
||||
}
|
||||
|
||||
if encrypted == "" {
|
||||
t.Error("加密结果为空")
|
||||
}
|
||||
|
||||
// 解密
|
||||
decrypted, err := Decrypt(encrypted, key)
|
||||
if err != nil {
|
||||
t.Fatalf("解密失败: %v", err)
|
||||
}
|
||||
|
||||
if decrypted != testData {
|
||||
t.Errorf("解密结果不匹配,期望: %s, 实际: %s", testData, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptWithInvalidKey(t *testing.T) {
|
||||
// 测试无效密钥
|
||||
invalidKeys := []string{
|
||||
"", // 空密钥
|
||||
"123", // 太短
|
||||
"invalid_key_string", // 非十六进制
|
||||
"1234567890abcdef", // 16位,不足32位
|
||||
}
|
||||
|
||||
testData := "test data"
|
||||
|
||||
for _, key := range invalidKeys {
|
||||
_, err := Encrypt(testData, key)
|
||||
if err == nil {
|
||||
t.Errorf("使用无效密钥 %s 应该返回错误", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptWithInvalidData(t *testing.T) {
|
||||
key := "1234567890abcdef1234567890abcdef"
|
||||
|
||||
// 测试无效的加密数据
|
||||
invalidData := []string{
|
||||
"", // 空数据
|
||||
"invalid_base64", // 无效的Base64
|
||||
"dGVzdA==", // 有效的Base64但不是AES加密数据
|
||||
}
|
||||
|
||||
for _, data := range invalidData {
|
||||
_, err := Decrypt(data, key)
|
||||
if err == nil {
|
||||
t.Errorf("使用无效数据 %s 应该返回错误", data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPKCS7Padding(t *testing.T) {
|
||||
testCases := []struct {
|
||||
input string
|
||||
blockSize int
|
||||
expected int
|
||||
}{
|
||||
{"", 16, 16},
|
||||
{"a", 16, 16},
|
||||
{"ab", 16, 16},
|
||||
{"abc", 16, 16},
|
||||
{"abcd", 16, 16},
|
||||
{"abcde", 16, 16},
|
||||
{"abcdef", 16, 16},
|
||||
{"abcdefg", 16, 16},
|
||||
{"abcdefgh", 16, 16},
|
||||
{"abcdefghi", 16, 16},
|
||||
{"abcdefghij", 16, 16},
|
||||
{"abcdefghijk", 16, 16},
|
||||
{"abcdefghijkl", 16, 16},
|
||||
{"abcdefghijklm", 16, 16},
|
||||
{"abcdefghijklmn", 16, 16},
|
||||
{"abcdefghijklmno", 16, 16},
|
||||
{"abcdefghijklmnop", 16, 16},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
padded := pkcs7Padding([]byte(tc.input), tc.blockSize)
|
||||
if len(padded)%tc.blockSize != 0 {
|
||||
t.Errorf("输入: %s, 期望块大小倍数,实际: %d", tc.input, len(padded))
|
||||
}
|
||||
|
||||
// 测试移除填充
|
||||
unpadded, err := pkcs7Unpadding(padded)
|
||||
if err != nil {
|
||||
t.Errorf("移除填充失败: %v", err)
|
||||
}
|
||||
|
||||
if string(unpadded) != tc.input {
|
||||
t.Errorf("输入: %s, 期望: %s, 实际: %s", tc.input, tc.input, string(unpadded))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateRequestID(t *testing.T) {
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
AppID: "test_app_id",
|
||||
},
|
||||
}
|
||||
|
||||
id1 := service.generateRequestID()
|
||||
|
||||
// 等待一小段时间确保时间戳不同
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
id2 := service.generateRequestID()
|
||||
|
||||
if id1 == "" || id2 == "" {
|
||||
t.Error("请求ID生成失败")
|
||||
}
|
||||
|
||||
if id1 == id2 {
|
||||
t.Error("不同时间生成的请求ID应该不同")
|
||||
}
|
||||
|
||||
// 验证ID格式
|
||||
if len(id1) < 20 { // zhicha_ + 8位十六进制 + 其他
|
||||
t.Errorf("请求ID长度不足,实际: %s", id1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallAPISuccess(t *testing.T) {
|
||||
// 创建测试服务
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: "http://proxy.tianyuanapi.com/dataMiddle/api/handle",
|
||||
AppID: "4b78fff61ab8426f",
|
||||
AppSecret: "1128f01b94124ae899c2e9f2b1f37681",
|
||||
EncryptKey: "af4ca0098e6a202a5c08c413ebd9fd62",
|
||||
},
|
||||
logger: nil, // 测试时不使用日志
|
||||
}
|
||||
|
||||
// 测试参数
|
||||
idCardEncrypted, err := service.Encrypt("45212220000827423X")
|
||||
if err != nil {
|
||||
t.Fatalf("加密身份证号失败: %v", err)
|
||||
}
|
||||
nameEncrypted, err := service.Encrypt("张荣宏")
|
||||
if err != nil {
|
||||
t.Fatalf("加密姓名失败: %v", err)
|
||||
}
|
||||
params := map[string]interface{}{
|
||||
"idCard": idCardEncrypted,
|
||||
"name": nameEncrypted,
|
||||
"authorized": "1",
|
||||
}
|
||||
|
||||
// 创建带超时的context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 调用API
|
||||
data, err := service.CallAPI(ctx, "ZCI001", params)
|
||||
|
||||
// 注意:这是真实API调用,可能会因为网络、认证等原因失败
|
||||
// 我们主要测试方法调用是否正常,不强制要求API返回成功
|
||||
if err != nil {
|
||||
// 如果是网络错误或认证错误,这是正常的
|
||||
t.Logf("API调用返回错误: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果成功,验证响应
|
||||
if data == nil {
|
||||
t.Error("响应数据为空")
|
||||
return
|
||||
}
|
||||
|
||||
// 将data转换为字符串进行显示
|
||||
var dataStr string
|
||||
if str, ok := data.(string); ok {
|
||||
dataStr = str
|
||||
} else {
|
||||
// 如果不是字符串,尝试JSON序列化
|
||||
if dataBytes, err := json.Marshal(data); err == nil {
|
||||
dataStr = string(dataBytes)
|
||||
} else {
|
||||
dataStr = fmt.Sprintf("%v", data)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("API调用成功,响应内容: %s", dataStr)
|
||||
}
|
||||
|
||||
func TestCallAPIWithInvalidURL(t *testing.T) {
|
||||
// 创建使用无效URL的服务
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: "https://invalid-url-that-does-not-exist.com/api",
|
||||
AppID: "test_app_id",
|
||||
AppSecret: "test_app_secret",
|
||||
EncryptKey: "test_encrypt_key",
|
||||
},
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"test": "data",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 应该返回错误
|
||||
_, err := service.CallAPI(ctx, "test_pro_id", params)
|
||||
if err == nil {
|
||||
t.Error("使用无效URL应该返回错误")
|
||||
}
|
||||
|
||||
t.Logf("预期的错误: %v", err)
|
||||
}
|
||||
|
||||
func TestCallAPIWithContextCancellation(t *testing.T) {
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: "https://www.zhichajinkong.com/dataMiddle/api/handle",
|
||||
AppID: "4b78fff61ab8426f",
|
||||
AppSecret: "1128f01b94124ae899c2e9f2b1f37681",
|
||||
EncryptKey: "af4ca0098e6a202a5c08c413ebd9fd62",
|
||||
},
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"test": "data",
|
||||
}
|
||||
|
||||
// 创建可取消的context
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// 立即取消
|
||||
cancel()
|
||||
|
||||
// 应该返回context取消错误
|
||||
_, err := service.CallAPI(ctx, "test_pro_id", params)
|
||||
if err == nil {
|
||||
t.Error("context取消后应该返回错误")
|
||||
}
|
||||
|
||||
// 检查是否是context取消错误
|
||||
if err != context.Canceled && !strings.Contains(err.Error(), "context") {
|
||||
t.Errorf("期望context相关错误,实际: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Context取消错误: %v", err)
|
||||
}
|
||||
|
||||
func TestCallAPIWithTimeout(t *testing.T) {
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: "https://www.zhichajinkong.com/dataMiddle/api/handle",
|
||||
AppID: "4b78fff61ab8426f",
|
||||
AppSecret: "1128f01b94124ae899c2e9f2b1f37681",
|
||||
EncryptKey: "af4ca0098e6a202a5c08c413ebd9fd62",
|
||||
},
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"test": "data",
|
||||
}
|
||||
|
||||
// 创建很短的超时
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
// 应该因为超时而失败
|
||||
_, err := service.CallAPI(ctx, "test_pro_id", params)
|
||||
if err == nil {
|
||||
t.Error("超时后应该返回错误")
|
||||
}
|
||||
|
||||
// 检查是否是超时错误
|
||||
if err != context.DeadlineExceeded && !strings.Contains(err.Error(), "timeout") && !strings.Contains(err.Error(), "deadline") {
|
||||
t.Errorf("期望超时相关错误,实际: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("超时错误: %v", err)
|
||||
}
|
||||
|
||||
func TestCallAPIRequestHeaders(t *testing.T) {
|
||||
// 这个测试验证请求头是否正确设置
|
||||
// 由于我们不能直接访问HTTP请求,我们通过日志或其他方式来验证
|
||||
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: "https://www.zhichajinkong.com/dataMiddle/api/handle",
|
||||
AppID: "test_app_id",
|
||||
AppSecret: "test_app_secret",
|
||||
EncryptKey: "test_encrypt_key",
|
||||
},
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
"test": "headers",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 调用API(可能会失败,但我们主要测试请求头设置)
|
||||
_, err := service.CallAPI(ctx, "test_pro_id", params)
|
||||
|
||||
// 验证签名生成是否正确
|
||||
timestamp := time.Now().Unix()
|
||||
sign := service.generateSign(timestamp)
|
||||
|
||||
if sign == "" {
|
||||
t.Error("签名生成失败")
|
||||
}
|
||||
|
||||
if len(sign) != 32 {
|
||||
t.Errorf("签名长度错误,期望32位,实际%d位", len(sign))
|
||||
}
|
||||
|
||||
t.Logf("签名生成成功: %s", sign)
|
||||
t.Logf("API调用结果: %v", err)
|
||||
}
|
||||
|
||||
func TestZhichaErrorHandling(t *testing.T) {
|
||||
// 测试核心错误类型
|
||||
testCases := []struct {
|
||||
name string
|
||||
code string
|
||||
message string
|
||||
expectedErr *ZhichaError
|
||||
}{
|
||||
{
|
||||
name: "成功状态",
|
||||
code: "200",
|
||||
message: "请求成功",
|
||||
expectedErr: ErrSuccess,
|
||||
},
|
||||
{
|
||||
name: "查询无记录",
|
||||
code: "201",
|
||||
message: "查询无记录",
|
||||
expectedErr: ErrNoRecord,
|
||||
},
|
||||
{
|
||||
name: "手机号错误",
|
||||
code: "306",
|
||||
message: "手机号错误",
|
||||
expectedErr: ErrPhoneError,
|
||||
},
|
||||
{
|
||||
name: "姓名错误",
|
||||
code: "305",
|
||||
message: "姓名错误",
|
||||
expectedErr: ErrNameError,
|
||||
},
|
||||
{
|
||||
name: "身份证号错误",
|
||||
code: "307",
|
||||
message: "身份证号错误",
|
||||
expectedErr: ErrIDCardError,
|
||||
},
|
||||
{
|
||||
name: "余额不足",
|
||||
code: "310",
|
||||
message: "余额不足",
|
||||
expectedErr: ErrInsufficientBalance,
|
||||
},
|
||||
{
|
||||
name: "用户不存在",
|
||||
code: "312",
|
||||
message: "用户不存在",
|
||||
expectedErr: ErrUserNotExist,
|
||||
},
|
||||
{
|
||||
name: "系统异常",
|
||||
code: "500",
|
||||
message: "系统异常,请联系管理员",
|
||||
expectedErr: ErrSystemError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// 测试从状态码创建错误
|
||||
err := NewZhichaErrorFromCode(tc.code)
|
||||
|
||||
if err.Code != tc.expectedErr.Code {
|
||||
t.Errorf("期望错误码 %s,实际 %s", tc.expectedErr.Code, err.Code)
|
||||
}
|
||||
|
||||
if err.Message != tc.expectedErr.Message {
|
||||
t.Errorf("期望错误消息 %s,实际 %s", tc.expectedErr.Message, err.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestZhichaErrorHelpers(t *testing.T) {
|
||||
// 测试错误类型判断函数
|
||||
err := NewZhichaError("302", "业务参数缺失")
|
||||
|
||||
// 测试IsZhichaError
|
||||
if !IsZhichaError(err) {
|
||||
t.Error("IsZhichaError应该返回true")
|
||||
}
|
||||
|
||||
// 测试GetZhichaError
|
||||
zhichaErr := GetZhichaError(err)
|
||||
if zhichaErr == nil {
|
||||
t.Error("GetZhichaError应该返回非nil值")
|
||||
}
|
||||
|
||||
if zhichaErr.Code != "302" {
|
||||
t.Errorf("期望错误码302,实际%s", zhichaErr.Code)
|
||||
}
|
||||
|
||||
// 测试普通错误
|
||||
normalErr := fmt.Errorf("普通错误")
|
||||
if IsZhichaError(normalErr) {
|
||||
t.Error("普通错误不应该被识别为智查金控错误")
|
||||
}
|
||||
|
||||
if GetZhichaError(normalErr) != nil {
|
||||
t.Error("普通错误的GetZhichaError应该返回nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZhichaErrorString(t *testing.T) {
|
||||
// 测试错误字符串格式
|
||||
err := NewZhichaError("304", "请求头参数缺失")
|
||||
expectedStr := "智查金控错误 [304]: 请求头参数缺失"
|
||||
|
||||
if err.Error() != expectedStr {
|
||||
t.Errorf("期望错误字符串 %s,实际 %s", expectedStr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsIsFunctionality(t *testing.T) {
|
||||
// 测试 errors.Is() 功能是否正常工作
|
||||
|
||||
// 创建各种错误
|
||||
testCases := []struct {
|
||||
name string
|
||||
err error
|
||||
expected error
|
||||
shouldMatch bool
|
||||
}{
|
||||
{
|
||||
name: "手机号错误匹配",
|
||||
err: ErrPhoneError,
|
||||
expected: ErrPhoneError,
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "姓名错误匹配",
|
||||
err: ErrNameError,
|
||||
expected: ErrNameError,
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "身份证号错误匹配",
|
||||
err: ErrIDCardError,
|
||||
expected: ErrIDCardError,
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "余额不足错误匹配",
|
||||
err: ErrInsufficientBalance,
|
||||
expected: ErrInsufficientBalance,
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "用户不存在错误匹配",
|
||||
err: ErrUserNotExist,
|
||||
expected: ErrUserNotExist,
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "系统错误匹配",
|
||||
err: ErrSystemError,
|
||||
expected: ErrSystemError,
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "不同错误不匹配",
|
||||
err: ErrPhoneError,
|
||||
expected: ErrNameError,
|
||||
shouldMatch: false,
|
||||
},
|
||||
{
|
||||
name: "手机号错误与身份证号错误不匹配",
|
||||
err: ErrPhoneError,
|
||||
expected: ErrIDCardError,
|
||||
shouldMatch: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// 使用 errors.Is() 进行判断
|
||||
if errors.Is(tc.err, tc.expected) != tc.shouldMatch {
|
||||
if tc.shouldMatch {
|
||||
t.Errorf("期望 errors.Is(%v, %v) 返回 true", tc.err, tc.expected)
|
||||
} else {
|
||||
t.Errorf("期望 errors.Is(%v, %v) 返回 false", tc.err, tc.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsIsInSwitch(t *testing.T) {
|
||||
// 测试在 switch 语句中使用 errors.Is()
|
||||
|
||||
// 模拟API调用返回手机号错误
|
||||
err := ErrPhoneError
|
||||
|
||||
// 使用 switch 语句进行错误判断
|
||||
var result string
|
||||
switch {
|
||||
case errors.Is(err, ErrSuccess):
|
||||
result = "请求成功"
|
||||
case errors.Is(err, ErrNoRecord):
|
||||
result = "查询无记录"
|
||||
case errors.Is(err, ErrPhoneError):
|
||||
result = "手机号格式错误"
|
||||
case errors.Is(err, ErrNameError):
|
||||
result = "姓名格式错误"
|
||||
case errors.Is(err, ErrIDCardError):
|
||||
result = "身份证号格式错误"
|
||||
case errors.Is(err, ErrHeaderParamMissing):
|
||||
result = "请求头参数缺失"
|
||||
case errors.Is(err, ErrInsufficientBalance):
|
||||
result = "余额不足"
|
||||
case errors.Is(err, ErrUserNotExist):
|
||||
result = "用户不存在"
|
||||
case errors.Is(err, ErrUserUnauthorized):
|
||||
result = "用户未授权"
|
||||
case errors.Is(err, ErrSystemError):
|
||||
result = "系统异常"
|
||||
default:
|
||||
result = "未知错误"
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
expected := "手机号格式错误"
|
||||
if result != expected {
|
||||
t.Errorf("期望结果 %s,实际 %s", expected, result)
|
||||
}
|
||||
|
||||
t.Logf("Switch语句错误判断结果: %s", result)
|
||||
}
|
||||
|
||||
func TestServiceEncryptDecrypt(t *testing.T) {
|
||||
// 创建测试服务
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: "https://test.com",
|
||||
AppID: "test_app_id",
|
||||
AppSecret: "test_app_secret",
|
||||
EncryptKey: "af4ca0098e6a202a5c08c413ebd9fd62",
|
||||
},
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
// 测试数据
|
||||
testData := "Hello, 智查金控!"
|
||||
|
||||
// 测试加密
|
||||
encrypted, err := service.Encrypt(testData)
|
||||
if err != nil {
|
||||
t.Fatalf("加密失败: %v", err)
|
||||
}
|
||||
|
||||
if encrypted == "" {
|
||||
t.Error("加密结果为空")
|
||||
}
|
||||
|
||||
if encrypted == testData {
|
||||
t.Error("加密结果与原文相同")
|
||||
}
|
||||
|
||||
t.Logf("原文: %s", testData)
|
||||
t.Logf("加密后: %s", encrypted)
|
||||
|
||||
// 测试解密
|
||||
decrypted, err := service.Decrypt(encrypted)
|
||||
if err != nil {
|
||||
t.Fatalf("解密失败: %v", err)
|
||||
}
|
||||
|
||||
if decrypted != testData {
|
||||
t.Errorf("解密结果不匹配,期望: %s,实际: %s", testData, decrypted)
|
||||
}
|
||||
|
||||
t.Logf("解密后: %s", decrypted)
|
||||
}
|
||||
|
||||
func TestEncryptWithoutKey(t *testing.T) {
|
||||
// 创建没有加密密钥的服务
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: "https://test.com",
|
||||
AppID: "test_app_id",
|
||||
AppSecret: "test_app_secret",
|
||||
// 没有设置 EncryptKey
|
||||
},
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
// 应该返回错误
|
||||
_, err := service.Encrypt("test data")
|
||||
if err == nil {
|
||||
t.Error("没有加密密钥时应该返回错误")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "加密密钥未配置") {
|
||||
t.Errorf("期望错误包含'加密密钥未配置',实际: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("预期的错误: %v", err)
|
||||
}
|
||||
|
||||
func TestDecryptWithoutKey(t *testing.T) {
|
||||
// 创建没有加密密钥的服务
|
||||
service := &ZhichaService{
|
||||
config: ZhichaConfig{
|
||||
URL: "https://test.com",
|
||||
AppID: "test_app_id",
|
||||
AppSecret: "test_app_secret",
|
||||
// 没有设置 EncryptKey
|
||||
},
|
||||
logger: nil,
|
||||
}
|
||||
|
||||
// 应该返回错误
|
||||
_, err := service.Decrypt("test encrypted data")
|
||||
if err == nil {
|
||||
t.Error("没有加密密钥时应该返回错误")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "加密密钥未配置") {
|
||||
t.Errorf("期望错误包含'加密密钥未配置',实际: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("预期的错误: %v", err)
|
||||
}
|
||||
Reference in New Issue
Block a user