diff --git a/supply-api/internal/benchmark/README.md b/supply-api/internal/benchmark/README.md new file mode 100644 index 00000000..72362bd2 --- /dev/null +++ b/supply-api/internal/benchmark/README.md @@ -0,0 +1,76 @@ +# 性能回归测试 + +本目录包含 Supply API 的性能回归测试,确保关键路径的性能不会随着代码变更而退化。 + +## 测试文件 + +| 文件 | 说明 | +|------|------| +| `domain_bench_test.go` | 领域模型性能测试(账号、套餐、结算服务) | +| `middleware_bench_test.go` | 中间件性能测试(认证、限流、追踪、幂等) | + +## 运行方式 + +```bash +# 运行所有性能测试(跳过快速测试) +go test -tags=slow ./internal/benchmark/... + +# 运行特定基准测试 +go test -tags=slow -run=XXX ./internal/benchmark/... -bench=BenchmarkAccountService_Create + +# 查看内存分配 +go test -tags=slow ./internal/benchmark/... -bench=BenchmarkAccountService_Create -benchmem + +# 输出详细信息 +go test -tags=slow ./internal/benchmark/... -bench=BenchmarkAccountService_Create -v +``` + +## 性能目标 + +| 模块 | 操作 | 目标 | 阈值 | +|------|------|------|------| +| AccountService | Create | < 1ms | 5ms | +| AccountService | Verify | < 5ms | 10ms | +| PackageService | CreateDraft | < 2ms | 10ms | +| PackageService | BatchUpdatePrice(50) | < 20ms | 50ms | +| SettlementService | Withdraw | < 5ms | 20ms | +| Middleware | Auth | < 0.5ms | 1ms | +| Middleware | RateLimit | < 0.2ms | 0.5ms | +| Middleware | Tracing | < 0.1ms | 0.5ms | + +## CI/CD 集成 + +性能测试在 CI/CD 中作为门禁检查: + +```yaml +# .github/workflows/performance.yml +- name: Run Performance Tests + run: | + go test -tags=slow -bench=. -benchmem ./internal/benchmark/... \ + | tee performance_report.txt +``` + +## 添加新的性能测试 + +```go +//go:build slow +// +build slow + +package benchmark + +func BenchmarkYourOperation(b *testing.B) { + if testing.Short() { + b.Skip("Skipping benchmark in short mode") + } + + // 准备测试数据 + setup() + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + // 执行被测操作 + } +} +``` diff --git a/supply-api/internal/benchmark/domain_bench_test.go b/supply-api/internal/benchmark/domain_bench_test.go index cd6f9f65..fde7c103 100644 --- a/supply-api/internal/benchmark/domain_bench_test.go +++ b/supply-api/internal/benchmark/domain_bench_test.go @@ -340,6 +340,14 @@ func (m *mockSettlementStoreForBenchmark) Create(ctx context.Context, s *domain. return nil } +func (m *mockSettlementStoreForBenchmark) CreateWithdrawTx(ctx context.Context, s *domain.Settlement) error { + return m.Create(ctx, s) +} + +func (m *mockSettlementStoreForBenchmark) CreateInTx(ctx context.Context, s *domain.Settlement) error { + return m.Create(ctx, s) +} + func (m *mockSettlementStoreForBenchmark) GetByID(ctx context.Context, supplierID, id int64) (*domain.Settlement, error) { if s, ok := m.settlements[id]; ok && s.SupplierID == supplierID { return s, nil diff --git a/supply-api/internal/benchmark/middleware_bench_test.go b/supply-api/internal/benchmark/middleware_bench_test.go new file mode 100644 index 00000000..d0bc261b --- /dev/null +++ b/supply-api/internal/benchmark/middleware_bench_test.go @@ -0,0 +1,115 @@ +//go:build slow +// +build slow + +package benchmark + +import ( + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + "lijiaoqiao/supply-api/internal/pkg/logging" + "lijiaoqiao/supply-api/internal/middleware" +) + +// mockLogger 用于基准测试的 mock Logger +type mockLogger struct{} + +func (m *mockLogger) Debug(msg string, fields ...map[string]interface{}) {} +func (m *mockLogger) Info(msg string, fields ...map[string]interface{}) {} +func (m *mockLogger) Warn(msg string, fields ...map[string]interface{}) {} +func (m *mockLogger) Error(msg string, fields ...map[string]interface{}) {} +func (m *mockLogger) Fatal(msg string, fields ...map[string]interface{}) {} +func (m *mockLogger) WithFields(fields map[string]interface{}) logging.Logger { return m } + +// BenchmarkLoggingMiddleware 基准测试:日志中间件性能 +func BenchmarkLoggingMiddleware(b *testing.B) { + if testing.Short() { + b.Skip("Skipping benchmark in short mode") + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + logger := &mockLogger{} + loggingHandler := middleware.Logging(handler, logger) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + req := httptest.NewRequest("GET", "/test", nil) + rec := httptest.NewRecorder() + loggingHandler.ServeHTTP(rec, req) + } +} + +// BenchmarkTracingMiddleware 基准测试:追踪中间件性能 +func BenchmarkTracingMiddleware(b *testing.B) { + if testing.Short() { + b.Skip("Skipping benchmark in short mode") + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + tracingHandler := middleware.TracingMiddleware(handler) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("traceparent", "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01") + rec := httptest.NewRecorder() + tracingHandler.ServeHTTP(rec, req) + } +} + +// BenchmarkTimeoutMiddleware 基准测试:超时中间件性能 +func BenchmarkTimeoutMiddleware(b *testing.B) { + if testing.Short() { + b.Skip("Skipping benchmark in short mode") + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + // 使用足够长的超时时间(100ms),确保 handler 几乎总是正常完成 + // 避免因超时设置过短导致的 race 条件 + timeoutHandler := middleware.WithTimeoutMiddleware(handler, 100*time.Millisecond) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + req := httptest.NewRequest("GET", "/test", nil) + rec := httptest.NewRecorder() + timeoutHandler.ServeHTTP(rec, req) + } +} + +// BenchmarkHTTPHandler_Empty 基准测试:空 Handler 性能(基线) +func BenchmarkHTTPHandler_Empty(b *testing.B) { + if testing.Short() { + b.Skip("Skipping benchmark in short mode") + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "OK") + }) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + req := httptest.NewRequest("GET", "/test", nil) + rec := httptest.NewRecorder() + handler.ServeHTTP(rec, req) + } +} diff --git a/supply-api/internal/testutil/assert/assertions.go b/supply-api/internal/testutil/assert/assertions.go new file mode 100644 index 00000000..294bcb47 --- /dev/null +++ b/supply-api/internal/testutil/assert/assertions.go @@ -0,0 +1,93 @@ +package assert + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "lijiaoqiao/supply-api/internal/domain" +) + +// Account 相关的自定义断言 + +// AssertAccountEqual 断言两个账号相等(忽略指针差异) +func AssertAccountEqual(t *testing.T, expected, actual *domain.Account, msgAndArgs ...interface{}) { + assert.Equal(t, expected, actual, msgAndArgs...) +} + +// AssertAccountStatus 断言账号状态 +func AssertAccountStatus(t *testing.T, account *domain.Account, expectedStatus domain.AccountStatus, msgAndArgs ...interface{}) { + assert.Equal(t, expectedStatus, account.Status, msgAndArgs...) +} + +// AssertAccountFields 断言账号字段 +func AssertAccountFields(t *testing.T, account *domain.Account, supplierID int64, provider domain.Provider, accountType domain.AccountType, msgAndArgs ...interface{}) { + assert.Equal(t, supplierID, account.SupplierID, msgAndArgs...) + assert.Equal(t, provider, account.Provider, msgAndArgs...) + assert.Equal(t, accountType, account.AccountType, msgAndArgs...) +} + +// Package 相关的自定义断言 + +// AssertPackageEqual 断言两个套餐相等 +func AssertPackageEqual(t *testing.T, expected, actual *domain.Package, msgAndArgs ...interface{}) { + assert.Equal(t, expected, actual, msgAndArgs...) +} + +// AssertPackageStatus 断言套餐状态 +func AssertPackageStatus(t *testing.T, pkg *domain.Package, expectedStatus domain.PackageStatus, msgAndArgs ...interface{}) { + assert.Equal(t, expectedStatus, pkg.Status, msgAndArgs...) +} + +// AssertPackageQuota 断言套餐配额 +func AssertPackageQuota(t *testing.T, pkg *domain.Package, total, available, sold float64, msgAndArgs ...interface{}) { + assert.InDelta(t, total, pkg.TotalQuota, 0.001, msgAndArgs...) + assert.InDelta(t, available, pkg.AvailableQuota, 0.001, msgAndArgs...) + assert.InDelta(t, sold, pkg.SoldQuota, 0.001, msgAndArgs...) +} + +// Settlement 相关的自定义断言 + +// AssertSettlementEqual 断言两个结算单相等 +func AssertSettlementEqual(t *testing.T, expected, actual *domain.Settlement, msgAndArgs ...interface{}) { + assert.Equal(t, expected, actual, msgAndArgs...) +} + +// AssertSettlementStatus 断言结算单状态 +func AssertSettlementStatus(t *testing.T, s *domain.Settlement, expectedStatus domain.SettlementStatus, msgAndArgs ...interface{}) { + assert.Equal(t, expectedStatus, s.Status, msgAndArgs...) +} + +// AssertSettlementAmount 断言结算单金额 +func AssertSettlementAmount(t *testing.T, s *domain.Settlement, total, fee, net float64, msgAndArgs ...interface{}) { + assert.InDelta(t, total, s.TotalAmount, 0.001, msgAndArgs...) + assert.InDelta(t, fee, s.FeeAmount, 0.001, msgAndArgs...) + assert.InDelta(t, net, s.NetAmount, 0.001, msgAndArgs...) +} + +// EarningRecord 相关的自定义断言 + +// AssertEarningRecordEqual 断言两条收益记录相等 +func AssertEarningRecordEqual(t *testing.T, expected, actual *domain.EarningRecord, msgAndArgs ...interface{}) { + assert.Equal(t, expected, actual, msgAndArgs...) +} + +// Error 相关的自定义断言 + +// AssertErrorCode 断言错误码包含特定字符串 +func AssertErrorCode(t *testing.T, err error, code string, msgAndArgs ...interface{}) { + if err == nil { + assert.Fail(t, "Expected error but got nil", msgAndArgs...) + return + } + assert.Contains(t, err.Error(), code, msgAndArgs...) +} + +// AssertErrorMessage 断言错误信息 +func AssertErrorMessage(t *testing.T, err error, msg string, msgAndArgs ...interface{}) { + if err == nil { + assert.Fail(t, "Expected error but got nil", msgAndArgs...) + return + } + assert.Contains(t, err.Error(), msg, msgAndArgs...) +} diff --git a/supply-api/internal/testutil/factory/account.go b/supply-api/internal/testutil/factory/account.go new file mode 100644 index 00000000..f379e4e7 --- /dev/null +++ b/supply-api/internal/testutil/factory/account.go @@ -0,0 +1,126 @@ +package factory + +import ( + "lijiaoqiao/supply-api/internal/domain" +) + +// AccountFactory 账号测试数据工厂 +type AccountFactory struct { + supplierID int64 + provider domain.Provider + accountType domain.AccountType + credential string + alias string + riskAck bool +} + +// NewAccountFactory 创建默认账号工厂 +func NewAccountFactory() *AccountFactory { + return &AccountFactory{ + supplierID: 1001, + provider: domain.ProviderOpenAI, + accountType: domain.AccountTypeAPIKey, + credential: "sk-test-key-" + randomString(8), + alias: "test-account", + riskAck: true, + } +} + +// WithSupplierID 设置供应商ID +func (f *AccountFactory) WithSupplierID(id int64) *AccountFactory { + f.supplierID = id + return f +} + +// WithProvider 设置提供商 +func (f *AccountFactory) WithProvider(p domain.Provider) *AccountFactory { + f.provider = p + return f +} + +// WithAccountType 设置账号类型 +func (f *AccountFactory) WithAccountType(t domain.AccountType) *AccountFactory { + f.accountType = t + return f +} + +// WithCredential 设置凭证 +func (f *AccountFactory) WithCredential(cred string) *AccountFactory { + f.credential = cred + return f +} + +// WithAlias 设置别名 +func (f *AccountFactory) WithAlias(alias string) *AccountFactory { + f.alias = alias + return f +} + +// WithRiskAck 设置风险确认 +func (f *AccountFactory) WithRiskAck(ack bool) *AccountFactory { + f.riskAck = ack + return f +} + +// BuildRequest 构建创建账号请求 +func (f *AccountFactory) BuildRequest() *domain.CreateAccountRequest { + return &domain.CreateAccountRequest{ + SupplierID: f.supplierID, + Provider: f.provider, + AccountType: f.accountType, + Credential: f.credential, + Alias: f.alias, + RiskAck: f.riskAck, + } +} + +// Build 构建账号对象 +func (f *AccountFactory) Build() *domain.Account { + return &domain.Account{ + ID: 1, + SupplierID: f.supplierID, + Provider: f.provider, + AccountType: f.accountType, + CredentialHash: f.credential, + KeyID: "key-" + randomString(8), + Alias: f.alias, + Status: domain.AccountStatusPending, + RiskLevel: "low", + TosCompliant: f.riskAck, + Version: 1, + } +} + +// BuildActive 构建活跃状态的账号 +func (f *AccountFactory) BuildActive() *domain.Account { + account := f.Build() + account.Status = domain.AccountStatusActive + return account +} + +// BuildSuspended 构建暂停状态的账号 +func (f *AccountFactory) BuildSuspended() *domain.Account { + account := f.Build() + account.Status = domain.AccountStatusSuspended + return account +} + +// BuildInvalidCredential 构建无效凭证的工厂 +func (f *AccountFactory) BuildInvalidCredential() *AccountFactory { + return f.WithCredential("") +} + +// BuildWithoutRiskAck 构建未确认风险的工厂 +func (f *AccountFactory) BuildWithoutRiskAck() *AccountFactory { + return f.WithRiskAck(false) +} + +// randomString 生成随机字符串(简化版) +func randomString(n int) string { + const letters = "abcdefghijklmnopqrstuvwxyz0123456789" + b := make([]byte, n) + for i := range b { + b[i] = letters[i%len(letters)] + } + return string(b) +} diff --git a/supply-api/internal/testutil/factory/package.go b/supply-api/internal/testutil/factory/package.go new file mode 100644 index 00000000..e0ff98a0 --- /dev/null +++ b/supply-api/internal/testutil/factory/package.go @@ -0,0 +1,175 @@ +package factory + +import ( + "time" + + "lijiaoqiao/supply-api/internal/domain" +) + +// PackageFactory 套餐测试数据工厂 +type PackageFactory struct { + supplierID int64 + accountID int64 + model string + totalQuota float64 + availableQuota float64 + soldQuota float64 + reservedQuota float64 + pricePer1MInput float64 + pricePer1MOutput float64 + validDays int + maxConcurrent int + rateLimitRPM int + status domain.PackageStatus + quotaUnit string + priceUnit string + currencyCode string +} + +// NewPackageFactory 创建默认套餐工厂 +func NewPackageFactory() *PackageFactory { + return &PackageFactory{ + supplierID: 1001, + accountID: 1, + model: "gpt-4o-mini", + totalQuota: 1000000, + availableQuota: 1000000, + soldQuota: 0, + reservedQuota: 0, + pricePer1MInput: 0.5, + pricePer1MOutput: 1.5, + validDays: 30, + maxConcurrent: 10, + rateLimitRPM: 1000, + status: domain.PackageStatusDraft, + quotaUnit: "tokens", + priceUnit: "USD", + currencyCode: "USDT", + } +} + +// WithSupplierID 设置供应商ID +func (f *PackageFactory) WithSupplierID(id int64) *PackageFactory { + f.supplierID = id + return f +} + +// WithAccountID 设置账户ID +func (f *PackageFactory) WithAccountID(id int64) *PackageFactory { + f.accountID = id + return f +} + +// WithModel 设置模型 +func (f *PackageFactory) WithModel(model string) *PackageFactory { + f.model = model + return f +} + +// WithQuota 设置配额 +func (f *PackageFactory) WithQuota(total, available float64) *PackageFactory { + f.totalQuota = total + f.availableQuota = available + return f +} + +// WithPrice 设置价格 +func (f *PackageFactory) WithPrice(input, output float64) *PackageFactory { + f.pricePer1MInput = input + f.pricePer1MOutput = output + return f +} + +// WithValidDays 设置有效期 +func (f *PackageFactory) WithValidDays(days int) *PackageFactory { + f.validDays = days + return f +} + +// WithStatus 设置状态 +func (f *PackageFactory) WithStatus(status domain.PackageStatus) *PackageFactory { + f.status = status + return f +} + +// BuildRequest 构建创建套餐草稿请求 +func (f *PackageFactory) BuildRequest() *domain.CreatePackageDraftRequest { + return &domain.CreatePackageDraftRequest{ + SupplierID: f.supplierID, + AccountID: f.accountID, + Model: f.model, + TotalQuota: f.totalQuota, + PricePer1MInput: f.pricePer1MInput, + PricePer1MOutput: f.pricePer1MOutput, + ValidDays: f.validDays, + MaxConcurrent: f.maxConcurrent, + RateLimitRPM: f.rateLimitRPM, + } +} + +// Build 构建套餐对象 +func (f *PackageFactory) Build() *domain.Package { + now := time.Now() + return &domain.Package{ + ID: 1, + SupplierID: f.supplierID, + AccountID: f.accountID, + Model: f.model, + TotalQuota: f.totalQuota, + AvailableQuota: f.availableQuota, + SoldQuota: f.soldQuota, + ReservedQuota: f.reservedQuota, + PricePer1MInput: f.pricePer1MInput, + PricePer1MOutput: f.pricePer1MOutput, + ValidDays: f.validDays, + MaxConcurrent: f.maxConcurrent, + RateLimitRPM: f.rateLimitRPM, + Status: f.status, + TotalOrders: 0, + TotalRevenue: 0, + Rating: 0, + RatingCount: 0, + QuotaUnit: f.quotaUnit, + PriceUnit: f.priceUnit, + CurrencyCode: f.currencyCode, + Version: 1, + CreatedAt: now, + UpdatedAt: now, + } +} + +// BuildActive 构建活跃状态的套餐 +func (f *PackageFactory) BuildActive() *domain.Package { + pkg := f.Build() + pkg.Status = domain.PackageStatusActive + return pkg +} + +// BuildPaused 构建暂停状态的套餐 +func (f *PackageFactory) BuildPaused() *domain.Package { + pkg := f.Build() + pkg.Status = domain.PackageStatusPaused + return pkg +} + +// BuildSoldOut 构建售罄状态的套餐 +func (f *PackageFactory) BuildSoldOut() *domain.Package { + pkg := f.Build() + pkg.Status = domain.PackageStatusSoldOut + pkg.AvailableQuota = 0 + return pkg +} + +// BuildExpired 构建过期状态的套餐 +func (f *PackageFactory) BuildExpired() *domain.Package { + pkg := f.Build() + pkg.Status = domain.PackageStatusExpired + return pkg +} + +// BuildWithID 构建带指定ID的套餐 +func (f *PackageFactory) BuildWithID(id int64) *domain.Package { + pkg := f.Build() + pkg.ID = id + return pkg +} diff --git a/supply-api/internal/testutil/factory/settlement.go b/supply-api/internal/testutil/factory/settlement.go new file mode 100644 index 00000000..2ffab07b --- /dev/null +++ b/supply-api/internal/testutil/factory/settlement.go @@ -0,0 +1,174 @@ +package factory + +import ( + "time" + + "lijiaoqiao/supply-api/internal/domain" +) + +// SettlementFactory 结算测试数据工厂 +type SettlementFactory struct { + supplierID int64 + settlementNo string + status domain.SettlementStatus + totalAmount float64 + feeAmount float64 + netAmount float64 + paymentMethod domain.PaymentMethod + paymentAccount string + paymentTransactionID string + periodStart time.Time + periodEnd time.Time + totalOrders int + totalUsageRecords int + currencyCode string + amountUnit string + requestID string + idempotencyKey string +} + +// NewSettlementFactory 创建默认结算工厂 +func NewSettlementFactory() *SettlementFactory { + now := time.Now() + return &SettlementFactory{ + supplierID: 1001, + settlementNo: "SET" + now.Format("20060102150405"), + status: domain.SettlementStatusPending, + totalAmount: 1000.00, + feeAmount: 10.00, + netAmount: 990.00, + paymentMethod: domain.PaymentMethodBank, + paymentAccount: "bank-1234567890", + periodStart: now.AddDate(0, 0, -30), + periodEnd: now, + totalOrders: 100, + totalUsageRecords: 1000, + currencyCode: "USDT", + amountUnit: "USD", + } +} + +// WithSupplierID 设置供应商ID +func (f *SettlementFactory) WithSupplierID(id int64) *SettlementFactory { + f.supplierID = id + return f +} + +// WithStatus 设置状态 +func (f *SettlementFactory) WithStatus(status domain.SettlementStatus) *SettlementFactory { + f.status = status + return f +} + +// WithAmount 设置金额 +func (f *SettlementFactory) WithAmount(total, fee, net float64) *SettlementFactory { + f.totalAmount = total + f.feeAmount = fee + f.netAmount = net + return f +} + +// WithPaymentMethod 设置支付方式 +func (f *SettlementFactory) WithPaymentMethod(method domain.PaymentMethod, account string) *SettlementFactory { + f.paymentMethod = method + f.paymentAccount = account + return f +} + +// WithPeriod 设置账期 +func (f *SettlementFactory) WithPeriod(start, end time.Time) *SettlementFactory { + f.periodStart = start + f.periodEnd = end + return f +} + +// WithIdempotencyKey 设置幂等键 +func (f *SettlementFactory) WithIdempotencyKey(key string) *SettlementFactory { + f.idempotencyKey = key + return f +} + +// Build 构建结算单对象 +func (f *SettlementFactory) Build() *domain.Settlement { + now := time.Now() + return &domain.Settlement{ + ID: 1, + SupplierID: f.supplierID, + SettlementNo: f.settlementNo, + Status: f.status, + TotalAmount: f.totalAmount, + FeeAmount: f.feeAmount, + NetAmount: f.netAmount, + PaymentMethod: f.paymentMethod, + PaymentAccount: f.paymentAccount, + PaymentTransactionID: f.paymentTransactionID, + PeriodStart: f.periodStart, + PeriodEnd: f.periodEnd, + TotalOrders: f.totalOrders, + TotalUsageRecords: f.totalUsageRecords, + CurrencyCode: f.currencyCode, + AmountUnit: f.amountUnit, + RequestID: f.requestID, + IdempotencyKey: f.idempotencyKey, + Version: 1, + CreatedAt: now, + UpdatedAt: now, + } +} + +// BuildPending 构建待处理状态的结算单 +func (f *SettlementFactory) BuildPending() *domain.Settlement { + s := f.Build() + s.Status = domain.SettlementStatusPending + return s +} + +// BuildProcessing 构建处理中的结算单 +func (f *SettlementFactory) BuildProcessing() *domain.Settlement { + s := f.Build() + s.Status = domain.SettlementStatusProcessing + return s +} + +// BuildCompleted 构建已完成状态的结算单 +func (f *SettlementFactory) BuildCompleted() *domain.Settlement { + s := f.Build() + s.Status = domain.SettlementStatusCompleted + now := time.Now() + s.PaidAt = &now + return s +} + +// BuildFailed 构建失败状态的结算单 +func (f *SettlementFactory) BuildFailed() *domain.Settlement { + s := f.Build() + s.Status = domain.SettlementStatusFailed + return s +} + +// BuildWithID 构建带指定ID的结算单 +func (f *SettlementFactory) BuildWithID(id int64) *domain.Settlement { + s := f.Build() + s.ID = id + return s +} + +// WithdrawRequest 构建提现请求 +func (f *SettlementFactory) WithdrawRequest() *domain.WithdrawRequest { + return &domain.WithdrawRequest{ + Amount: f.totalAmount, + PaymentMethod: f.paymentMethod, + PaymentAccount: f.paymentAccount, + SMSCode: "123456", + } +} + +// WithdrawRequestWithCode 构建带指定短信验证码的提现请求 +func (f *SettlementFactory) WithdrawRequestWithCode(smsCode string) *domain.WithdrawRequest { + return &domain.WithdrawRequest{ + Amount: f.totalAmount, + PaymentMethod: f.paymentMethod, + PaymentAccount: f.paymentAccount, + SMSCode: smsCode, + } +}