Files
tyapi-server/internal/infrastructure/external/shumai/shumai_service.go
2026-01-23 17:53:11 +08:00

280 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package shumai
import (
"context"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"tyapi-server/internal/shared/external_logger"
)
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
ErrNotFound = errors.New("查询为空")
)
// ShumaiResponse 数脉 API 通用响应(占位,按实际文档调整)
type ShumaiResponse struct {
Code int `json:"code"` // 状态码
Msg string `json:"msg"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// ShumaiConfig 数脉服务配置
type ShumaiConfig struct {
URL string
AppID string
AppSecret string
AppID2 string // 走政务接口使用这个
AppSecret2 string // 走政务接口使用这个
SignMethod SignMethod
Timeout time.Duration
}
// ShumaiService 数脉服务
type ShumaiService struct {
config ShumaiConfig
logger *external_logger.ExternalServiceLogger
useGovernment bool // 是否使用政务接口app_id2
}
// NewShumaiService 创建数脉服务实例
// appID2 和 appSecret2 用于政务接口,如果为空则只使用普通接口
func NewShumaiService(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, logger *external_logger.ExternalServiceLogger, appID2, appSecret2 string) *ShumaiService {
if signMethod == "" {
signMethod = SignMethodHMACMD5
}
if timeout == 0 {
timeout = 60 * time.Second
}
return &ShumaiService{
config: ShumaiConfig{
URL: url,
AppID: appID,
AppSecret: appSecret,
AppID2: appID2, // 走政务接口使用这个
AppSecret2: appSecret2, // 走政务接口使用这个
SignMethod: signMethod,
Timeout: timeout,
},
logger: logger,
useGovernment: false,
}
}
func (s *ShumaiService) generateRequestID() string {
timestamp := time.Now().UnixNano()
appID := s.getCurrentAppID()
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, appID)))
return fmt.Sprintf("shumai_%x", hash[:8])
}
// getCurrentAppID 获取当前使用的 AppID
func (s *ShumaiService) getCurrentAppID() string {
if s.useGovernment && s.config.AppID2 != "" {
return s.config.AppID2
}
return s.config.AppID
}
// getCurrentAppSecret 获取当前使用的 AppSecret
func (s *ShumaiService) getCurrentAppSecret() string {
if s.useGovernment && s.config.AppSecret2 != "" {
return s.config.AppSecret2
}
return s.config.AppSecret
}
// UseGovernment 切换到政务接口(使用 app_id2 和 app_secret2
func (s *ShumaiService) UseGovernment() {
s.useGovernment = true
}
// UseNormal 切换到普通接口(使用 app_id 和 app_secret
func (s *ShumaiService) UseNormal() {
s.useGovernment = false
}
// IsUsingGovernment 检查是否正在使用政务接口
func (s *ShumaiService) IsUsingGovernment() bool {
return s.useGovernment
}
// GetConfig 返回当前配置
func (s *ShumaiService) GetConfig() ShumaiConfig {
return s.config
}
// CallAPIForm 以表单方式调用数脉 APIapplication/x-www-form-urlencoded
// 在方法内部将 reqFormData 转为表单:先写入业务参数,再追加 appid、timestamp、sign。
// 签名算法md5(appid&timestamp&app_security)32 位小写,不足补 0。
func (s *ShumaiService) CallAPIForm(ctx context.Context, apiPath string, reqFormData map[string]interface{}) ([]byte, error) {
startTime := time.Now()
requestID := s.generateRequestID()
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
appID := s.getCurrentAppID()
appSecret := s.getCurrentAppSecret()
sign := GenerateSignForm(appID, timestamp, appSecret)
var transactionID string
if id, ok := ctx.Value("transaction_id").(string); ok {
transactionID = id
}
form := url.Values{}
form.Set("appid", appID)
form.Set("timestamp", timestamp)
form.Set("sign", sign)
for k, v := range reqFormData {
if v == nil {
continue
}
form.Set(k, fmt.Sprint(v))
}
body := form.Encode()
baseURL := strings.TrimSuffix(s.config.URL, "/")
reqURL := baseURL
if apiPath != "" {
reqURL = baseURL + "/" + strings.TrimPrefix(apiPath, "/")
}
if apiPath == "" {
apiPath = "shumai_form"
}
if s.logger != nil {
s.logger.LogRequest(requestID, transactionID, apiPath, reqURL)
}
req, err := http.NewRequestWithContext(ctx, "POST", reqURL, strings.NewReader(body))
if err != nil {
err = errors.Join(ErrSystem, err)
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData)
}
return nil, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{Timeout: s.config.Timeout}
resp, err := client.Do(req)
if err != nil {
isTimeout := ctx.Err() == context.DeadlineExceeded
if !isTimeout {
if te, ok := err.(interface{ Timeout() bool }); ok && te.Timeout() {
isTimeout = true
}
}
if !isTimeout {
es := err.Error()
if strings.Contains(es, "deadline exceeded") || strings.Contains(es, "timeout") || strings.Contains(es, "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, reqFormData)
}
return nil, err
}
defer resp.Body.Close()
duration := time.Since(startTime)
raw, err := io.ReadAll(resp.Body)
if err != nil {
err = errors.Join(ErrSystem, err)
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData)
}
return nil, err
}
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, reqFormData)
}
return nil, err
}
if s.logger != nil {
s.logger.LogResponse(requestID, transactionID, apiPath, resp.StatusCode, duration)
}
var shumaiResp ShumaiResponse
if err := json.Unmarshal(raw, &shumaiResp); err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData)
}
return nil, err
}
codeStr := strconv.Itoa(shumaiResp.Code)
msg := shumaiResp.Msg
if msg == "" {
msg = shumaiResp.Message
}
shumaiErr := NewShumaiErrorFromCode(codeStr)
if !shumaiErr.IsSuccess() {
if shumaiErr.Message == "未知错误" && msg != "" {
shumaiErr = NewShumaiError(codeStr, msg)
}
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiPath, shumaiErr, reqFormData)
}
if shumaiErr.IsNoRecord() {
return nil, errors.Join(ErrNotFound, shumaiErr)
}
return nil, errors.Join(ErrDatasource, shumaiErr)
}
if shumaiResp.Data == nil {
return []byte("{}"), nil
}
dataBytes, err := json.Marshal(shumaiResp.Data)
if err != nil {
err = errors.Join(ErrSystem, fmt.Errorf("data 序列化失败: %w", err))
if s.logger != nil {
s.logger.LogError(requestID, transactionID, apiPath, err, reqFormData)
}
return nil, err
}
return dataBytes, nil
}
func (s *ShumaiService) Encrypt(data string) (string, error) {
appSecret := s.getCurrentAppSecret()
encryptedValue, err := Encrypt(data, appSecret)
if err != nil {
return "", ErrSystem
}
return encryptedValue, nil
}
func (s *ShumaiService) Decrypt(encodedData string) ([]byte, error) {
appSecret := s.getCurrentAppSecret()
decryptedValue, err := Decrypt(encodedData, appSecret)
if err != nil {
return nil, ErrSystem
}
return decryptedValue, nil
}