package tianyuanapi import ( "bytes" "context" "crypto/md5" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" "strings" "time" "hyapi-server/internal/shared/crypto" "hyapi-server/internal/shared/external_logger" ) const ( maxLogParamValueLen = 300 maxLogResponseBodyLen = 500 ) var ( ErrDatasource = errors.New("数据源异常") ErrSystem = errors.New("系统异常") ErrNotFound = errors.New("查询为空") ) // TianyuanapiConfig 天远 API 服务配置 type TianyuanapiConfig struct { BaseURL string AccessID string EncryptionKey string Timeout time.Duration } // apiResponse 天远平台 HTTP 外层响应 type apiResponse struct { Code int `json:"code"` Message string `json:"message"` Data string `json:"data"` } // requestPayload 请求体 type requestPayload struct { Data string `json:"data"` } // TianyuanapiService 天远平台中转服务 type TianyuanapiService struct { config TianyuanapiConfig logger *external_logger.ExternalServiceLogger } // NewTianyuanapiService 创建天远 API 服务实例 func NewTianyuanapiService(cfg TianyuanapiConfig, logger *external_logger.ExternalServiceLogger) *TianyuanapiService { if cfg.Timeout == 0 { cfg.Timeout = 30 * time.Second } return &TianyuanapiService{ config: cfg, logger: logger, } } func (s *TianyuanapiService) generateRequestID() string { timestamp := time.Now().UnixNano() hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, s.config.AccessID))) return fmt.Sprintf("tianyuanapi_%x", hash[:8]) } func truncateForLog(str string, maxLen int) string { if maxLen <= 0 || len(str) <= maxLen { return str } return str[:maxLen] + "...[truncated, total " + strconv.Itoa(len(str)) + " chars]" } func requestParamsForLog(params map[string]interface{}) map[string]interface{} { if params == nil { return nil } out := make(map[string]interface{}, len(params)) for k, v := range params { if v == nil { out[k] = nil continue } switch val := v.(type) { case string: out[k] = truncateForLog(val, maxLogParamValueLen) default: out[k] = truncateForLog(fmt.Sprint(v), maxLogParamValueLen) } } return out } // CallAPI 调用天远平台指定产品(产品编号与处理器 ApiCode 一致) func (s *TianyuanapiService) CallAPI(ctx context.Context, productCode string, params map[string]interface{}) ([]byte, error) { startTime := time.Now() requestID := s.generateRequestID() var transactionID string if id, ok := ctx.Value("transaction_id").(string); ok { transactionID = id } baseURL := strings.TrimSuffix(s.config.BaseURL, "/") reqURL := fmt.Sprintf("%s/api/v1/%s", baseURL, productCode) if s.logger != nil { s.logger.LogRequest(requestID, transactionID, productCode, reqURL) } jsonData, err := json.Marshal(params) if err != nil { err = errors.Join(ErrSystem, err) if s.logger != nil { s.logger.LogError(requestID, transactionID, productCode, err, map[string]interface{}{"request_params": requestParamsForLog(params)}) } return nil, err } encryptedData, err := crypto.AesEncrypt(jsonData, s.config.EncryptionKey) if err != nil { err = errors.Join(ErrSystem, fmt.Errorf("加密请求失败: %w", err)) if s.logger != nil { s.logger.LogError(requestID, transactionID, productCode, err, map[string]interface{}{"request_params": requestParamsForLog(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, productCode, err, map[string]interface{}{"request_params": requestParamsForLog(params)}) } return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqURL, bytes.NewReader(bodyBytes)) if err != nil { err = errors.Join(ErrSystem, err) if s.logger != nil { s.logger.LogError(requestID, transactionID, productCode, err, map[string]interface{}{"request_params": requestParamsForLog(params)}) } return nil, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Access-Id", s.config.AccessID) 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, productCode, err, map[string]interface{}{"request_params": requestParamsForLog(params)}) } 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, productCode, err, map[string]interface{}{"request_params": requestParamsForLog(params)}) } 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, productCode, err, map[string]interface{}{ "request_params": requestParamsForLog(params), "response_body": truncateForLog(string(raw), maxLogResponseBodyLen), }) } return nil, err } if s.logger != nil { s.logger.LogResponse(requestID, transactionID, productCode, resp.StatusCode, duration) } var outer apiResponse if err := json.Unmarshal(raw, &outer); err != nil { parseErr := errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err)) if s.logger != nil { s.logger.LogError(requestID, transactionID, productCode, parseErr, map[string]interface{}{ "request_params": requestParamsForLog(params), "response_body": truncateForLog(string(raw), maxLogResponseBodyLen), }) } return nil, parseErr } if outer.Code != 0 { mappedErr := mapBusinessError(outer.Code, outer.Message) if s.logger != nil { s.logger.LogError(requestID, transactionID, productCode, mappedErr, map[string]interface{}{ "request_params": requestParamsForLog(params), "response_body": truncateForLog(string(raw), maxLogResponseBodyLen), "api_code": outer.Code, }) } if errors.Is(mappedErr, ErrNotFound) { return nil, mappedErr } return nil, mappedErr } if outer.Data == "" { return []byte("{}"), nil } plain, err := crypto.AesDecrypt(outer.Data, s.config.EncryptionKey) if err != nil { decErr := errors.Join(ErrSystem, fmt.Errorf("解密响应失败: %w", err)) if s.logger != nil { s.logger.LogError(requestID, transactionID, productCode, decErr, map[string]interface{}{ "request_params": requestParamsForLog(params), "response_body": truncateForLog(string(raw), maxLogResponseBodyLen), }) } return nil, decErr } if len(plain) == 0 { return []byte("{}"), nil } if !json.Valid(plain) { return plain, nil } return plain, nil } func mapBusinessError(code int, message string) error { switch code { case 0: return nil case 1000: if message != "" { return errors.Join(ErrNotFound, errors.New(message)) } return ErrNotFound case 1003: if message != "" { return fmt.Errorf("%s", message) } return errors.New("请求参数结构不正确") case 2001: if message != "" { return errors.Join(ErrDatasource, errors.New(message)) } return ErrDatasource default: if message != "" { return errors.Join(ErrDatasource, errors.New(message)) } return ErrDatasource } }