新增:新增极光源,极光婚姻接口(测试)
This commit is contained in:
47
internal/infrastructure/external/jiguang/crypto.go
vendored
Normal file
47
internal/infrastructure/external/jiguang/crypto.go
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
package jiguang
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SignMethod 签名方法类型
|
||||
type SignMethod string
|
||||
|
||||
const (
|
||||
SignMethodMD5 SignMethod = "md5"
|
||||
SignMethodHMACMD5 SignMethod = "hmac"
|
||||
)
|
||||
|
||||
// GenerateSign 生成签名
|
||||
// 根据 signMethod 参数选择使用 MD5 或 HMAC-MD5 算法
|
||||
// MD5: md5(timestamp + "&appSecret=" + appSecret),然后转大写十六进制
|
||||
// HMAC-MD5: hmac_md5(timestamp, appSecret),然后转大写十六进制
|
||||
func GenerateSign(timestamp string, appSecret string, signMethod SignMethod) (string, error) {
|
||||
var hashBytes []byte
|
||||
|
||||
switch signMethod {
|
||||
case SignMethodMD5:
|
||||
// MD5算法:在待签名字符串后面加上 &appSecret=xxx 再进行计算
|
||||
signStr := timestamp + "&appSecret=" + appSecret
|
||||
hash := md5.Sum([]byte(signStr))
|
||||
hashBytes = hash[:]
|
||||
case SignMethodHMACMD5:
|
||||
// HMAC-MD5算法:使用 appSecret 初始化摘要算法再进行计算
|
||||
mac := hmac.New(md5.New, []byte(appSecret))
|
||||
mac.Write([]byte(timestamp))
|
||||
hashBytes = mac.Sum(nil)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的签名方法: %s", signMethod)
|
||||
}
|
||||
|
||||
// 将二进制转化为大写的十六进制(正确签名应该为32大写字符串)
|
||||
return hex.EncodeToString(hashBytes), nil
|
||||
}
|
||||
|
||||
// GenerateSignWithDefault 使用默认的 HMAC-MD5 方法生成签名
|
||||
func GenerateSignWithDefault(timestamp string, appSecret string) (string, error) {
|
||||
return GenerateSign(timestamp, appSecret, SignMethodHMACMD5)
|
||||
}
|
||||
149
internal/infrastructure/external/jiguang/jiguang_errors.go
vendored
Normal file
149
internal/infrastructure/external/jiguang/jiguang_errors.go
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
package jiguang
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// JiguangError 极光服务错误
|
||||
type JiguangError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// Error 实现error接口
|
||||
func (e *JiguangError) Error() string {
|
||||
return fmt.Sprintf("极光错误 [%d]: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// IsSuccess 检查是否成功
|
||||
func (e *JiguangError) IsSuccess() bool {
|
||||
return e.Code == 0
|
||||
}
|
||||
|
||||
// IsQueryFailed 检查是否查询失败
|
||||
func (e *JiguangError) IsQueryFailed() bool {
|
||||
return e.Code == 922
|
||||
}
|
||||
|
||||
// IsNoRecord 检查是否查无记录
|
||||
func (e *JiguangError) IsNoRecord() bool {
|
||||
return e.Code == 921
|
||||
}
|
||||
|
||||
// IsParamError 检查是否是参数相关错误
|
||||
func (e *JiguangError) IsParamError() bool {
|
||||
return e.Code == 400 || e.Code == 906 || e.Code == 914 || e.Code == 918
|
||||
}
|
||||
|
||||
// IsAuthError 检查是否是认证相关错误
|
||||
func (e *JiguangError) IsAuthError() bool {
|
||||
return e.Code == 902 || e.Code == 903 || e.Code == 904 || e.Code == 905
|
||||
}
|
||||
|
||||
// IsSystemError 检查是否是系统错误
|
||||
func (e *JiguangError) IsSystemError() bool {
|
||||
return e.Code == 405 || e.Code == 911 || e.Code == 912 || e.Code == 915 || e.Code == 916 || e.Code == 917 || e.Code == 919 || e.Code == 923
|
||||
}
|
||||
|
||||
// 预定义错误常量
|
||||
var (
|
||||
// 成功状态
|
||||
ErrSuccess = &JiguangError{Code: 0, Message: "请求成功"}
|
||||
|
||||
// 参数错误
|
||||
ErrParamInvalid = &JiguangError{Code: 400, Message: "请求参数不正确"}
|
||||
ErrMethodInvalid = &JiguangError{Code: 405, Message: "请求方法不正确"}
|
||||
ErrParamFormInvalid = &JiguangError{Code: 906, Message: "请求参数形式不正确"}
|
||||
ErrBodyIncomplete = &JiguangError{Code: 914, Message: "Body 请求参数不完整"}
|
||||
ErrBodyNotSupported = &JiguangError{Code: 918, Message: "Body 请求参数不支持"}
|
||||
|
||||
// 认证错误
|
||||
ErrAppIDInvalid = &JiguangError{Code: 902, Message: "错误的 appId/账户已删除"}
|
||||
ErrTimestampInvalid = &JiguangError{Code: 903, Message: "错误的时间戳/时间误差大于 10 分钟"}
|
||||
ErrSignMethodInvalid = &JiguangError{Code: 904, Message: "无法识别的签名方法"}
|
||||
ErrSignInvalid = &JiguangError{Code: 905, Message: "签名不合法"}
|
||||
|
||||
// 系统错误
|
||||
ErrAccountStatusError = &JiguangError{Code: 911, Message: "账户状态异常"}
|
||||
ErrInterfaceDisabled = &JiguangError{Code: 912, Message: "接口状态不可用"}
|
||||
ErrAPICallError = &JiguangError{Code: 915, Message: "API 接口调用有误"}
|
||||
ErrInternalError = &JiguangError{Code: 916, Message: "内部接口调用错误,请联系相关人员"}
|
||||
ErrTimeout = &JiguangError{Code: 917, Message: "请求超时"}
|
||||
ErrBusinessDisabled = &JiguangError{Code: 919, Message: "业务状态不可用"}
|
||||
ErrInterfaceException = &JiguangError{Code: 923, Message: "接口异常"}
|
||||
|
||||
// 业务错误
|
||||
ErrNoRecord = &JiguangError{Code: 921, Message: "查无记录"}
|
||||
ErrQueryFailed = &JiguangError{Code: 922, Message: "查询失败"}
|
||||
)
|
||||
|
||||
// NewJiguangError 创建新的极光错误
|
||||
func NewJiguangError(code int, message string) *JiguangError {
|
||||
return &JiguangError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// NewJiguangErrorFromCode 根据状态码创建错误
|
||||
func NewJiguangErrorFromCode(code int) *JiguangError {
|
||||
switch code {
|
||||
case 0:
|
||||
return ErrSuccess
|
||||
case 400:
|
||||
return ErrParamInvalid
|
||||
case 405:
|
||||
return ErrMethodInvalid
|
||||
case 902:
|
||||
return ErrAppIDInvalid
|
||||
case 903:
|
||||
return ErrTimestampInvalid
|
||||
case 904:
|
||||
return ErrSignMethodInvalid
|
||||
case 905:
|
||||
return ErrSignInvalid
|
||||
case 906:
|
||||
return ErrParamFormInvalid
|
||||
case 911:
|
||||
return ErrAccountStatusError
|
||||
case 912:
|
||||
return ErrInterfaceDisabled
|
||||
case 914:
|
||||
return ErrBodyIncomplete
|
||||
case 915:
|
||||
return ErrAPICallError
|
||||
case 916:
|
||||
return ErrInternalError
|
||||
case 917:
|
||||
return ErrTimeout
|
||||
case 918:
|
||||
return ErrBodyNotSupported
|
||||
case 919:
|
||||
return ErrBusinessDisabled
|
||||
case 921:
|
||||
return ErrNoRecord
|
||||
case 922:
|
||||
return ErrQueryFailed
|
||||
case 923:
|
||||
return ErrInterfaceException
|
||||
default:
|
||||
return &JiguangError{
|
||||
Code: code,
|
||||
Message: fmt.Sprintf("未知错误码: %d", code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IsJiguangError 检查是否是极光错误
|
||||
func IsJiguangError(err error) bool {
|
||||
_, ok := err.(*JiguangError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// GetJiguangError 获取极光错误
|
||||
func GetJiguangError(err error) *JiguangError {
|
||||
if jiguangErr, ok := err.(*JiguangError); ok {
|
||||
return jiguangErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
85
internal/infrastructure/external/jiguang/jiguang_factory.go
vendored
Normal file
85
internal/infrastructure/external/jiguang/jiguang_factory.go
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
package jiguang
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/config"
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
// NewJiguangServiceWithConfig 使用配置创建极光服务
|
||||
func NewJiguangServiceWithConfig(cfg *config.Config) (*JiguangService, error) {
|
||||
// 将配置类型转换为通用外部服务日志配置
|
||||
loggingConfig := external_logger.ExternalServiceLoggingConfig{
|
||||
Enabled: cfg.Jiguang.Logging.Enabled,
|
||||
LogDir: cfg.Jiguang.Logging.LogDir,
|
||||
ServiceName: "jiguang",
|
||||
UseDaily: cfg.Jiguang.Logging.UseDaily,
|
||||
EnableLevelSeparation: cfg.Jiguang.Logging.EnableLevelSeparation,
|
||||
LevelConfigs: make(map[string]external_logger.ExternalServiceLevelFileConfig),
|
||||
}
|
||||
|
||||
// 转换级别配置
|
||||
for key, value := range cfg.Jiguang.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
|
||||
}
|
||||
|
||||
// 解析签名方法
|
||||
var signMethod SignMethod
|
||||
if cfg.Jiguang.SignMethod == "md5" {
|
||||
signMethod = SignMethodMD5
|
||||
} else {
|
||||
signMethod = SignMethodHMACMD5 // 默认使用 HMAC-MD5
|
||||
}
|
||||
|
||||
// 解析超时时间
|
||||
timeout := 60 * time.Second
|
||||
if cfg.Jiguang.Timeout > 0 {
|
||||
timeout = cfg.Jiguang.Timeout
|
||||
}
|
||||
|
||||
// 创建极光服务
|
||||
service := NewJiguangService(
|
||||
cfg.Jiguang.URL,
|
||||
cfg.Jiguang.AppID,
|
||||
cfg.Jiguang.AppSecret,
|
||||
signMethod,
|
||||
timeout,
|
||||
logger,
|
||||
)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// NewJiguangServiceWithLogging 使用自定义日志配置创建极光服务
|
||||
func NewJiguangServiceWithLogging(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, loggingConfig external_logger.ExternalServiceLoggingConfig) (*JiguangService, error) {
|
||||
// 设置服务名称
|
||||
loggingConfig.ServiceName = "jiguang"
|
||||
|
||||
// 创建通用外部服务日志器
|
||||
logger, err := external_logger.NewExternalServiceLogger(loggingConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建极光服务
|
||||
service := NewJiguangService(url, appID, appSecret, signMethod, timeout, logger)
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// NewJiguangServiceSimple 创建简单的极光服务(无日志)
|
||||
func NewJiguangServiceSimple(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration) *JiguangService {
|
||||
return NewJiguangService(url, appID, appSecret, signMethod, timeout, nil)
|
||||
}
|
||||
265
internal/infrastructure/external/jiguang/jiguang_service.go
vendored
Normal file
265
internal/infrastructure/external/jiguang/jiguang_service.go
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
package jiguang
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/shared/external_logger"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDatasource = errors.New("数据源异常")
|
||||
ErrSystem = errors.New("系统异常")
|
||||
ErrNotFound = errors.New("查询为空")
|
||||
)
|
||||
|
||||
// JiguangResponse 极光API响应结构
|
||||
type JiguangResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
OrderID string `json:"order_id"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// JiguangConfig 极光服务配置
|
||||
type JiguangConfig struct {
|
||||
URL string
|
||||
AppID string
|
||||
AppSecret string
|
||||
SignMethod SignMethod // 签名方法:md5 或 hmac
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// JiguangService 极光服务
|
||||
type JiguangService struct {
|
||||
config JiguangConfig
|
||||
logger *external_logger.ExternalServiceLogger
|
||||
}
|
||||
|
||||
// NewJiguangService 创建一个新的极光服务实例
|
||||
func NewJiguangService(url, appID, appSecret string, signMethod SignMethod, timeout time.Duration, logger *external_logger.ExternalServiceLogger) *JiguangService {
|
||||
// 如果没有指定签名方法,默认使用 HMAC-MD5
|
||||
if signMethod == "" {
|
||||
signMethod = SignMethodHMACMD5
|
||||
}
|
||||
|
||||
// 如果没有指定超时时间,默认使用 60 秒
|
||||
if timeout == 0 {
|
||||
timeout = 60 * time.Second
|
||||
}
|
||||
|
||||
return &JiguangService{
|
||||
config: JiguangConfig{
|
||||
URL: url,
|
||||
AppID: appID,
|
||||
AppSecret: appSecret,
|
||||
SignMethod: signMethod,
|
||||
Timeout: timeout,
|
||||
},
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求ID
|
||||
func (j *JiguangService) generateRequestID() string {
|
||||
timestamp := time.Now().UnixNano()
|
||||
hash := md5.Sum([]byte(fmt.Sprintf("%d_%s", timestamp, j.config.AppID)))
|
||||
return fmt.Sprintf("jiguang_%x", hash[:8])
|
||||
}
|
||||
|
||||
// CallAPI 调用极光API
|
||||
// apiCode: API服务编码(如 marriage-single-v2)
|
||||
// params: 请求参数(会作为JSON body发送)
|
||||
func (j *JiguangService) CallAPI(ctx context.Context, apiCode string, params map[string]interface{}) (resp []byte, err error) {
|
||||
startTime := time.Now()
|
||||
requestID := j.generateRequestID()
|
||||
|
||||
// 生成时间戳(毫秒)
|
||||
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
|
||||
// 从ctx中获取transactionId
|
||||
var transactionID string
|
||||
if ctxTransactionID, ok := ctx.Value("transaction_id").(string); ok {
|
||||
transactionID = ctxTransactionID
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
sign, signErr := GenerateSign(timestamp, j.config.AppSecret, j.config.SignMethod)
|
||||
if signErr != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("生成签名失败: %w", signErr))
|
||||
if j.logger != nil {
|
||||
j.logger.LogError(requestID, transactionID, apiCode, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录请求日志
|
||||
if j.logger != nil {
|
||||
j.logger.LogRequest(requestID, transactionID, apiCode, j.config.URL, params)
|
||||
}
|
||||
|
||||
// 将请求参数转换为JSON
|
||||
jsonData, marshalErr := json.Marshal(params)
|
||||
if marshalErr != nil {
|
||||
err = errors.Join(ErrSystem, marshalErr)
|
||||
if j.logger != nil {
|
||||
j.logger.LogError(requestID, transactionID, apiCode, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建HTTP POST请求
|
||||
req, newRequestErr := http.NewRequestWithContext(ctx, "POST", j.config.URL, bytes.NewBuffer(jsonData))
|
||||
if newRequestErr != nil {
|
||||
err = errors.Join(ErrSystem, newRequestErr)
|
||||
if j.logger != nil {
|
||||
j.logger.LogError(requestID, transactionID, apiCode, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设置请求头
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("appId", j.config.AppID)
|
||||
req.Header.Set("apiCode", apiCode)
|
||||
req.Header.Set("timestamp", timestamp)
|
||||
req.Header.Set("signMethod", string(j.config.SignMethod))
|
||||
req.Header.Set("sign", sign)
|
||||
|
||||
// 创建HTTP客户端
|
||||
client := &http.Client{
|
||||
Timeout: j.config.Timeout,
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
httpResp, clientDoErr := client.Do(req)
|
||||
if clientDoErr != nil {
|
||||
// 检查是否是超时错误
|
||||
isTimeout := false
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
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" {
|
||||
isTimeout = true
|
||||
}
|
||||
|
||||
if isTimeout {
|
||||
err = errors.Join(ErrDatasource, fmt.Errorf("API请求超时: %v", clientDoErr))
|
||||
} else {
|
||||
err = errors.Join(ErrSystem, clientDoErr)
|
||||
}
|
||||
if j.logger != nil {
|
||||
j.logger.LogError(requestID, transactionID, apiCode, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
closeErr := Body.Close()
|
||||
if closeErr != nil {
|
||||
// 记录关闭错误
|
||||
if j.logger != nil {
|
||||
j.logger.LogError(requestID, transactionID, apiCode, errors.Join(ErrSystem, fmt.Errorf("关闭响应体失败: %w", closeErr)), params)
|
||||
}
|
||||
}
|
||||
}(httpResp.Body)
|
||||
|
||||
// 计算请求耗时
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// 读取响应体
|
||||
bodyBytes, readErr := io.ReadAll(httpResp.Body)
|
||||
if readErr != nil {
|
||||
err = errors.Join(ErrSystem, readErr)
|
||||
if j.logger != nil {
|
||||
j.logger.LogError(requestID, transactionID, apiCode, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查HTTP状态码
|
||||
if httpResp.StatusCode != http.StatusOK {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("极光请求失败,状态码: %d", httpResp.StatusCode))
|
||||
if j.logger != nil {
|
||||
j.logger.LogError(requestID, transactionID, apiCode, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析响应结构
|
||||
var jiguangResp JiguangResponse
|
||||
if err := json.Unmarshal(bodyBytes, &jiguangResp); err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("响应解析失败: %w", err))
|
||||
if j.logger != nil {
|
||||
j.logger.LogError(requestID, transactionID, apiCode, err, params)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 记录响应日志
|
||||
if j.logger != nil {
|
||||
if jiguangResp.OrderID != "" {
|
||||
j.logger.LogResponseWithID(requestID, transactionID, apiCode, httpResp.StatusCode, bodyBytes, duration, jiguangResp.OrderID)
|
||||
} else {
|
||||
j.logger.LogResponse(requestID, transactionID, apiCode, httpResp.StatusCode, bodyBytes, duration)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查业务状态码
|
||||
if jiguangResp.Code != 0 {
|
||||
// 创建极光错误
|
||||
jiguangErr := NewJiguangErrorFromCode(jiguangResp.Code)
|
||||
if jiguangErr.Message == fmt.Sprintf("未知错误码: %d", jiguangResp.Code) && jiguangResp.Msg != "" {
|
||||
jiguangErr.Message = jiguangResp.Msg
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
if j.logger != nil {
|
||||
j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, jiguangErr, params, jiguangResp.OrderID)
|
||||
}
|
||||
|
||||
// 根据错误类型返回不同的错误
|
||||
if jiguangErr.IsNoRecord() {
|
||||
return nil, errors.Join(ErrNotFound, jiguangErr)
|
||||
} else if jiguangErr.IsQueryFailed() {
|
||||
return nil, errors.Join(ErrDatasource, jiguangErr)
|
||||
} else if jiguangErr.IsSystemError() {
|
||||
return nil, errors.Join(ErrSystem, jiguangErr)
|
||||
} else {
|
||||
return nil, errors.Join(ErrDatasource, jiguangErr)
|
||||
}
|
||||
}
|
||||
|
||||
// 成功响应,返回data字段
|
||||
if jiguangResp.Data == nil {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
|
||||
// 将data转换为JSON字节
|
||||
dataBytes, err := json.Marshal(jiguangResp.Data)
|
||||
if err != nil {
|
||||
err = errors.Join(ErrSystem, fmt.Errorf("data字段序列化失败: %w", err))
|
||||
if j.logger != nil {
|
||||
j.logger.LogErrorWithResponseID(requestID, transactionID, apiCode, err, params, jiguangResp.OrderID)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dataBytes, nil
|
||||
}
|
||||
|
||||
// GetConfig 获取配置信息
|
||||
func (j *JiguangService) GetConfig() JiguangConfig {
|
||||
return j.config
|
||||
}
|
||||
Reference in New Issue
Block a user