Files
tyapi-server/internal/domains/api/entities/api_call.go
2025-07-28 01:46:39 +08:00

197 lines
6.8 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 entities
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
"github.com/google/uuid"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
// ApiCallStatus API调用状态
const (
ApiCallStatusPending = "pending"
ApiCallStatusSuccess = "success"
ApiCallStatusFailed = "failed"
)
// ApiCall错误类型常量定义供各领域服务和应用层统一引用。
// 使用时可通过 entities.ApiCallErrorInvalidAccess 方式获得,编辑器可自动补全和提示。
// 错误类型与业务含义:
//
// ApiCallErrorInvalidAccess = "invalid_access" // 无效AccessId
// ApiCallErrorFrozenAccount = "frozen_account" // 账户冻结
// ApiCallErrorInvalidIP = "invalid_ip" // IP无效
// ApiCallErrorArrears = "arrears" // 账户欠费
// ApiCallErrorNotSubscribed = "not_subscribed" // 未订阅产品
// ApiCallErrorProductNotFound = "product_not_found" // 产品不存在
// ApiCallErrorProductDisabled = "product_disabled" // 产品已停用
// ApiCallErrorSystem = "system_error" // 系统错误
// ApiCallErrorDatasource = "datasource_error" // 数据源异常
// ApiCallErrorInvalidParam = "invalid_param" // 参数不正确
// ApiCallErrorDecryptFail = "decrypt_fail" // 解密失败
const (
ApiCallErrorInvalidAccess = "invalid_access" // 无效AccessId
ApiCallErrorFrozenAccount = "frozen_account" // 账户冻结
ApiCallErrorInvalidIP = "invalid_ip" // IP无效
ApiCallErrorArrears = "arrears" // 账户欠费
ApiCallErrorNotSubscribed = "not_subscribed" // 未订阅产品
ApiCallErrorProductNotFound = "product_not_found" // 产品不存在
ApiCallErrorProductDisabled = "product_disabled" // 产品已停用
ApiCallErrorSystem = "system_error" // 系统错误
ApiCallErrorDatasource = "datasource_error" // 数据源异常
ApiCallErrorInvalidParam = "invalid_param" // 参数不正确
ApiCallErrorDecryptFail = "decrypt_fail" // 解密失败
ApiCallErrorQueryEmpty = "query_empty" // 查询为空
)
// ApiCall API调用聚合根
type ApiCall struct {
ID string `gorm:"type:varchar(64);primaryKey" json:"id"`
AccessId string `gorm:"type:varchar(64);not null;index" json:"access_id"`
UserId *string `gorm:"type:varchar(36);index" json:"user_id,omitempty"`
ProductId *string `gorm:"type:varchar(64);index" json:"product_id,omitempty"`
TransactionId string `gorm:"type:varchar(64);not null;uniqueIndex" json:"transaction_id"`
ClientIp string `gorm:"type:varchar(64);not null;index" json:"client_ip"`
RequestParams string `gorm:"type:text" json:"request_params"`
ResponseData *string `gorm:"type:text" json:"response_data,omitempty"`
Status string `gorm:"type:varchar(20);not null;default:'pending'" json:"status"`
StartAt time.Time `gorm:"not null;index" json:"start_at"`
EndAt *time.Time `gorm:"index" json:"end_at,omitempty"`
Cost *decimal.Decimal `gorm:"default:0" json:"cost,omitempty"`
ErrorType *string `gorm:"type:varchar(32)" json:"error_type,omitempty"`
ErrorMsg *string `gorm:"type:varchar(256)" json:"error_msg,omitempty"`
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"`
}
// NewApiCall 工厂方法
func NewApiCall(accessId, requestParams, clientIp string) (*ApiCall, error) {
if accessId == "" {
return nil, errors.New("AccessId不能为空")
}
if requestParams == "" {
return nil, errors.New("请求参数不能为空")
}
if clientIp == "" {
return nil, errors.New("ClientIp不能为空")
}
return &ApiCall{
ID: uuid.New().String(),
AccessId: accessId,
TransactionId: GenerateTransactionID(),
ClientIp: clientIp,
RequestParams: requestParams,
Status: ApiCallStatusPending,
StartAt: time.Now(),
}, nil
}
// MarkSuccess 标记为成功
func (a *ApiCall) MarkSuccess(responseData string, cost decimal.Decimal) error {
// 校验除ErrorMsg和ErrorType外所有字段不能为空
if a.ID == "" || a.AccessId == "" || a.TransactionId == "" || a.RequestParams == "" || a.Status == "" || a.StartAt.IsZero() {
return errors.New("ApiCall字段不能为空除ErrorMsg和ErrorType")
}
// 可选字段也要有值
if a.UserId == nil || a.ProductId == nil || responseData == "" {
return errors.New("ApiCall标记成功时UserId、ProductId、ResponseData不能为空")
}
a.Status = ApiCallStatusSuccess
a.ResponseData = &responseData
endAt := time.Now()
a.EndAt = &endAt
a.Cost = &cost
a.ErrorType = nil
a.ErrorMsg = nil
return nil
}
// MarkFailed 标记为失败
func (a *ApiCall) MarkFailed(errorType, errorMsg string) {
a.Status = ApiCallStatusFailed
a.ErrorType = &errorType
shortMsg := errorMsg
if len(shortMsg) > 120 {
shortMsg = shortMsg[:120]
}
a.ErrorMsg = &shortMsg
endAt := time.Now()
a.EndAt = &endAt
}
// Validate 校验ApiCall聚合根的业务规则
func (a *ApiCall) Validate() error {
if a.ID == "" {
return errors.New("ID不能为空")
}
if a.AccessId == "" {
return errors.New("AccessId不能为空")
}
if a.TransactionId == "" {
return errors.New("TransactionId不能为空")
}
if a.RequestParams == "" {
return errors.New("请求参数不能为空")
}
if a.Status != ApiCallStatusPending && a.Status != ApiCallStatusSuccess && a.Status != ApiCallStatusFailed {
return errors.New("无效的调用状态")
}
return nil
}
// 全局计数器用于确保TransactionID的唯一性
var (
transactionCounter int64
counterMutex sync.Mutex
)
// GenerateTransactionID 生成16位数的交易单号
func GenerateTransactionID() string {
// 使用互斥锁确保计数器的线程安全
counterMutex.Lock()
transactionCounter++
currentCounter := transactionCounter
counterMutex.Unlock()
// 获取当前时间戳(微秒精度)
timestamp := time.Now().UnixMicro()
// 组合时间戳和计数器,确保唯一性
combined := fmt.Sprintf("%d%06d", timestamp, currentCounter%1000000)
// 如果长度超出16位截断如果不够填充随机字符
if len(combined) >= 16 {
return combined[:16]
}
// 如果长度不够,使用随机字节填充
if len(combined) < 16 {
randomBytes := make([]byte, 8)
rand.Read(randomBytes)
randomHex := hex.EncodeToString(randomBytes)
combined += randomHex[:16-len(combined)]
}
return combined
}
// TableName 指定数据库表名
func (ApiCall) TableName() string {
return "api_calls"
}
// BeforeCreate GORM钩子创建前自动生成UUID
func (c *ApiCall) BeforeCreate(tx *gorm.DB) error {
if c.ID == "" {
c.ID = uuid.New().String()
}
return nil
}