feat(supply-api): 完善domain层和main入口

修改内容:
- cmd/supply-api/main.go: 完善HTTP API入口和路由配置
- go.mod: 更新依赖版本
- domain/account.go: 完善账户领域模型
- domain/package.go: 完善套餐领域模型
- domain/settlement.go: 完善结算领域模型

这些是supply-api的核心domain层实现
This commit is contained in:
Your Name
2026-04-01 08:53:47 +08:00
parent 0196ee5d47
commit ecb5fad1c9
5 changed files with 560 additions and 70 deletions

View File

@@ -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
}

View File

@@ -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
)

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"time"
"lijiaoqiao/supply-api/internal/audit"
@@ -46,11 +47,44 @@ type Account struct {
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"`
RiskLevel string `json:"risk_level"`
TotalQuota float64 `json:"total_quota,omitempty"`
AvailableQuota float64 `json:"available_quota,omitempty"`
RiskScore int `json:"risk_score,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"`
}

View File

@@ -3,6 +3,7 @@ package domain
import (
"context"
"errors"
"net/netip"
"time"
"lijiaoqiao/supply-api/internal/audit"
@@ -24,16 +25,37 @@ type Package struct {
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"`
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"`
}

View File

@@ -3,6 +3,7 @@ package domain
import (
"context"
"errors"
"net/netip"
"time"
"lijiaoqiao/supply-api/internal/audit"
@@ -38,7 +39,29 @@ type Settlement struct {
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"`
}