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 maxLogParamValueLen = 300 maxLogResponseBodyLen = 500 ) // HaiyuapiConfig 海宇 API 服务配置 type HaiyuapiConfig struct { BaseURL string AccessID string SecretKey string Timeout time.Duration } // HaiyuapiService 海宇平台中转服务 type HaiyuapiService struct { config HaiyuapiConfig logger *external_logger.ExternalServiceLogger } // NewHaiyuapiService 创建海宇 API 服务实例 func NewHaiyuapiService(cfg HaiyuapiConfig, logger *external_logger.ExternalServiceLogger) *HaiyuapiService { if cfg.Timeout == 0 { cfg.Timeout = defaultRequestTimeout } return &HaiyuapiService{ config: cfg, logger: logger, } } func (s *HaiyuapiService) generateRequestID() string { timestamp := time.Now().UnixNano() hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, s.config.AccessID))) return fmt.Sprintf("haiyuapi_%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 *HaiyuapiService) 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, "/") timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) reqURL := fmt.Sprintf("%s/api/v1/%s?t=%s", baseURL, productCode, timestamp) if s.logger != nil { s.logger.LogRequest(requestID, transactionID, productCode, 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, 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(HeaderContentType, ContentTypeJSON) req.Header.Set(HeaderAccessID, s.config.AccessID) client := &http.Client{Timeout: s.config.Timeout} resp, err := client.Do(req) if err != nil { err = wrapHTTPError(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 != CodeSuccess { 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 } plain, err := DecryptData(outer.Data, s.config.SecretKey) 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 CodeSuccess: return nil case CodeQueryEmpty: if message != "" { return errors.Join(ErrNotFound, errors.New(message)) } return ErrNotFound case CodeRequestParam: if message != "" { return errors.Join(ErrSystem, errors.New(message)) } return errors.Join(ErrSystem, errors.New("请求参数结构不正确")) case CodeSystem, CodeDecryptFail: if message != "" { return errors.Join(ErrSystem, errors.New(message)) } return ErrSystem case CodeInvalidIP, CodeMissingAccessID, CodeInvalidAccessID, CodeInsufficientBalance, CodeProductNotSubscribed, CodeBusiness: if message != "" { return errors.Join(ErrDatasource, errors.New(message)) } return ErrDatasource default: if message != "" { return errors.Join(ErrDatasource, errors.New(message)) } return ErrDatasource } } 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) } es := err.Error() if strings.Contains(es, "deadline exceeded") || strings.Contains(es, "timeout") || strings.Contains(es, "canceled") { return errors.Join(ErrDatasource, err) } return errors.Join(ErrSystem, err) }