This commit is contained in:
Mrx
2026-01-23 17:53:11 +08:00
parent 6104bfb84f
commit b05c459b81
28 changed files with 1416 additions and 59 deletions

View File

@@ -0,0 +1,199 @@
package shumai
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"errors"
"strings"
)
// SignMethod 签名方法
type SignMethod string
const (
SignMethodMD5 SignMethod = "md5"
SignMethodHMACMD5 SignMethod = "hmac"
)
// GenerateSignForm 生成表单接口签名appid & timestamp & app_security
// 拼接规则appid + "&" + timestamp + "&" + app_security对拼接串做 MD532 位小写十六进制;
// 不足 32 位左侧补 0。
func GenerateSignForm(appid, timestamp, appSecret string) string {
str := appid + "&" + timestamp + "&" + appSecret
hash := md5.Sum([]byte(str))
sign := strings.ToLower(hex.EncodeToString(hash[:]))
if n := 32 - len(sign); n > 0 {
sign = strings.Repeat("0", n) + sign
}
return sign
}
// app_secret: "BnJWo61hUgNEa5fqBCueiT1IZ1e0DxPU"
// Encrypt 使用 AES/ECB/PKCS5Padding 加密数据
// 加密算法AES工作模式ECB无初始向量填充方式PKCS5Padding
// 加密 key 是服务商分配的 app_securityAES 加密之后再进行 base64 编码
func Encrypt(data, appSecurity string) (string, error) {
key := prepareAESKey([]byte(appSecurity))
ciphertext, err := aesEncryptECB([]byte(data), key)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// Decrypt 解密 base64 编码的 AES/ECB/PKCS5Padding 加密数据
func Decrypt(encodedData, appSecurity string) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(encodedData)
if err != nil {
return nil, err
}
key := prepareAESKey([]byte(appSecurity))
plaintext, err := aesDecryptECB(ciphertext, key)
if err != nil {
return nil, err
}
return plaintext, nil
}
// prepareAESKey 准备 AES 密钥,确保长度为 16/24/32 字节
// 如果 key 长度不足,用 0 填充;如果过长,截取前 32 字节
func prepareAESKey(key []byte) []byte {
keyLen := len(key)
if keyLen == 16 || keyLen == 24 || keyLen == 32 {
return key
}
if keyLen < 16 {
// 不足 16 字节,用 0 填充到 16 字节AES-128
padded := make([]byte, 16)
copy(padded, key)
return padded
}
if keyLen < 24 {
// 不足 24 字节,用 0 填充到 24 字节AES-192
padded := make([]byte, 24)
copy(padded, key)
return padded
}
if keyLen < 32 {
// 不足 32 字节,用 0 填充到 32 字节AES-256
padded := make([]byte, 32)
copy(padded, key)
return padded
}
// 超过 32 字节,截取前 32 字节AES-256
return key[:32]
}
// aesEncryptECB 使用 AES ECB 模式加密PKCS5 填充
func aesEncryptECB(plaintext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
paddedPlaintext := pkcs5Padding(plaintext, block.BlockSize())
ciphertext := make([]byte, len(paddedPlaintext))
mode := newECBEncrypter(block)
mode.CryptBlocks(ciphertext, paddedPlaintext)
return ciphertext, nil
}
// aesDecryptECB 使用 AES ECB 模式解密PKCS5 去填充
func aesDecryptECB(ciphertext, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext)%block.BlockSize() != 0 {
return nil, errors.New("ciphertext length is not a multiple of block size")
}
plaintext := make([]byte, len(ciphertext))
mode := newECBDecrypter(block)
mode.CryptBlocks(plaintext, ciphertext)
return pkcs5Unpadding(plaintext), nil
}
// pkcs5Padding PKCS5 填充
func pkcs5Padding(src []byte, blockSize int) []byte {
padding := blockSize - len(src)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
// pkcs5Unpadding 去除 PKCS5 填充
func pkcs5Unpadding(src []byte) []byte {
length := len(src)
if length == 0 {
return src
}
unpadding := int(src[length-1])
if unpadding > length {
return src
}
return src[:length-unpadding]
}
// ECB 模式加密/解密实现
type ecb struct {
b cipher.Block
blockSize int
}
func newECB(b cipher.Block) *ecb {
return &ecb{
b: b,
blockSize: b.BlockSize(),
}
}
type ecbEncrypter ecb
func newECBEncrypter(b cipher.Block) cipher.BlockMode {
return (*ecbEncrypter)(newECB(b))
}
func (x *ecbEncrypter) BlockSize() int {
return x.blockSize
}
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
if len(src)%x.blockSize != 0 {
panic("crypto/cipher: input not full blocks")
}
if len(dst) < len(src) {
panic("crypto/cipher: output smaller than input")
}
for len(src) > 0 {
x.b.Encrypt(dst, src[:x.blockSize])
src = src[x.blockSize:]
dst = dst[x.blockSize:]
}
}
type ecbDecrypter ecb
func newECBDecrypter(b cipher.Block) cipher.BlockMode {
return (*ecbDecrypter)(newECB(b))
}
func (x *ecbDecrypter) BlockSize() int {
return x.blockSize
}
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
if len(src)%x.blockSize != 0 {
panic("crypto/cipher: input not full blocks")
}
if len(dst) < len(src) {
panic("crypto/cipher: output smaller than input")
}
for len(src) > 0 {
x.b.Decrypt(dst, src[:x.blockSize])
src = src[x.blockSize:]
dst = dst[x.blockSize:]
}
}

View File

@@ -0,0 +1,108 @@
package shumai
import (
"fmt"
)
// ShumaiError 数脉服务错误
type ShumaiError struct {
Code string `json:"code"`
Message string `json:"message"`
}
// Error 实现 error 接口
func (e *ShumaiError) Error() string {
return fmt.Sprintf("数脉错误 [%s]: %s", e.Code, e.Message)
}
// IsSuccess 是否成功
func (e *ShumaiError) IsSuccess() bool {
return e.Code == "0" || e.Code == "200"
}
// IsNoRecord 是否查无记录
func (e *ShumaiError) IsNoRecord() bool {
return e.Code == "404"
}
// IsParamError 是否参数错误
func (e *ShumaiError) IsParamError() bool {
return e.Code == "400"
}
// IsAuthError 是否认证错误
func (e *ShumaiError) IsAuthError() bool {
return e.Code == "601" || e.Code == "602"
}
// IsSystemError 是否系统错误
func (e *ShumaiError) IsSystemError() bool {
return e.Code == "500" || e.Code == "501"
}
// 预定义错误
var (
ErrSuccess = &ShumaiError{Code: "200", Message: "成功"}
ErrParamError = &ShumaiError{Code: "400", Message: "参数错误"}
ErrNoRecord = &ShumaiError{Code: "404", Message: "请求资源不存在"}
ErrSystemError = &ShumaiError{Code: "500", Message: "系统内部错误,请联系服务商"}
ErrThirdPartyError = &ShumaiError{Code: "501", Message: "第三方服务异常"}
ErrNoPermission = &ShumaiError{Code: "601", Message: "服务商未开通接口权限"}
ErrAccountDisabled = &ShumaiError{Code: "602", Message: "账号停用"}
ErrInsufficientBalance = &ShumaiError{Code: "603", Message: "余额不足请充值"}
ErrInterfaceDisabled = &ShumaiError{Code: "604", Message: "接口停用"}
ErrInsufficientQuota = &ShumaiError{Code: "605", Message: "次数不足,请购买套餐"}
ErrRateLimitExceeded = &ShumaiError{Code: "606", Message: "调用超限,请联系服务商"}
ErrOther = &ShumaiError{Code: "1001", Message: "其他,以实际返回为准"}
)
// NewShumaiError 创建数脉错误
func NewShumaiError(code, message string) *ShumaiError {
return &ShumaiError{Code: code, Message: message}
}
// NewShumaiErrorFromCode 根据状态码创建错误
func NewShumaiErrorFromCode(code string) *ShumaiError {
switch code {
case "0", "200":
return ErrSuccess
case "400":
return ErrParamError
case "404":
return ErrNoRecord
case "500":
return ErrSystemError
case "501":
return ErrThirdPartyError
case "601":
return ErrNoPermission
case "602":
return ErrAccountDisabled
case "603":
return ErrInsufficientBalance
case "604":
return ErrInterfaceDisabled
case "605":
return ErrInsufficientQuota
case "606":
return ErrRateLimitExceeded
case "1001":
return ErrOther
default:
return &ShumaiError{Code: code, Message: "未知错误"}
}
}
// IsShumaiError 是否为数脉错误
func IsShumaiError(err error) bool {
_, ok := err.(*ShumaiError)
return ok
}
// GetShumaiError 获取数脉错误
func GetShumaiError(err error) *ShumaiError {
if e, ok := err.(*ShumaiError); ok {
return e
}
return nil
}

View File

@@ -0,0 +1,69 @@
package shumai
import (
"time"
"tyapi-server/internal/config"
"tyapi-server/internal/shared/external_logger"
)
// NewShumaiServiceWithConfig 使用 config 创建数脉服务
func NewShumaiServiceWithConfig(cfg *config.Config) (*ShumaiService, error) {
loggingConfig := external_logger.ExternalServiceLoggingConfig{
Enabled: cfg.Shumai.Logging.Enabled,
LogDir: cfg.Shumai.Logging.LogDir,
ServiceName: "shumai",
UseDaily: cfg.Shumai.Logging.UseDaily,
EnableLevelSeparation: cfg.Shumai.Logging.EnableLevelSeparation,
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
}
for k, v := range cfg.Shumai.Logging.LevelConfigs {
loggingConfig.LevelConfigs[k] = external_logger.ExternalServiceLevelFileConfig{
MaxSize: v.MaxSize,
MaxBackups: v.MaxBackups,
MaxAge: v.MaxAge,
Compress: v.Compress,
}
}
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
var signMethod SignMethod
if cfg.Shumai.SignMethod == "md5" {
signMethod = SignMethodMD5
} else {
signMethod = SignMethodHMACMD5
}
timeout := 60 * time.Second
if cfg.Shumai.Timeout > 0 {
timeout = cfg.Shumai.Timeout
}
return NewShumaiService(
cfg.Shumai.URL,
cfg.Shumai.AppID,
cfg.Shumai.AppSecret,
signMethod,
timeout,
logger,
cfg.Shumai.AppID2, // 走政务接口使用这个
cfg.Shumai.AppSecret2, // 走政务接口使用这个
), nil
}
// NewShumaiServiceWithLogging 使用自定义日志配置创建数脉服务
func NewShumaiServiceWithLogging(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig, appID2, appSecret2 string) (*ShumaiService, error) {
loggingConfig.ServiceName = "shumai"
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
if err != nil {
return nil, err
}
return NewShumaiService(url, appID, appSecret, signMethod, timeout, logger, appID2, appSecret2), nil
}
// NewShumaiServiceSimple 创建无数脉日志的数脉服务
func NewShumaiServiceSimple(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, appID2, appSecret2 string) *ShumaiService {
return NewShumaiService(url, appID, appSecret, signMethod, timeout, nil, appID2, appSecret2)
}

View File

@@ -0,0 +1,279 @@
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
}

View File

@@ -19,7 +19,7 @@ import (
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
ErrNotFound = errors.New("查询为空")
ErrNotFound = errors.New("查询为空")
)
type WestResp struct {
@@ -72,7 +72,6 @@ func (w *WestDexService) generateRequestID() string {
return fmt.Sprintf("westdex_%x", hash[:8])
}
// buildRequestURL 构建请求URL
func (w *WestDexService) buildRequestURL(code string) string {
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
@@ -132,14 +131,13 @@ func (w *WestDexService) CallAPI(ctx context.Context, code string, reqData map[s
isTimeout = true
} else if netErr, ok := clientDoErr.(interface{ Timeout() bool }); ok && netErr.Timeout() {
isTimeout = true
} else if errStr := clientDoErr.Error();
errStr == "context deadline exceeded" ||
errStr == "timeout" ||
errStr == "Client.Timeout exceeded" ||
errStr == "net/http: request canceled" {
} else if errStr := clientDoErr.Error(); errStr == "context deadline exceeded" ||
errStr == "timeout" ||
errStr == "Client.Timeout exceeded" ||
errStr == "net/http: request canceled" {
isTimeout = true
}
if isTimeout {
err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", clientDoErr))
} else {
@@ -185,7 +183,6 @@ func (w *WestDexService) CallAPI(ctx context.Context, code string, reqData map[s
}
return nil, err
}
// 记录响应日志(不记录具体响应数据)
if w.logger != nil {
w.logger.LogResponseWithID(requestID, transactionID, code, httpResp.StatusCode, duration, westDexResp.ID)
@@ -305,14 +302,13 @@ func (w *WestDexService) G05HZ01CallAPI(ctx context.Context, code string, reqDat
isTimeout = true
} else if netErr, ok := clientDoErr.(interface{ Timeout() bool }); ok && netErr.Timeout() {
isTimeout = true
} else if errStr := clientDoErr.Error();
errStr == "context deadline exceeded" ||
errStr == "timeout" ||
errStr == "Client.Timeout exceeded" ||
errStr == "net/http: request canceled" {
} else if errStr := clientDoErr.Error(); errStr == "context deadline exceeded" ||
errStr == "timeout" ||
errStr == "Client.Timeout exceeded" ||
errStr == "net/http: request canceled" {
isTimeout = true
}
if isTimeout {
err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", clientDoErr))
} else {

View File

@@ -22,7 +22,6 @@ import (
var (
ErrDatasource = errors.New("数据源异常")
ErrSystem = errors.New("系统异常")
ErrNotFound = errors.New("数据为空")
)
// contextKey 用于在 context 中存储不跳过 201 错误检查的标志
@@ -30,12 +29,6 @@ type contextKey string
const dontSkipCode201CheckKey contextKey = "dont_skip_code_201_check"
// WithSkipCode201Check 返回一个设置了不跳过 201 错误检查标志的 context
// 默认情况下会跳过 201 检查(继续执行),使用此函数后会在 Code == "201" 时返回错误
func WithSkipCode201Check(ctx context.Context) context.Context {
return context.WithValue(ctx, dontSkipCode201CheckKey, true)
}
type ZhichaResp struct {
Code string `json:"code"`
Message string `json:"message"`
@@ -200,24 +193,8 @@ func (z *ZhichaService) CallAPI(ctx context.Context, proID string, params map[st
return nil, err
}
// 检查是否需要不跳过 201 错误检查(默认跳过,继续执行)
// 如果设置了 dontSkipCode201CheckKey则返回错误
dontSkipCode201Check := false
if dontSkip, ok := ctx.Value(dontSkipCode201CheckKey).(bool); ok {
dontSkipCode201Check = dontSkip
}
// 如果设置了不跳过检查,当 Code == "201" 时返回错误
if zhichaResp.Code == "201" && dontSkipCode201Check {
if z.logger != nil {
z.logger.LogError(requestID, transactionID, proID, ErrNotFound, params)
}
return nil, ErrNotFound
}
// 检查业务状态码
if zhichaResp.Code != "200" && zhichaResp.Code != "201" {
if zhichaResp.Code != "200" {
// 创建智查金控错误用于日志记录
zhichaErr := NewZhichaErrorFromCode(zhichaResp.Code)
if zhichaErr.Code == "未知错误" {