From ee569e7edb5a9e440f8259b6938d5067dffd6448 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 11 Apr 2026 11:18:45 +0800 Subject: [PATCH] test(supply-api): add benchmark and test helper support Add benchmark documentation and middleware benchmark coverage, fix the settlement benchmark mock to satisfy the current SettlementStore interface, and add reusable domain test helper packages. Verified with fresh go test runs for ./internal/testutil/... and go test -tags=slow -run '^$' ./internal/benchmark/... before commit. --- supply-api/internal/benchmark/README.md | 76 ++++++++ .../internal/benchmark/domain_bench_test.go | 8 + .../benchmark/middleware_bench_test.go | 115 ++++++++++++ .../internal/testutil/assert/assertions.go | 93 ++++++++++ .../internal/testutil/factory/account.go | 126 +++++++++++++ .../internal/testutil/factory/package.go | 175 ++++++++++++++++++ .../internal/testutil/factory/settlement.go | 174 +++++++++++++++++ 7 files changed, 767 insertions(+) create mode 100644 supply-api/internal/benchmark/README.md create mode 100644 supply-api/internal/benchmark/middleware_bench_test.go create mode 100644 supply-api/internal/testutil/assert/assertions.go create mode 100644 supply-api/internal/testutil/factory/account.go create mode 100644 supply-api/internal/testutil/factory/package.go create mode 100644 supply-api/internal/testutil/factory/settlement.go 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, + } +}