Files
lijiaoqiao/supply-api/internal/domain/account.go
Your Name ed0961d486 fix(supply-api): 修复编译错误和测试问题
- 添加 ErrNotFound 和 ErrConcurrencyConflict 错误定义
- 修复 pgx.NullTime 替换为 *time.Time
- 修复 db.go 事务类型 (pgx.Tx vs pgxpool.Tx)
- 移除未使用的导入和变量
- 修复 NewSupplyAPI 调用参数
- 修复中间件链路 handler 类型问题
- 修复适配器类型引用 (storage.InMemoryAccountStore 等)
- 所有测试通过

Test: go test ./...
2026-04-01 13:03:44 +08:00

289 lines
8.3 KiB
Go

package domain
import (
"context"
"errors"
"fmt"
"net/netip"
"time"
"lijiaoqiao/supply-api/internal/audit"
)
// 账号状态
type AccountStatus string
const (
AccountStatusPending AccountStatus = "pending"
AccountStatusActive AccountStatus = "active"
AccountStatusSuspended AccountStatus = "suspended"
AccountStatusDisabled AccountStatus = "disabled"
)
// 账号类型
type AccountType string
const (
AccountTypeAPIKey AccountType = "api_key"
AccountTypeOAuth AccountType = "oauth"
)
// 供应商
type Provider string
const (
ProviderOpenAI Provider = "openai"
ProviderAnthropic Provider = "anthropic"
ProviderGemini Provider = "gemini"
ProviderBaidu Provider = "baidu"
ProviderXfyun Provider = "xfyun"
ProviderTencent Provider = "tencent"
)
// 账号
type Account struct {
ID int64 `json:"account_id"`
SupplierID int64 `json:"supplier_id"`
Provider Provider `json:"provider"`
AccountType AccountType `json:"account_type"`
CredentialHash string `json:"-"` // 不暴露
KeyID string `json:"key_id,omitempty"` // 不暴露
Alias string `json:"account_alias,omitempty"`
Status AccountStatus `json:"status"`
RiskLevel string `json:"risk_level"`
TotalQuota float64 `json:"total_quota,omitempty"`
AvailableQuota float64 `json:"available_quota,omitempty"`
FrozenQuota float64 `json:"frozen_quota,omitempty"`
IsVerified bool `json:"is_verified"`
VerifiedAt *time.Time `json:"verified_at,omitempty"`
LastCheckAt *time.Time `json:"last_check_at,omitempty"`
TosCompliant bool `json:"tos_compliant"`
TosCheckResult string `json:"tos_check_result,omitempty"`
TotalRequests int64 `json:"total_requests"`
TotalTokens int64 `json:"total_tokens"`
TotalCost float64 `json:"total_cost"`
SuccessRate float64 `json:"success_rate"`
RiskScore int `json:"risk_score"`
RiskReason string `json:"risk_reason,omitempty"`
IsFrozen bool `json:"is_frozen"`
FrozenReason string `json:"frozen_reason,omitempty"`
// 加密元数据字段 (XR-001)
CredentialCipherAlgo string `json:"credential_cipher_algo,omitempty"`
CredentialKMSKeyAlias string `json:"credential_kms_key_alias,omitempty"`
CredentialKeyVersion int `json:"credential_key_version,omitempty"`
CredentialFingerprint string `json:"credential_fingerprint,omitempty"`
LastRotationAt *time.Time `json:"last_rotation_at,omitempty"`
// 单位与币种 (XR-001)
QuotaUnit string `json:"quota_unit"`
CurrencyCode string `json:"currency_code"`
// 审计字段 (XR-001)
Version int `json:"version"`
CreatedIP *netip.Addr `json:"created_ip,omitempty"`
UpdatedIP *netip.Addr `json:"updated_ip,omitempty"`
AuditTraceID string `json:"audit_trace_id,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 验证结果
type VerifyResult struct {
VerifyStatus string `json:"verify_status"` // pass, review_required, reject
AvailableQuota float64 `json:"available_quota,omitempty"`
RiskScore int `json:"risk_score"`
CheckItems []CheckItem `json:"check_items,omitempty"`
}
type CheckItem struct {
Item string `json:"item"`
Result string `json:"result"` // pass, fail, warn
Message string `json:"message,omitempty"`
}
// 账号服务接口
type AccountService interface {
Verify(ctx context.Context, supplierID int64, provider Provider, accountType AccountType, credential string) (*VerifyResult, error)
Create(ctx context.Context, req *CreateAccountRequest) (*Account, error)
Activate(ctx context.Context, supplierID, accountID int64) (*Account, error)
Suspend(ctx context.Context, supplierID, accountID int64) (*Account, error)
Delete(ctx context.Context, supplierID, accountID int64) error
GetByID(ctx context.Context, supplierID, accountID int64) (*Account, error)
}
// 创建账号请求
type CreateAccountRequest struct {
SupplierID int64
Provider Provider
AccountType AccountType
Credential string
Alias string
RiskAck bool
}
// 账号仓储接口
type AccountStore interface {
Create(ctx context.Context, account *Account) error
GetByID(ctx context.Context, supplierID, id int64) (*Account, error)
Update(ctx context.Context, account *Account) error
List(ctx context.Context, supplierID int64) ([]*Account, error)
}
// 账号服务实现
type accountService struct {
store AccountStore
auditStore audit.AuditStore
}
func NewAccountService(store AccountStore, auditStore audit.AuditStore) AccountService {
return &accountService{store: store, auditStore: auditStore}
}
func (s *accountService) Verify(ctx context.Context, supplierID int64, provider Provider, accountType AccountType, credential string) (*VerifyResult, error) {
// 开发阶段:模拟验证逻辑
result := &VerifyResult{
VerifyStatus: "pass",
RiskScore: 10,
CheckItems: []CheckItem{
{Item: "credential_format", Result: "pass", Message: "凭证格式正确"},
{Item: "provider_connectivity", Result: "pass", Message: "供应商连接正常"},
{Item: "quota_availability", Result: "pass", Message: "额度可用"},
},
}
// 模拟获取额度
result.AvailableQuota = 1000.0
return result, nil
}
func (s *accountService) Create(ctx context.Context, req *CreateAccountRequest) (*Account, error) {
if !req.RiskAck {
return nil, errors.New("risk_ack is required")
}
account := &Account{
SupplierID: req.SupplierID,
Provider: req.Provider,
AccountType: req.AccountType,
CredentialHash: hashCredential(req.Credential),
Alias: req.Alias,
Status: AccountStatusPending,
Version: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.store.Create(ctx, account); err != nil {
return nil, err
}
// 记录审计日志
s.auditStore.Emit(ctx, audit.Event{
TenantID: req.SupplierID,
ObjectType: "supply_account",
ObjectID: account.ID,
Action: "create",
ResultCode: "OK",
})
return account, nil
}
func (s *accountService) Activate(ctx context.Context, supplierID, accountID int64) (*Account, error) {
account, err := s.store.GetByID(ctx, supplierID, accountID)
if err != nil {
return nil, err
}
if account.Status != AccountStatusPending && account.Status != AccountStatusSuspended {
return nil, errors.New("SUP_ACC_4091: can only activate pending or suspended accounts")
}
account.Status = AccountStatusActive
account.UpdatedAt = time.Now()
account.Version++
if err := s.store.Update(ctx, account); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_account",
ObjectID: accountID,
Action: "activate",
ResultCode: "OK",
})
return account, nil
}
func (s *accountService) Suspend(ctx context.Context, supplierID, accountID int64) (*Account, error) {
account, err := s.store.GetByID(ctx, supplierID, accountID)
if err != nil {
return nil, err
}
if account.Status != AccountStatusActive {
return nil, errors.New("SUP_ACC_4091: can only suspend active accounts")
}
account.Status = AccountStatusSuspended
account.UpdatedAt = time.Now()
account.Version++
if err := s.store.Update(ctx, account); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_account",
ObjectID: accountID,
Action: "suspend",
ResultCode: "OK",
})
return account, nil
}
func (s *accountService) Delete(ctx context.Context, supplierID, accountID int64) error {
account, err := s.store.GetByID(ctx, supplierID, accountID)
if err != nil {
return err
}
if account.Status == AccountStatusActive {
return errors.New("SUP_ACC_4092: cannot delete active accounts")
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_account",
ObjectID: accountID,
Action: "delete",
ResultCode: "OK",
})
return nil
}
func (s *accountService) GetByID(ctx context.Context, supplierID, accountID int64) (*Account, error) {
return s.store.GetByID(ctx, supplierID, accountID)
}
func hashCredential(cred string) string {
// 开发阶段简单实现
return fmt.Sprintf("hash_%s", cred[:min(8, len(cred))])
}
func min(a, b int) int {
if a < b {
return a
}
return b
}