| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- package service
- import (
- "context"
- "fmt"
- infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- )
- var (
- ErrGroupNotFound = infraerrors.NotFound("GROUP_NOT_FOUND", "group not found")
- ErrGroupExists = infraerrors.Conflict("GROUP_EXISTS", "group name already exists")
- ErrGroupNotEmpty = infraerrors.Conflict("GROUP_NOT_EMPTY", "group contains accounts")
- )
- type GroupRepository interface {
- Create(ctx context.Context, group *Group) error
- GetByID(ctx context.Context, id int64) (*Group, error)
- GetByIDLite(ctx context.Context, id int64) (*Group, error)
- Update(ctx context.Context, group *Group) error
- Delete(ctx context.Context, id int64) error
- DeleteCascade(ctx context.Context, id int64) ([]int64, error)
- List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error)
- ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error)
- ListActive(ctx context.Context) ([]Group, error)
- ListActiveByPlatform(ctx context.Context, platform string) ([]Group, error)
- ExistsByName(ctx context.Context, name string) (bool, error)
- GetAccountCount(ctx context.Context, groupID int64) (total int64, active int64, err error)
- DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error)
- // GetAccountIDsByGroupIDs 获取多个分组的所有账号 ID(去重)
- GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error)
- // BindAccountsToGroup 将多个账号绑定到指定分组
- BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error
- // UpdateSortOrders 批量更新分组排序
- UpdateSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error
- }
- type GroupDuplicateRepository interface {
- // FindByDuplicateOperationID performs the read-only recovery lookup used
- // after an ambiguous idempotency-store failure.
- FindByDuplicateOperationID(ctx context.Context, operationID string) (*Group, error)
- // CreateFromSource atomically persists the group, copies the source group's
- // exact account priorities, and writes the scheduler outbox event.
- CreateFromSource(ctx context.Context, group *Group, sourceGroupID int64) error
- }
- // AdminGroupRepository makes the group-duplication write capability an explicit
- // admin-service dependency without widening gateway-only group test doubles.
- type AdminGroupRepository interface {
- GroupRepository
- GroupDuplicateRepository
- EmptyGroupDeleteRepository
- }
- // EmptyGroupDeleteRepository provides the guarded cascade used by simple mode.
- type EmptyGroupDeleteRepository interface {
- DeleteCascadeIfEmpty(ctx context.Context, id int64) ([]int64, error)
- }
- // GroupSortOrderUpdate 分组排序更新
- type GroupSortOrderUpdate struct {
- ID int64 `json:"id"`
- SortOrder int `json:"sort_order"`
- }
- // CreateGroupRequest 创建分组请求
- type CreateGroupRequest struct {
- Name string `json:"name"`
- Description string `json:"description"`
- RateMultiplier float64 `json:"rate_multiplier"`
- IsExclusive bool `json:"is_exclusive"`
- AllowImageGeneration bool `json:"allow_image_generation"`
- ImageRateIndependent bool `json:"image_rate_independent"`
- ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
- }
- // UpdateGroupRequest 更新分组请求
- type UpdateGroupRequest struct {
- Name *string `json:"name"`
- Description *string `json:"description"`
- RateMultiplier *float64 `json:"rate_multiplier"`
- IsExclusive *bool `json:"is_exclusive"`
- Status *string `json:"status"`
- AllowImageGeneration *bool `json:"allow_image_generation"`
- ImageRateIndependent *bool `json:"image_rate_independent"`
- ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
- }
- // GroupService 分组管理服务
- type GroupService struct {
- groupRepo GroupRepository
- authCacheInvalidator APIKeyAuthCacheInvalidator
- }
- // NewGroupService 创建分组服务实例
- func NewGroupService(groupRepo GroupRepository, authCacheInvalidator APIKeyAuthCacheInvalidator) *GroupService {
- return &GroupService{
- groupRepo: groupRepo,
- authCacheInvalidator: authCacheInvalidator,
- }
- }
- // Create 创建分组
- func (s *GroupService) Create(ctx context.Context, req CreateGroupRequest) (*Group, error) {
- imageRateMultiplier := 1.0
- if req.ImageRateMultiplier != nil {
- if *req.ImageRateMultiplier < 0 {
- return nil, fmt.Errorf("image_rate_multiplier must be >= 0")
- }
- imageRateMultiplier = *req.ImageRateMultiplier
- }
- // 检查名称是否已存在
- exists, err := s.groupRepo.ExistsByName(ctx, req.Name)
- if err != nil {
- return nil, fmt.Errorf("check group exists: %w", err)
- }
- if exists {
- return nil, ErrGroupExists
- }
- // 创建分组
- group := &Group{
- Name: req.Name,
- Description: req.Description,
- Platform: PlatformAnthropic,
- RateMultiplier: req.RateMultiplier,
- IsExclusive: req.IsExclusive,
- Status: StatusActive,
- SubscriptionType: SubscriptionTypeStandard,
- AllowImageGeneration: req.AllowImageGeneration,
- ImageRateIndependent: req.ImageRateIndependent,
- ImageRateMultiplier: imageRateMultiplier,
- }
- if err := s.groupRepo.Create(ctx, group); err != nil {
- return nil, fmt.Errorf("create group: %w", err)
- }
- return group, nil
- }
- // GetByID 根据ID获取分组
- func (s *GroupService) GetByID(ctx context.Context, id int64) (*Group, error) {
- group, err := s.groupRepo.GetByID(ctx, id)
- if err != nil {
- return nil, fmt.Errorf("get group: %w", err)
- }
- return group, nil
- }
- // List 获取分组列表
- func (s *GroupService) List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) {
- groups, pagination, err := s.groupRepo.List(ctx, params)
- if err != nil {
- return nil, nil, fmt.Errorf("list groups: %w", err)
- }
- return groups, pagination, nil
- }
- // ListActive 获取活跃分组列表
- func (s *GroupService) ListActive(ctx context.Context) ([]Group, error) {
- groups, err := s.groupRepo.ListActive(ctx)
- if err != nil {
- return nil, fmt.Errorf("list active groups: %w", err)
- }
- return groups, nil
- }
- // Update 更新分组
- func (s *GroupService) Update(ctx context.Context, id int64, req UpdateGroupRequest) (*Group, error) {
- group, err := s.groupRepo.GetByID(ctx, id)
- if err != nil {
- return nil, fmt.Errorf("get group: %w", err)
- }
- // 更新字段
- if req.Name != nil && *req.Name != group.Name {
- // 检查新名称是否已存在
- exists, err := s.groupRepo.ExistsByName(ctx, *req.Name)
- if err != nil {
- return nil, fmt.Errorf("check group exists: %w", err)
- }
- if exists {
- return nil, ErrGroupExists
- }
- group.Name = *req.Name
- }
- if req.Description != nil {
- group.Description = *req.Description
- }
- if req.RateMultiplier != nil {
- group.RateMultiplier = *req.RateMultiplier
- }
- if req.IsExclusive != nil {
- group.IsExclusive = *req.IsExclusive
- }
- if req.Status != nil {
- group.Status = *req.Status
- }
- if req.AllowImageGeneration != nil {
- group.AllowImageGeneration = *req.AllowImageGeneration
- }
- if req.ImageRateIndependent != nil {
- group.ImageRateIndependent = *req.ImageRateIndependent
- }
- if req.ImageRateMultiplier != nil {
- if *req.ImageRateMultiplier < 0 {
- return nil, fmt.Errorf("image_rate_multiplier must be >= 0")
- }
- group.ImageRateMultiplier = *req.ImageRateMultiplier
- }
- if err := s.groupRepo.Update(ctx, group); err != nil {
- return nil, fmt.Errorf("update group: %w", err)
- }
- if s.authCacheInvalidator != nil {
- s.authCacheInvalidator.InvalidateAuthCacheByGroupID(ctx, id)
- }
- return group, nil
- }
- // Delete 删除分组
- func (s *GroupService) Delete(ctx context.Context, id int64) error {
- // 检查分组是否存在
- _, err := s.groupRepo.GetByID(ctx, id)
- if err != nil {
- return fmt.Errorf("get group: %w", err)
- }
- if s.authCacheInvalidator != nil {
- s.authCacheInvalidator.InvalidateAuthCacheByGroupID(ctx, id)
- }
- if err := s.groupRepo.Delete(ctx, id); err != nil {
- return fmt.Errorf("delete group: %w", err)
- }
- return nil
- }
- // GetStats 获取分组统计信息
- func (s *GroupService) GetStats(ctx context.Context, id int64) (map[string]any, error) {
- group, err := s.groupRepo.GetByID(ctx, id)
- if err != nil {
- return nil, fmt.Errorf("get group: %w", err)
- }
- // 获取账号数量
- accountCount, _, err := s.groupRepo.GetAccountCount(ctx, id)
- if err != nil {
- return nil, fmt.Errorf("get account count: %w", err)
- }
- stats := map[string]any{
- "id": group.ID,
- "name": group.Name,
- "rate_multiplier": group.RateMultiplier,
- "is_exclusive": group.IsExclusive,
- "status": group.Status,
- "account_count": accountCount,
- }
- return stats, nil
- }
|