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:
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -10,23 +12,91 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"lijiaoqiao/supply-api/internal/audit"
|
"lijiaoqiao/supply-api/internal/audit"
|
||||||
|
"lijiaoqiao/supply-api/internal/cache"
|
||||||
|
"lijiaoqiao/supply-api/internal/config"
|
||||||
"lijiaoqiao/supply-api/internal/domain"
|
"lijiaoqiao/supply-api/internal/domain"
|
||||||
"lijiaoqiao/supply-api/internal/httpapi"
|
"lijiaoqiao/supply-api/internal/httpapi"
|
||||||
"lijiaoqiao/supply-api/internal/middleware"
|
"lijiaoqiao/supply-api/internal/middleware"
|
||||||
|
"lijiaoqiao/supply-api/internal/repository"
|
||||||
"lijiaoqiao/supply-api/internal/storage"
|
"lijiaoqiao/supply-api/internal/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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()
|
if *configPath == "" {
|
||||||
packageStore := storage.NewInMemoryPackageStore()
|
*configPath = "./config/config." + *env + ".yaml"
|
||||||
settlementStore := storage.NewInMemorySettlementStore()
|
}
|
||||||
earningStore := storage.NewInMemoryEarningStore()
|
|
||||||
idempotencyStore := storage.NewInMemoryIdempotencyStore()
|
// 加载配置
|
||||||
auditStore := audit.NewMemoryAuditStore()
|
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)
|
accountService := domain.NewAccountService(accountStore, auditStore)
|
||||||
@@ -34,64 +104,369 @@ func main() {
|
|||||||
settlementService := domain.NewSettlementService(settlementStore, earningStore, auditStore)
|
settlementService := domain.NewSettlementService(settlementStore, earningStore, auditStore)
|
||||||
earningService := domain.NewEarningService(earningStore)
|
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(
|
api := httpapi.NewSupplyAPI(
|
||||||
accountService,
|
accountService,
|
||||||
packageService,
|
packageService,
|
||||||
settlementService,
|
settlementService,
|
||||||
earningService,
|
earningService,
|
||||||
idempotencyStore,
|
|
||||||
auditStore,
|
auditStore,
|
||||||
supplierID,
|
1, // 默认供应商ID
|
||||||
time.Now,
|
time.Now,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 创建路由器
|
// 创建路由器
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// 健康检查
|
// 健康检查端点
|
||||||
mux.HandleFunc("/actuator/health", func(w http.ResponseWriter, _ *http.Request) {
|
mux.HandleFunc("/actuator/health", handleHealthCheck(db, redisCache))
|
||||||
w.Header().Set("Content-Type", "application/json")
|
mux.HandleFunc("/actuator/health/live", handleLiveness)
|
||||||
w.WriteHeader(http.StatusOK)
|
mux.HandleFunc("/actuator/health/ready", handleReadiness(db, redisCache))
|
||||||
_, _ = w.Write([]byte(`{"status":"UP"}`))
|
|
||||||
})
|
|
||||||
|
|
||||||
// 注册 API 路由
|
// 注册API路由(应用鉴权和幂等中间件)
|
||||||
api.Register(mux)
|
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{
|
handler := apiHandler
|
||||||
Addr: addr,
|
handler = middleware.RequestID(handler)
|
||||||
Handler: handler,
|
handler = middleware.Recovery(handler)
|
||||||
ReadHeaderTimeout: 5 * time.Second,
|
handler = middleware.Logging(handler)
|
||||||
ReadTimeout: 10 * time.Second,
|
|
||||||
WriteTimeout: 15 * time.Second,
|
// 生产环境启用安全中间件
|
||||||
IdleTimeout: 30 * time.Second,
|
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() {
|
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 {
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalf("listen failed: %v", err)
|
log.Fatalf("listen failed: %v", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 优雅关闭
|
||||||
sigCh := make(chan os.Signal, 1)
|
sigCh := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigCh
|
<-sigCh
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
log.Println("shutting down...")
|
||||||
defer cancel()
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
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.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 {
|
// handleLiveness 存活探针
|
||||||
if v := os.Getenv(key); v != "" {
|
func handleLiveness(w http.ResponseWriter, r *http.Request) {
|
||||||
return v
|
w.Header().Set("Content-Type", "application/json")
|
||||||
}
|
w.WriteHeader(http.StatusOK)
|
||||||
return fallback
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,39 @@
|
|||||||
module lijiaoqiao/supply-api
|
module lijiaoqiao/supply-api
|
||||||
|
|
||||||
go 1.21
|
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
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"lijiaoqiao/supply-api/internal/audit"
|
"lijiaoqiao/supply-api/internal/audit"
|
||||||
@@ -41,18 +42,51 @@ const (
|
|||||||
|
|
||||||
// 账号
|
// 账号
|
||||||
type Account struct {
|
type Account struct {
|
||||||
ID int64 `json:"account_id"`
|
ID int64 `json:"account_id"`
|
||||||
SupplierID int64 `json:"supplier_id"`
|
SupplierID int64 `json:"supplier_id"`
|
||||||
Provider Provider `json:"provider"`
|
Provider Provider `json:"provider"`
|
||||||
AccountType AccountType `json:"account_type"`
|
AccountType AccountType `json:"account_type"`
|
||||||
CredentialHash string `json:"-"` // 不暴露
|
CredentialHash string `json:"-"` // 不暴露
|
||||||
Alias string `json:"account_alias,omitempty"`
|
KeyID string `json:"key_id,omitempty"` // 不暴露
|
||||||
|
Alias string `json:"account_alias,omitempty"`
|
||||||
Status AccountStatus `json:"status"`
|
Status AccountStatus `json:"status"`
|
||||||
AvailableQuota float64 `json:"available_quota,omitempty"`
|
RiskLevel string `json:"risk_level"`
|
||||||
RiskScore int `json:"risk_score,omitempty"`
|
TotalQuota float64 `json:"total_quota,omitempty"`
|
||||||
Version int `json:"version"`
|
AvailableQuota float64 `json:"available_quota,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
FrozenQuota float64 `json:"frozen_quota,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证结果
|
// 验证结果
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package domain
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"lijiaoqiao/supply-api/internal/audit"
|
"lijiaoqiao/supply-api/internal/audit"
|
||||||
@@ -21,21 +22,42 @@ const (
|
|||||||
|
|
||||||
// 套餐
|
// 套餐
|
||||||
type Package struct {
|
type Package struct {
|
||||||
ID int64 `json:"package_id"`
|
ID int64 `json:"package_id"`
|
||||||
SupplierID int64 `json:"supply_account_id"`
|
SupplierID int64 `json:"supply_account_id"`
|
||||||
AccountID int64 `json:"account_id,omitempty"`
|
AccountID int64 `json:"account_id,omitempty"`
|
||||||
Model string `json:"model"`
|
Platform string `json:"platform,omitempty"`
|
||||||
TotalQuota float64 `json:"total_quota"`
|
Model string `json:"model"`
|
||||||
AvailableQuota float64 `json:"available_quota"`
|
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"`
|
PricePer1MInput float64 `json:"price_per_1m_input"`
|
||||||
PricePer1MOutput float64 `json:"price_per_1m_output"`
|
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"`
|
ValidDays int `json:"valid_days"`
|
||||||
MaxConcurrent int `json:"max_concurrent,omitempty"`
|
MaxConcurrent int `json:"max_concurrent,omitempty"`
|
||||||
RateLimitRPM int `json:"rate_limit_rpm,omitempty"`
|
RateLimitRPM int `json:"rate_limit_rpm,omitempty"`
|
||||||
Status PackageStatus `json:"status"`
|
Status PackageStatus `json:"status"`
|
||||||
Version int `json:"version"`
|
TotalOrders int `json:"total_orders"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
TotalRevenue float64 `json:"total_revenue"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 套餐服务接口
|
// 套餐服务接口
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package domain
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"lijiaoqiao/supply-api/internal/audit"
|
"lijiaoqiao/supply-api/internal/audit"
|
||||||
@@ -29,18 +30,40 @@ const (
|
|||||||
|
|
||||||
// 结算单
|
// 结算单
|
||||||
type Settlement struct {
|
type Settlement struct {
|
||||||
ID int64 `json:"settlement_id"`
|
ID int64 `json:"settlement_id"`
|
||||||
SupplierID int64 `json:"supplier_id"`
|
SupplierID int64 `json:"supplier_id"`
|
||||||
SettlementNo string `json:"settlement_no"`
|
SettlementNo string `json:"settlement_no"`
|
||||||
Status SettlementStatus `json:"status"`
|
Status SettlementStatus `json:"status"`
|
||||||
TotalAmount float64 `json:"total_amount"`
|
TotalAmount float64 `json:"total_amount"`
|
||||||
FeeAmount float64 `json:"fee_amount"`
|
FeeAmount float64 `json:"fee_amount"`
|
||||||
NetAmount float64 `json:"net_amount"`
|
NetAmount float64 `json:"net_amount"`
|
||||||
PaymentMethod PaymentMethod `json:"payment_method"`
|
PaymentMethod PaymentMethod `json:"payment_method"`
|
||||||
PaymentAccount string `json:"payment_account,omitempty"`
|
PaymentAccount string `json:"payment_account,omitempty"`
|
||||||
Version int `json:"version"`
|
PaymentTransactionID string `json:"payment_transaction_id,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
PaidAt *time.Time `json:"paid_at,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
|
// 账期 (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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 收益记录
|
// 收益记录
|
||||||
|
|||||||
Reference in New Issue
Block a user