package domain import ( "context" "errors" "time" "lijiaoqiao/supply-api/internal/audit" ) // 套餐状态 type PackageStatus string const ( PackageStatusDraft PackageStatus = "draft" PackageStatusActive PackageStatus = "active" PackageStatusPaused PackageStatus = "paused" PackageStatusSoldOut PackageStatus = "sold_out" PackageStatusExpired PackageStatus = "expired" ) // 套餐 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"` PricePer1MInput float64 `json:"price_per_1m_input"` PricePer1MOutput float64 `json:"price_per_1m_output"` 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"` } // 套餐服务接口 type PackageService interface { CreateDraft(ctx context.Context, supplierID int64, req *CreatePackageDraftRequest) (*Package, error) Publish(ctx context.Context, supplierID, packageID int64) (*Package, error) Pause(ctx context.Context, supplierID, packageID int64) (*Package, error) Unlist(ctx context.Context, supplierID, packageID int64) (*Package, error) Clone(ctx context.Context, supplierID, packageID int64) (*Package, error) BatchUpdatePrice(ctx context.Context, supplierID int64, req *BatchUpdatePriceRequest) (*BatchUpdatePriceResponse, error) GetByID(ctx context.Context, supplierID, packageID int64) (*Package, error) } // 创建套餐草稿请求 type CreatePackageDraftRequest struct { SupplierID int64 AccountID int64 Model string TotalQuota float64 PricePer1MInput float64 PricePer1MOutput float64 ValidDays int MaxConcurrent int RateLimitRPM int } // 批量调价请求 type BatchUpdatePriceRequest struct { Items []BatchPriceItem `json:"items"` } type BatchPriceItem struct { PackageID int64 `json:"package_id"` PricePer1MInput float64 `json:"price_per_1m_input"` PricePer1MOutput float64 `json:"price_per_1m_output"` } // 批量调价响应 type BatchUpdatePriceResponse struct { Total int `json:"total"` SuccessCount int `json:"success_count"` FailedCount int `json:"failed_count"` Failures []BatchPriceFailure `json:"failures,omitempty"` } type BatchPriceFailure struct { PackageID int64 `json:"package_id"` ErrorCode string `json:"error_code"` Message string `json:"message"` } // 套餐仓储接口 type PackageStore interface { Create(ctx context.Context, pkg *Package) error GetByID(ctx context.Context, supplierID, id int64) (*Package, error) Update(ctx context.Context, pkg *Package) error List(ctx context.Context, supplierID int64) ([]*Package, error) } // 套餐服务实现 type packageService struct { store PackageStore accountStore AccountStore auditStore audit.AuditStore } func NewPackageService(store PackageStore, accountStore AccountStore, auditStore audit.AuditStore) PackageService { return &packageService{ store: store, accountStore: accountStore, auditStore: auditStore, } } func (s *packageService) CreateDraft(ctx context.Context, supplierID int64, req *CreatePackageDraftRequest) (*Package, error) { pkg := &Package{ SupplierID: supplierID, AccountID: req.AccountID, Model: req.Model, TotalQuota: req.TotalQuota, AvailableQuota: req.TotalQuota, PricePer1MInput: req.PricePer1MInput, PricePer1MOutput: req.PricePer1MOutput, ValidDays: req.ValidDays, MaxConcurrent: req.MaxConcurrent, RateLimitRPM: req.RateLimitRPM, Status: PackageStatusDraft, Version: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.store.Create(ctx, pkg); err != nil { return nil, err } s.auditStore.Emit(ctx, audit.Event{ TenantID: supplierID, ObjectType: "supply_package", ObjectID: pkg.ID, Action: "create_draft", ResultCode: "OK", }) return pkg, nil } func (s *packageService) Publish(ctx context.Context, supplierID, packageID int64) (*Package, error) { pkg, err := s.store.GetByID(ctx, supplierID, packageID) if err != nil { return nil, err } if pkg.Status != PackageStatusDraft && pkg.Status != PackageStatusPaused { return nil, errors.New("SUP_PKG_4092: can only publish draft or paused packages") } pkg.Status = PackageStatusActive pkg.UpdatedAt = time.Now() pkg.Version++ if err := s.store.Update(ctx, pkg); err != nil { return nil, err } s.auditStore.Emit(ctx, audit.Event{ TenantID: supplierID, ObjectType: "supply_package", ObjectID: packageID, Action: "publish", ResultCode: "OK", }) return pkg, nil } func (s *packageService) Pause(ctx context.Context, supplierID, packageID int64) (*Package, error) { pkg, err := s.store.GetByID(ctx, supplierID, packageID) if err != nil { return nil, err } if pkg.Status != PackageStatusActive { return nil, errors.New("SUP_PKG_4092: can only pause active packages") } pkg.Status = PackageStatusPaused pkg.UpdatedAt = time.Now() pkg.Version++ if err := s.store.Update(ctx, pkg); err != nil { return nil, err } s.auditStore.Emit(ctx, audit.Event{ TenantID: supplierID, ObjectType: "supply_package", ObjectID: packageID, Action: "pause", ResultCode: "OK", }) return pkg, nil } func (s *packageService) Unlist(ctx context.Context, supplierID, packageID int64) (*Package, error) { pkg, err := s.store.GetByID(ctx, supplierID, packageID) if err != nil { return nil, err } pkg.Status = PackageStatusExpired pkg.UpdatedAt = time.Now() pkg.Version++ if err := s.store.Update(ctx, pkg); err != nil { return nil, err } s.auditStore.Emit(ctx, audit.Event{ TenantID: supplierID, ObjectType: "supply_package", ObjectID: packageID, Action: "unlist", ResultCode: "OK", }) return pkg, nil } func (s *packageService) Clone(ctx context.Context, supplierID, packageID int64) (*Package, error) { original, err := s.store.GetByID(ctx, supplierID, packageID) if err != nil { return nil, err } clone := &Package{ SupplierID: supplierID, AccountID: original.AccountID, Model: original.Model, TotalQuota: original.TotalQuota, AvailableQuota: original.TotalQuota, PricePer1MInput: original.PricePer1MInput, PricePer1MOutput: original.PricePer1MOutput, ValidDays: original.ValidDays, MaxConcurrent: original.MaxConcurrent, RateLimitRPM: original.RateLimitRPM, Status: PackageStatusDraft, Version: 1, CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.store.Create(ctx, clone); err != nil { return nil, err } s.auditStore.Emit(ctx, audit.Event{ TenantID: supplierID, ObjectType: "supply_package", ObjectID: clone.ID, Action: "clone", ResultCode: "OK", }) return clone, nil } func (s *packageService) BatchUpdatePrice(ctx context.Context, supplierID int64, req *BatchUpdatePriceRequest) (*BatchUpdatePriceResponse, error) { resp := &BatchUpdatePriceResponse{ Total: len(req.Items), } for _, item := range req.Items { pkg, err := s.store.GetByID(ctx, supplierID, item.PackageID) if err != nil { resp.FailedCount++ resp.Failures = append(resp.Failures, BatchPriceFailure{ PackageID: item.PackageID, ErrorCode: "NOT_FOUND", Message: err.Error(), }) continue } if pkg.Status == PackageStatusSoldOut || pkg.Status == PackageStatusExpired { resp.FailedCount++ resp.Failures = append(resp.Failures, BatchPriceFailure{ PackageID: item.PackageID, ErrorCode: "SUP_PKG_4093", Message: "cannot update price for sold_out or expired packages", }) continue } pkg.PricePer1MInput = item.PricePer1MInput pkg.PricePer1MOutput = item.PricePer1MOutput pkg.UpdatedAt = time.Now() pkg.Version++ if err := s.store.Update(ctx, pkg); err != nil { resp.FailedCount++ resp.Failures = append(resp.Failures, BatchPriceFailure{ PackageID: item.PackageID, ErrorCode: "UPDATE_FAILED", Message: err.Error(), }) continue } resp.SuccessCount++ } return resp, nil } func (s *packageService) GetByID(ctx context.Context, supplierID, packageID int64) (*Package, error) { return s.store.GetByID(ctx, supplierID, packageID) }