2026-03-31 13:40:00 +08:00
|
|
|
package domain
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
2026-04-01 08:53:47 +08:00
|
|
|
"net/netip"
|
2026-03-31 13:40:00 +08:00
|
|
|
"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 {
|
2026-04-01 08:53:47 +08:00
|
|
|
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"`
|
2026-03-31 13:40:00 +08:00
|
|
|
Status AccountStatus `json:"status"`
|
2026-04-01 08:53:47 +08:00
|
|
|
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"`
|
2026-03-31 13:40:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 验证结果
|
|
|
|
|
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
|
|
|
|
|
}
|