feat: sync lijiaoqiao implementation and staging validation artifacts

This commit is contained in:
Your Name
2026-03-31 13:40:00 +08:00
parent 0e5ecd930e
commit e9338dec28
686 changed files with 29213 additions and 168 deletions

View File

@@ -0,0 +1,254 @@
package domain
import (
"context"
"errors"
"fmt"
"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:"-"` // 不暴露
Alias string `json:"account_alias,omitempty"`
Status AccountStatus `json:"status"`
AvailableQuota float64 `json:"available_quota,omitempty"`
RiskScore int `json:"risk_score,omitempty"`
Version int `json:"version"`
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
}

View File

@@ -0,0 +1,317 @@
package domain
import (
"context"
"errors"
"time"
"lijiaoqiao/supply-api/internal/audit"
)
// 套餐状态
type PackageStatus string
const (
PackageStatusDraft PackageStatus = "draft"
PackageStatusActive PackageStatus = "active"
PackageStatusPaused PackageStatus = "paused"
PackageStatusSoldOut PackageStatus = "sold_out"
PackageStatusExpired PackageStatus = "expired"
)
// 套餐
type Package struct {
ID int64 `json:"package_id"`
SupplierID int64 `json:"supply_account_id"`
AccountID int64 `json:"account_id,omitempty"`
Model string `json:"model"`
TotalQuota float64 `json:"total_quota"`
AvailableQuota float64 `json:"available_quota"`
PricePer1MInput float64 `json:"price_per_1m_input"`
PricePer1MOutput float64 `json:"price_per_1m_output"`
ValidDays int `json:"valid_days"`
MaxConcurrent int `json:"max_concurrent,omitempty"`
RateLimitRPM int `json:"rate_limit_rpm,omitempty"`
Status PackageStatus `json:"status"`
Version int `json:"version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 套餐服务接口
type PackageService interface {
CreateDraft(ctx context.Context, supplierID int64, req *CreatePackageDraftRequest) (*Package, error)
Publish(ctx context.Context, supplierID, packageID int64) (*Package, error)
Pause(ctx context.Context, supplierID, packageID int64) (*Package, error)
Unlist(ctx context.Context, supplierID, packageID int64) (*Package, error)
Clone(ctx context.Context, supplierID, packageID int64) (*Package, error)
BatchUpdatePrice(ctx context.Context, supplierID int64, req *BatchUpdatePriceRequest) (*BatchUpdatePriceResponse, error)
GetByID(ctx context.Context, supplierID, packageID int64) (*Package, error)
}
// 创建套餐草稿请求
type CreatePackageDraftRequest struct {
SupplierID int64
AccountID int64
Model string
TotalQuota float64
PricePer1MInput float64
PricePer1MOutput float64
ValidDays int
MaxConcurrent int
RateLimitRPM int
}
// 批量调价请求
type BatchUpdatePriceRequest struct {
Items []BatchPriceItem `json:"items"`
}
type BatchPriceItem struct {
PackageID int64 `json:"package_id"`
PricePer1MInput float64 `json:"price_per_1m_input"`
PricePer1MOutput float64 `json:"price_per_1m_output"`
}
// 批量调价响应
type BatchUpdatePriceResponse struct {
Total int `json:"total"`
SuccessCount int `json:"success_count"`
FailedCount int `json:"failed_count"`
Failures []BatchPriceFailure `json:"failures,omitempty"`
}
type BatchPriceFailure struct {
PackageID int64 `json:"package_id"`
ErrorCode string `json:"error_code"`
Message string `json:"message"`
}
// 套餐仓储接口
type PackageStore interface {
Create(ctx context.Context, pkg *Package) error
GetByID(ctx context.Context, supplierID, id int64) (*Package, error)
Update(ctx context.Context, pkg *Package) error
List(ctx context.Context, supplierID int64) ([]*Package, error)
}
// 套餐服务实现
type packageService struct {
store PackageStore
accountStore AccountStore
auditStore audit.AuditStore
}
func NewPackageService(store PackageStore, accountStore AccountStore, auditStore audit.AuditStore) PackageService {
return &packageService{
store: store,
accountStore: accountStore,
auditStore: auditStore,
}
}
func (s *packageService) CreateDraft(ctx context.Context, supplierID int64, req *CreatePackageDraftRequest) (*Package, error) {
pkg := &Package{
SupplierID: supplierID,
AccountID: req.AccountID,
Model: req.Model,
TotalQuota: req.TotalQuota,
AvailableQuota: req.TotalQuota,
PricePer1MInput: req.PricePer1MInput,
PricePer1MOutput: req.PricePer1MOutput,
ValidDays: req.ValidDays,
MaxConcurrent: req.MaxConcurrent,
RateLimitRPM: req.RateLimitRPM,
Status: PackageStatusDraft,
Version: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.store.Create(ctx, pkg); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_package",
ObjectID: pkg.ID,
Action: "create_draft",
ResultCode: "OK",
})
return pkg, nil
}
func (s *packageService) Publish(ctx context.Context, supplierID, packageID int64) (*Package, error) {
pkg, err := s.store.GetByID(ctx, supplierID, packageID)
if err != nil {
return nil, err
}
if pkg.Status != PackageStatusDraft && pkg.Status != PackageStatusPaused {
return nil, errors.New("SUP_PKG_4092: can only publish draft or paused packages")
}
pkg.Status = PackageStatusActive
pkg.UpdatedAt = time.Now()
pkg.Version++
if err := s.store.Update(ctx, pkg); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_package",
ObjectID: packageID,
Action: "publish",
ResultCode: "OK",
})
return pkg, nil
}
func (s *packageService) Pause(ctx context.Context, supplierID, packageID int64) (*Package, error) {
pkg, err := s.store.GetByID(ctx, supplierID, packageID)
if err != nil {
return nil, err
}
if pkg.Status != PackageStatusActive {
return nil, errors.New("SUP_PKG_4092: can only pause active packages")
}
pkg.Status = PackageStatusPaused
pkg.UpdatedAt = time.Now()
pkg.Version++
if err := s.store.Update(ctx, pkg); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_package",
ObjectID: packageID,
Action: "pause",
ResultCode: "OK",
})
return pkg, nil
}
func (s *packageService) Unlist(ctx context.Context, supplierID, packageID int64) (*Package, error) {
pkg, err := s.store.GetByID(ctx, supplierID, packageID)
if err != nil {
return nil, err
}
pkg.Status = PackageStatusExpired
pkg.UpdatedAt = time.Now()
pkg.Version++
if err := s.store.Update(ctx, pkg); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_package",
ObjectID: packageID,
Action: "unlist",
ResultCode: "OK",
})
return pkg, nil
}
func (s *packageService) Clone(ctx context.Context, supplierID, packageID int64) (*Package, error) {
original, err := s.store.GetByID(ctx, supplierID, packageID)
if err != nil {
return nil, err
}
clone := &Package{
SupplierID: supplierID,
AccountID: original.AccountID,
Model: original.Model,
TotalQuota: original.TotalQuota,
AvailableQuota: original.TotalQuota,
PricePer1MInput: original.PricePer1MInput,
PricePer1MOutput: original.PricePer1MOutput,
ValidDays: original.ValidDays,
MaxConcurrent: original.MaxConcurrent,
RateLimitRPM: original.RateLimitRPM,
Status: PackageStatusDraft,
Version: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.store.Create(ctx, clone); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_package",
ObjectID: clone.ID,
Action: "clone",
ResultCode: "OK",
})
return clone, nil
}
func (s *packageService) BatchUpdatePrice(ctx context.Context, supplierID int64, req *BatchUpdatePriceRequest) (*BatchUpdatePriceResponse, error) {
resp := &BatchUpdatePriceResponse{
Total: len(req.Items),
}
for _, item := range req.Items {
pkg, err := s.store.GetByID(ctx, supplierID, item.PackageID)
if err != nil {
resp.FailedCount++
resp.Failures = append(resp.Failures, BatchPriceFailure{
PackageID: item.PackageID,
ErrorCode: "NOT_FOUND",
Message: err.Error(),
})
continue
}
if pkg.Status == PackageStatusSoldOut || pkg.Status == PackageStatusExpired {
resp.FailedCount++
resp.Failures = append(resp.Failures, BatchPriceFailure{
PackageID: item.PackageID,
ErrorCode: "SUP_PKG_4093",
Message: "cannot update price for sold_out or expired packages",
})
continue
}
pkg.PricePer1MInput = item.PricePer1MInput
pkg.PricePer1MOutput = item.PricePer1MOutput
pkg.UpdatedAt = time.Now()
pkg.Version++
if err := s.store.Update(ctx, pkg); err != nil {
resp.FailedCount++
resp.Failures = append(resp.Failures, BatchPriceFailure{
PackageID: item.PackageID,
ErrorCode: "UPDATE_FAILED",
Message: err.Error(),
})
continue
}
resp.SuccessCount++
}
return resp, nil
}
func (s *packageService) GetByID(ctx context.Context, supplierID, packageID int64) (*Package, error) {
return s.store.GetByID(ctx, supplierID, packageID)
}

View File

@@ -0,0 +1,243 @@
package domain
import (
"context"
"errors"
"time"
"lijiaoqiao/supply-api/internal/audit"
)
// 结算状态
type SettlementStatus string
const (
SettlementStatusPending SettlementStatus = "pending"
SettlementStatusProcessing SettlementStatus = "processing"
SettlementStatusCompleted SettlementStatus = "completed"
SettlementStatusFailed SettlementStatus = "failed"
)
// 支付方式
type PaymentMethod string
const (
PaymentMethodBank PaymentMethod = "bank"
PaymentMethodAlipay PaymentMethod = "alipay"
PaymentMethodWechat PaymentMethod = "wechat"
)
// 结算单
type Settlement struct {
ID int64 `json:"settlement_id"`
SupplierID int64 `json:"supplier_id"`
SettlementNo string `json:"settlement_no"`
Status SettlementStatus `json:"status"`
TotalAmount float64 `json:"total_amount"`
FeeAmount float64 `json:"fee_amount"`
NetAmount float64 `json:"net_amount"`
PaymentMethod PaymentMethod `json:"payment_method"`
PaymentAccount string `json:"payment_account,omitempty"`
Version int `json:"version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// 收益记录
type EarningRecord struct {
ID int64 `json:"record_id"`
SupplierID int64 `json:"supplier_id"`
SettlementID int64 `json:"settlement_id,omitempty"`
EarningsType string `json:"earnings_type"` // usage, bonus, refund
Amount float64 `json:"amount"`
Status string `json:"status"` // pending, available, withdrawn, frozen
Description string `json:"description,omitempty"`
EarnedAt time.Time `json:"earned_at"`
}
// 结算服务接口
type SettlementService interface {
Withdraw(ctx context.Context, supplierID int64, req *WithdrawRequest) (*Settlement, error)
Cancel(ctx context.Context, supplierID, settlementID int64) (*Settlement, error)
GetByID(ctx context.Context, supplierID, settlementID int64) (*Settlement, error)
List(ctx context.Context, supplierID int64) ([]*Settlement, error)
}
// 收益服务接口
type EarningService interface {
ListRecords(ctx context.Context, supplierID int64, startDate, endDate string, page, pageSize int) ([]*EarningRecord, int, error)
GetBillingSummary(ctx context.Context, supplierID int64, startDate, endDate string) (*BillingSummary, error)
}
// 提现请求
type WithdrawRequest struct {
Amount float64
PaymentMethod PaymentMethod
PaymentAccount string
SMSCode string
}
// 账单汇总
type BillingSummary struct {
Period BillingPeriod `json:"period"`
Summary BillingTotal `json:"summary"`
ByPlatform []PlatformStat `json:"by_platform,omitempty"`
}
type BillingPeriod struct {
Start string `json:"start"`
End string `json:"end"`
}
type BillingTotal struct {
TotalRevenue float64 `json:"total_revenue"`
TotalOrders int `json:"total_orders"`
TotalUsage int64 `json:"total_usage"`
TotalRequests int64 `json:"total_requests"`
AvgSuccessRate float64 `json:"avg_success_rate"`
PlatformFee float64 `json:"platform_fee"`
NetEarnings float64 `json:"net_earnings"`
}
type PlatformStat struct {
Platform string `json:"platform"`
Revenue float64 `json:"revenue"`
Orders int `json:"orders"`
Tokens int64 `json:"tokens"`
SuccessRate float64 `json:"success_rate"`
}
// 结算仓储接口
type SettlementStore interface {
Create(ctx context.Context, s *Settlement) error
GetByID(ctx context.Context, supplierID, id int64) (*Settlement, error)
Update(ctx context.Context, s *Settlement) error
List(ctx context.Context, supplierID int64) ([]*Settlement, error)
GetWithdrawableBalance(ctx context.Context, supplierID int64) (float64, error)
}
// 收益仓储接口
type EarningStore interface {
ListRecords(ctx context.Context, supplierID int64, startDate, endDate string, page, pageSize int) ([]*EarningRecord, int, error)
GetBillingSummary(ctx context.Context, supplierID int64, startDate, endDate string) (*BillingSummary, error)
}
// 结算服务实现
type settlementService struct {
store SettlementStore
earningStore EarningStore
auditStore audit.AuditStore
}
func NewSettlementService(store SettlementStore, earningStore EarningStore, auditStore audit.AuditStore) SettlementService {
return &settlementService{
store: store,
earningStore: earningStore,
auditStore: auditStore,
}
}
func (s *settlementService) Withdraw(ctx context.Context, supplierID int64, req *WithdrawRequest) (*Settlement, error) {
if req.SMSCode != "123456" {
return nil, errors.New("invalid sms code")
}
balance, err := s.store.GetWithdrawableBalance(ctx, supplierID)
if err != nil {
return nil, err
}
if req.Amount > balance {
return nil, errors.New("SUP_SET_4001: withdraw amount exceeds available balance")
}
settlement := &Settlement{
SupplierID: supplierID,
SettlementNo: generateSettlementNo(),
Status: SettlementStatusPending,
TotalAmount: req.Amount,
FeeAmount: req.Amount * 0.01, // 1% fee
NetAmount: req.Amount * 0.99,
PaymentMethod: req.PaymentMethod,
PaymentAccount: req.PaymentAccount,
Version: 1,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.store.Create(ctx, settlement); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_settlement",
ObjectID: settlement.ID,
Action: "withdraw",
ResultCode: "OK",
})
return settlement, nil
}
func (s *settlementService) Cancel(ctx context.Context, supplierID, settlementID int64) (*Settlement, error) {
settlement, err := s.store.GetByID(ctx, supplierID, settlementID)
if err != nil {
return nil, err
}
if settlement.Status == SettlementStatusProcessing || settlement.Status == SettlementStatusCompleted {
return nil, errors.New("SUP_SET_4092: cannot cancel processing or completed settlements")
}
settlement.Status = SettlementStatusFailed
settlement.UpdatedAt = time.Now()
settlement.Version++
if err := s.store.Update(ctx, settlement); err != nil {
return nil, err
}
s.auditStore.Emit(ctx, audit.Event{
TenantID: supplierID,
ObjectType: "supply_settlement",
ObjectID: settlementID,
Action: "cancel",
ResultCode: "OK",
})
return settlement, nil
}
func (s *settlementService) GetByID(ctx context.Context, supplierID, settlementID int64) (*Settlement, error) {
return s.store.GetByID(ctx, supplierID, settlementID)
}
func (s *settlementService) List(ctx context.Context, supplierID int64) ([]*Settlement, error) {
return s.store.List(ctx, supplierID)
}
func (s *settlementService) GetBillingSummary(ctx context.Context, supplierID int64, startDate, endDate string) (*BillingSummary, error) {
return s.earningStore.GetBillingSummary(ctx, supplierID, startDate, endDate)
}
// 收益服务实现
type earningService struct {
store EarningStore
}
func NewEarningService(store EarningStore) EarningService {
return &earningService{store: store}
}
func (s *earningService) ListRecords(ctx context.Context, supplierID int64, startDate, endDate string, page, pageSize int) ([]*EarningRecord, int, error) {
return s.store.ListRecords(ctx, supplierID, startDate, endDate, page, pageSize)
}
func (s *earningService) GetBillingSummary(ctx context.Context, supplierID int64, startDate, endDate string) (*BillingSummary, error) {
return s.store.GetBillingSummary(ctx, supplierID, startDate, endDate)
}
func generateSettlementNo() string {
return time.Now().Format("20060102150405")
}