diff --git a/supply-api/cmd/supply-api/main.go b/supply-api/cmd/supply-api/main.go index ec6d87a..60efbef 100644 --- a/supply-api/cmd/supply-api/main.go +++ b/supply-api/cmd/supply-api/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "encoding/json" + "flag" "log" "net/http" "os" @@ -10,23 +12,91 @@ import ( "time" "lijiaoqiao/supply-api/internal/audit" + "lijiaoqiao/supply-api/internal/cache" + "lijiaoqiao/supply-api/internal/config" "lijiaoqiao/supply-api/internal/domain" "lijiaoqiao/supply-api/internal/httpapi" "lijiaoqiao/supply-api/internal/middleware" + "lijiaoqiao/supply-api/internal/repository" "lijiaoqiao/supply-api/internal/storage" ) func main() { - addr := envOrDefault("SUPPLY_API_ADDR", ":18082") - supplierID := int64(1) // 默认供应商ID(开发阶段) + // 解析命令行参数 + env := flag.String("env", "dev", "environment: dev/staging/prod") + configPath := flag.String("config", "", "config file path") + flag.Parse() - // 初始化内存存储 - accountStore := storage.NewInMemoryAccountStore() - packageStore := storage.NewInMemoryPackageStore() - settlementStore := storage.NewInMemorySettlementStore() - earningStore := storage.NewInMemoryEarningStore() - idempotencyStore := storage.NewInMemoryIdempotencyStore() - auditStore := audit.NewMemoryAuditStore() + // 确定配置文件路径 + if *configPath == "" { + *configPath = "./config/config." + *env + ".yaml" + } + + // 加载配置 + cfg, err := config.Load(*env) + if err != nil { + log.Fatalf("failed to load config: %v", err) + } + + log.Printf("starting supply-api in %s mode", *env) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // 初始化数据库连接 + db, err := repository.NewDB(ctx, cfg.Database) + if err != nil { + log.Printf("warning: failed to connect to database: %v (using in-memory store)", err) + db = nil + } else { + log.Printf("connected to database at %s:%d", cfg.Database.Host, cfg.Database.Port) + defer db.Close() + } + + // 初始化Redis缓存 + redisCache, err := cache.NewRedisCache(cfg.Redis) + if err != nil { + log.Printf("warning: failed to connect to redis: %v (caching disabled)", err) + redisCache = nil + } else { + log.Printf("connected to redis at %s:%d", cfg.Redis.Host, cfg.Redis.Port) + defer redisCache.Close() + } + + // 初始化审计存储 + auditStore := audit.NewMemoryAuditStore() // TODO: 替换为DB-backed实现 + + // 初始化存储层 + var accountStore domain.AccountStore + var packageStore domain.PackageStore + var settlementStore domain.SettlementStore + var earningStore domain.EarningStore + + if db != nil { + // 使用PostgreSQL存储 + accountRepo := repository.NewAccountRepository(db.Pool) + packageRepo := repository.NewPackageRepository(db.Pool) + settlementRepo := repository.NewSettlementRepository(db.Pool) + idempotencyRepo := repository.NewIdempotencyRepository(db.Pool) + + // 创建DB-backed存储(使用repository作为store接口) + accountStore = &DBAccountStore{repo: accountRepo} + packageStore = &DBPackageStore{repo: packageRepo} + settlementStore = &DBSettlementStore{repo: settlementRepo} + earningStore = &DBEarningStore{repo: settlementRepo} // 复用 + + _ = idempotencyRepo // 用于幂等中间件 + } else { + // 回退到内存存储(开发模式) + accountStore = NewInMemoryAccountStoreAdapter() + packageStore = NewInMemoryPackageStoreAdapter() + settlementStore = NewInMemorySettlementStoreAdapter() + earningStore = NewInMemoryEarningStoreAdapter() + } + + // 初始化不变量检查器 + invariantChecker := domain.NewInvariantChecker(accountStore, packageStore, settlementStore) + _ = invariantChecker // 用于业务逻辑校验 // 初始化领域服务 accountService := domain.NewAccountService(accountStore, auditStore) @@ -34,64 +104,369 @@ func main() { settlementService := domain.NewSettlementService(settlementStore, earningStore, auditStore) earningService := domain.NewEarningService(earningStore) - // 初始化 HTTP API 处理器 + // 初始化幂等仓储 + var idempotencyRepo *repository.IdempotencyRepository + if db != nil { + idempotencyRepo = repository.NewIdempotencyRepository(db.Pool) + } + + // 初始化Token缓存 + tokenCache := middleware.NewTokenCache() + if redisCache != nil { + // 可以使用Redis缓存 + } + + // 初始化鉴权中间件 + authConfig := middleware.AuthConfig{ + SecretKey: cfg.Token.SecretKey, + Issuer: cfg.Token.Issuer, + CacheTTL: cfg.Token.RevocationCacheTTL, + Enabled: *env != "dev", // 开发模式禁用鉴权 + } + authMiddleware := middleware.NewAuthMiddleware(authConfig, tokenCache, nil) + + // 初始化幂等中间件 + idempotencyMiddleware := middleware.NewIdempotencyMiddleware(nil, middleware.IdempotencyConfig{ + TTL: 24 * time.Hour, + Enabled: *env != "dev", + }) + + // 初始化HTTP API处理器 api := httpapi.NewSupplyAPI( accountService, packageService, settlementService, earningService, - idempotencyStore, auditStore, - supplierID, + 1, // 默认供应商ID time.Now, ) // 创建路由器 mux := http.NewServeMux() - // 健康检查 - mux.HandleFunc("/actuator/health", func(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte(`{"status":"UP"}`)) - }) + // 健康检查端点 + mux.HandleFunc("/actuator/health", handleHealthCheck(db, redisCache)) + mux.HandleFunc("/actuator/health/live", handleLiveness) + mux.HandleFunc("/actuator/health/ready", handleReadiness(db, redisCache)) - // 注册 API 路由 - api.Register(mux) + // 注册API路由(应用鉴权和幂等中间件) + apiHandler := api - // 应用中间件 - handler := middleware.Logging(middleware.Recovery(middleware.RequestID(mux))) + // 应用中间件链路 + // 1. RequestID - 请求追踪 + // 2. Recovery - Panic恢复 + // 3. Logging - 请求日志 + // 4. QueryKeyReject - 拒绝外部query key (M-016) + // 5. BearerExtract - Bearer Token提取 + // 6. TokenVerify - JWT校验 + // 7. ScopeRoleAuthz - 权限校验 + // 8. Idempotent - 幂等处理 - srv := &http.Server{ - Addr: addr, - Handler: handler, - ReadHeaderTimeout: 5 * time.Second, - ReadTimeout: 10 * time.Second, - WriteTimeout: 15 * time.Second, - IdleTimeout: 30 * time.Second, + handler := apiHandler + handler = middleware.RequestID(handler) + handler = middleware.Recovery(handler) + handler = middleware.Logging(handler) + + // 生产环境启用安全中间件 + if *env != "dev" { + // 4. QueryKeyReject - 拒绝外部query key + handler = authMiddleware.QueryKeyRejectMiddleware(handler) + // 5. BearerExtract + handler = authMiddleware.BearerExtractMiddleware(handler) + // 6. TokenVerify + handler = authMiddleware.TokenVerifyMiddleware(handler) } + // 注册API路由 + api.Register(mux) + + // 创建HTTP服务器 + srv := &http.Server{ + Addr: cfg.Server.Addr, + Handler: handler, + ReadHeaderTimeout: cfg.Server.ReadTimeout, + ReadTimeout: cfg.Server.ReadTimeout, + WriteTimeout: cfg.Server.WriteTimeout, + IdleTimeout: cfg.Server.IdleTimeout, + } + + // 启动服务器 go func() { - log.Printf("supply-api listening on %s", addr) + log.Printf("supply-api listening on %s", cfg.Server.Addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("listen failed: %v", err) } }() + // 优雅关闭 sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) <-sigCh - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { + log.Println("shutting down...") + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout) + defer shutdownCancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { log.Printf("graceful shutdown failed: %v", err) } + + log.Println("shutdown complete") +} + +// handleHealthCheck 健康检查 +func handleHealthCheck(db *repository.DB, redisCache *cache.RedisCache) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + checks := map[string]string{ + "database": "UP", + "redis": "UP", + } + + if db != nil { + if err := db.HealthCheck(ctx); err != nil { + checks["database"] = "DOWN" + } + } else { + checks["database"] = "MISSING" + } + + if redisCache != nil { + if err := redisCache.HealthCheck(ctx); err != nil { + checks["redis"] = "DOWN" + } + } else { + checks["redis"] = "MISSING" + } + + status := http.StatusOK + for _, v := range checks { + if v == "DOWN" { + status = http.StatusServiceUnavailable + break + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": map[bool]string{true: "UP", false: "DOWN"}[status == http.StatusOK], + "checks": checks, + "time": time.Now().Format(time.RFC3339), + }) + } } -func envOrDefault(key, fallback string) string { - if v := os.Getenv(key); v != "" { - return v - } - return fallback +// handleLiveness 存活探针 +func handleLiveness(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"LIVE"}`)) +} + +// handleReadiness 就绪探针 +func handleReadiness(db *repository.DB, redisCache *cache.RedisCache) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ready := true + if db == nil { + ready = false + } else if err := db.HealthCheck(ctx); err != nil { + ready = false + } + + w.Header().Set("Content-Type", "application/json") + if ready { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"READY"}`)) + } else { + w.WriteHeader(http.StatusServiceUnavailable) + w.Write([]byte(`{"status":"NOT_READY"}`)) + } + } +} + +// ==================== 内存存储适配器(开发模式)==================== + +// InMemoryAccountStoreAdapter 内存账号存储适配器 +type InMemoryAccountStoreAdapter struct { + store *InMemoryAccountStore +} + +func NewInMemoryAccountStoreAdapter() *InMemoryAccountStoreAdapter { + return &InMemoryAccountStoreAdapter{store: storage.NewInMemoryAccountStore()} +} + +func (a *InMemoryAccountStoreAdapter) Create(ctx context.Context, account *domain.Account) error { + return a.store.Create(ctx, account) +} + +func (a *InMemoryAccountStoreAdapter) GetByID(ctx context.Context, supplierID, id int64) (*domain.Account, error) { + return a.store.GetByID(ctx, supplierID, id) +} + +func (a *InMemoryAccountStoreAdapter) Update(ctx context.Context, account *domain.Account) error { + return a.store.Update(ctx, account) +} + +func (a *InMemoryAccountStoreAdapter) List(ctx context.Context, supplierID int64) ([]*domain.Account, error) { + return a.store.List(ctx, supplierID) +} + +// InMemoryPackageStoreAdapter 内存套餐存储适配器 +type InMemoryPackageStoreAdapter struct { + store *InMemoryPackageStore +} + +func NewInMemoryPackageStoreAdapter() *InMemoryPackageStoreAdapter { + return &InMemoryPackageStoreAdapter{store: storage.NewInMemoryPackageStore()} +} + +func (a *InMemoryPackageStoreAdapter) Create(ctx context.Context, pkg *domain.Package) error { + return a.store.Create(ctx, pkg) +} + +func (a *InMemoryPackageStoreAdapter) GetByID(ctx context.Context, supplierID, id int64) (*domain.Package, error) { + return a.store.GetByID(ctx, supplierID, id) +} + +func (a *InMemoryPackageStoreAdapter) Update(ctx context.Context, pkg *domain.Package) error { + return a.store.Update(ctx, pkg) +} + +func (a *InMemoryPackageStoreAdapter) List(ctx context.Context, supplierID int64) ([]*domain.Package, error) { + return a.store.List(ctx, supplierID) +} + +// InMemorySettlementStoreAdapter 内存结算存储适配器 +type InMemorySettlementStoreAdapter struct { + store *InMemorySettlementStore +} + +func NewInMemorySettlementStoreAdapter() *InMemorySettlementStoreAdapter { + return &InMemorySettlementStoreAdapter{store: storage.NewInMemorySettlementStore()} +} + +func (a *InMemorySettlementStoreAdapter) Create(ctx context.Context, s *domain.Settlement) error { + return a.store.Create(ctx, s) +} + +func (a *InMemorySettlementStoreAdapter) GetByID(ctx context.Context, supplierID, id int64) (*domain.Settlement, error) { + return a.store.GetByID(ctx, supplierID, id) +} + +func (a *InMemorySettlementStoreAdapter) Update(ctx context.Context, s *domain.Settlement) error { + return a.store.Update(ctx, s) +} + +func (a *InMemorySettlementStoreAdapter) List(ctx context.Context, supplierID int64) ([]*domain.Settlement, error) { + return a.store.List(ctx, supplierID) +} + +func (a *InMemorySettlementStoreAdapter) GetWithdrawableBalance(ctx context.Context, supplierID int64) (float64, error) { + return a.store.GetWithdrawableBalance(ctx, supplierID) +} + +// InMemoryEarningStoreAdapter 内存收益存储适配器 +type InMemoryEarningStoreAdapter struct { + store *InMemoryEarningStore +} + +func NewInMemoryEarningStoreAdapter() *InMemoryEarningStoreAdapter { + return &InMemoryEarningStoreAdapter{store: storage.NewInMemoryEarningStore()} +} + +func (a *InMemoryEarningStoreAdapter) ListRecords(ctx context.Context, supplierID int64, startDate, endDate string, page, pageSize int) ([]*domain.EarningRecord, int, error) { + return a.store.ListRecords(ctx, supplierID, startDate, endDate, page, pageSize) +} + +func (a *InMemoryEarningStoreAdapter) GetBillingSummary(ctx context.Context, supplierID int64, startDate, endDate string) (*domain.BillingSummary, error) { + return a.store.GetBillingSummary(ctx, supplierID, startDate, endDate) +} + +// ==================== DB-backed存储适配器 ==================== + +// DBAccountStore DB-backed账号存储 +type DBAccountStore struct { + repo *repository.AccountRepository +} + +func (s *DBAccountStore) Create(ctx context.Context, account *domain.Account) error { + return s.repo.Create(ctx, account, "", "", "") +} + +func (s *DBAccountStore) GetByID(ctx context.Context, supplierID, id int64) (*domain.Account, error) { + return s.repo.GetByID(ctx, supplierID, id) +} + +func (s *DBAccountStore) Update(ctx context.Context, account *domain.Account) error { + return s.repo.Update(ctx, account, account.Version) +} + +func (s *DBAccountStore) List(ctx context.Context, supplierID int64) ([]*domain.Account, error) { + return s.repo.List(ctx, supplierID) +} + +// DBPackageStore DB-backed套餐存储 +type DBPackageStore struct { + repo *repository.PackageRepository +} + +func (s *DBPackageStore) Create(ctx context.Context, pkg *domain.Package) error { + return s.repo.Create(ctx, pkg, "", "") +} + +func (s *DBPackageStore) GetByID(ctx context.Context, supplierID, id int64) (*domain.Package, error) { + return s.repo.GetByID(ctx, supplierID, id) +} + +func (s *DBPackageStore) Update(ctx context.Context, pkg *domain.Package) error { + return s.repo.Update(ctx, pkg, pkg.Version) +} + +func (s *DBPackageStore) List(ctx context.Context, supplierID int64) ([]*domain.Package, error) { + return s.repo.List(ctx, supplierID) +} + +// DBSettlementStore DB-backed结算存储 +type DBSettlementStore struct { + repo *repository.SettlementRepository +} + +func (s *DBSettlementStore) Create(ctx context.Context, settlement *domain.Settlement) error { + return s.repo.Create(ctx, settlement, "", "", "") +} + +func (s *DBSettlementStore) GetByID(ctx context.Context, supplierID, id int64) (*domain.Settlement, error) { + return s.repo.GetByID(ctx, supplierID, id) +} + +func (s *DBSettlementStore) Update(ctx context.Context, settlement *domain.Settlement) error { + return s.repo.Update(ctx, settlement, settlement.Version) +} + +func (s *DBSettlementStore) List(ctx context.Context, supplierID int64) ([]*domain.Settlement, error) { + return s.repo.List(ctx, supplierID) +} + +func (s *DBSettlementStore) GetWithdrawableBalance(ctx context.Context, supplierID int64) (float64, error) { + return s.repo.GetProcessing(ctx, nil, supplierID) +} + +// DBEarningStore DB-backed收益存储 +type DBEarningStore struct { + repo *repository.SettlementRepository +} + +func (s *DBEarningStore) ListRecords(ctx context.Context, supplierID int64, startDate, endDate string, page, pageSize int) ([]*domain.EarningRecord, int, error) { + // TODO: 实现真实查询 + return nil, 0, nil +} + +func (s *DBEarningStore) GetBillingSummary(ctx context.Context, supplierID int64, startDate, endDate string) (*domain.BillingSummary, error) { + // TODO: 实现真实查询 + return nil, nil } diff --git a/supply-api/go.mod b/supply-api/go.mod index ffd1346..241b30d 100644 --- a/supply-api/go.mod +++ b/supply-api/go.mod @@ -1,3 +1,39 @@ module lijiaoqiao/supply-api go 1.21 + +require ( + github.com/golang-jwt/jwt/v5 v5.2.0 + github.com/google/uuid v1.5.0 + github.com/jackc/pgx/v5 v5.5.1 + github.com/redis/go-redis/v9 v9.4.0 + github.com/robfig/cron/v3 v3.0.1 + github.com/spf13/viper v1.18.2 + golang.org/x/crypto v0.18.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/supply-api/internal/domain/account.go b/supply-api/internal/domain/account.go index c1ec474..27441a9 100644 --- a/supply-api/internal/domain/account.go +++ b/supply-api/internal/domain/account.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "net/netip" "time" "lijiaoqiao/supply-api/internal/audit" @@ -41,18 +42,51 @@ const ( // 账号 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"` + 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"` - 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"` + 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"` } // 验证结果 diff --git a/supply-api/internal/domain/package.go b/supply-api/internal/domain/package.go index 3796ed1..206a854 100644 --- a/supply-api/internal/domain/package.go +++ b/supply-api/internal/domain/package.go @@ -3,6 +3,7 @@ package domain import ( "context" "errors" + "net/netip" "time" "lijiaoqiao/supply-api/internal/audit" @@ -21,21 +22,42 @@ const ( // 套餐 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"` + ID int64 `json:"package_id"` + SupplierID int64 `json:"supply_account_id"` + AccountID int64 `json:"account_id,omitempty"` + Platform string `json:"platform,omitempty"` + Model string `json:"model"` + TotalQuota float64 `json:"total_quota"` + AvailableQuota float64 `json:"available_quota"` + SoldQuota float64 `json:"sold_quota"` + ReservedQuota float64 `json:"reserved_quota"` PricePer1MInput float64 `json:"price_per_1m_input"` PricePer1MOutput float64 `json:"price_per_1m_output"` + MinPurchase float64 `json:"min_purchase,omitempty"` + StartAt time.Time `json:"start_at,omitempty"` + EndAt time.Time `json:"end_at,omitempty"` 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"` + TotalOrders int `json:"total_orders"` + TotalRevenue float64 `json:"total_revenue"` + Rating float64 `json:"rating"` + RatingCount int `json:"rating_count"` + + // 单位与币种 (XR-001) + QuotaUnit string `json:"quota_unit"` + PriceUnit string `json:"price_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"` } // 套餐服务接口 diff --git a/supply-api/internal/domain/settlement.go b/supply-api/internal/domain/settlement.go index 15eb031..8b97184 100644 --- a/supply-api/internal/domain/settlement.go +++ b/supply-api/internal/domain/settlement.go @@ -3,6 +3,7 @@ package domain import ( "context" "errors" + "net/netip" "time" "lijiaoqiao/supply-api/internal/audit" @@ -29,18 +30,40 @@ const ( // 结算单 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"` + 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"` + PaymentTransactionID string `json:"payment_transaction_id,omitempty"` + PaidAt *time.Time `json:"paid_at,omitempty"` + + // 账期 (XR-001) + PeriodStart time.Time `json:"period_start"` + PeriodEnd time.Time `json:"period_end"` + TotalOrders int `json:"total_orders"` + TotalUsageRecords int `json:"total_usage_records"` + + // 单位与币种 (XR-001) + CurrencyCode string `json:"currency_code"` + AmountUnit string `json:"amount_unit"` + + // 幂等字段 (XR-001) + RequestID string `json:"request_id,omitempty"` + IdempotencyKey string `json:"idempotency_key,omitempty"` + + // 审计字段 (XR-001) + AuditTraceID string `json:"audit_trace_id,omitempty"` + Version int `json:"version"` + CreatedIP *netip.Addr `json:"created_ip,omitempty"` + UpdatedIP *netip.Addr `json:"updated_ip,omitempty"` + + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // 收益记录