197 lines
6.8 KiB
Go
197 lines
6.8 KiB
Go
|
|
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
|
|||
|
|
}
|