2026-03-31 13:40:00 +08:00
|
|
|
package domain
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
2026-04-01 08:53:47 +08:00
|
|
|
"net/netip"
|
2026-03-31 13:40:00 +08:00
|
|
|
"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 {
|
2026-04-01 08:53:47 +08:00
|
|
|
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"`
|
2026-03-31 13:40:00 +08:00
|
|
|
PricePer1MInput float64 `json:"price_per_1m_input"`
|
|
|
|
|
PricePer1MOutput float64 `json:"price_per_1m_output"`
|
2026-04-01 08:53:47 +08:00
|
|
|
MinPurchase float64 `json:"min_purchase,omitempty"`
|
|
|
|
|
StartAt time.Time `json:"start_at,omitempty"`
|
|
|
|
|
EndAt time.Time `json:"end_at,omitempty"`
|
2026-03-31 13:40:00 +08:00
|
|
|
ValidDays int `json:"valid_days"`
|
|
|
|
|
MaxConcurrent int `json:"max_concurrent,omitempty"`
|
|
|
|
|
RateLimitRPM int `json:"rate_limit_rpm,omitempty"`
|
|
|
|
|
Status PackageStatus `json:"status"`
|
2026-04-01 08:53:47 +08:00
|
|
|
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"`
|
2026-03-31 13:40:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 套餐服务接口
|
|
|
|
|
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)
|
|
|
|
|
}
|