group_service.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. package service
  2. import (
  3. "context"
  4. "fmt"
  5. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  6. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  7. )
  8. var (
  9. ErrGroupNotFound = infraerrors.NotFound("GROUP_NOT_FOUND", "group not found")
  10. ErrGroupExists = infraerrors.Conflict("GROUP_EXISTS", "group name already exists")
  11. ErrGroupNotEmpty = infraerrors.Conflict("GROUP_NOT_EMPTY", "group contains accounts")
  12. )
  13. type GroupRepository interface {
  14. Create(ctx context.Context, group *Group) error
  15. GetByID(ctx context.Context, id int64) (*Group, error)
  16. GetByIDLite(ctx context.Context, id int64) (*Group, error)
  17. Update(ctx context.Context, group *Group) error
  18. Delete(ctx context.Context, id int64) error
  19. DeleteCascade(ctx context.Context, id int64) ([]int64, error)
  20. List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error)
  21. ListWithFilters(ctx context.Context, params pagination.PaginationParams, platform, status, search string, isExclusive *bool) ([]Group, *pagination.PaginationResult, error)
  22. ListActive(ctx context.Context) ([]Group, error)
  23. ListActiveByPlatform(ctx context.Context, platform string) ([]Group, error)
  24. ExistsByName(ctx context.Context, name string) (bool, error)
  25. GetAccountCount(ctx context.Context, groupID int64) (total int64, active int64, err error)
  26. DeleteAccountGroupsByGroupID(ctx context.Context, groupID int64) (int64, error)
  27. // GetAccountIDsByGroupIDs 获取多个分组的所有账号 ID(去重)
  28. GetAccountIDsByGroupIDs(ctx context.Context, groupIDs []int64) ([]int64, error)
  29. // BindAccountsToGroup 将多个账号绑定到指定分组
  30. BindAccountsToGroup(ctx context.Context, groupID int64, accountIDs []int64) error
  31. // UpdateSortOrders 批量更新分组排序
  32. UpdateSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error
  33. }
  34. type GroupDuplicateRepository interface {
  35. // FindByDuplicateOperationID performs the read-only recovery lookup used
  36. // after an ambiguous idempotency-store failure.
  37. FindByDuplicateOperationID(ctx context.Context, operationID string) (*Group, error)
  38. // CreateFromSource atomically persists the group, copies the source group's
  39. // exact account priorities, and writes the scheduler outbox event.
  40. CreateFromSource(ctx context.Context, group *Group, sourceGroupID int64) error
  41. }
  42. // AdminGroupRepository makes the group-duplication write capability an explicit
  43. // admin-service dependency without widening gateway-only group test doubles.
  44. type AdminGroupRepository interface {
  45. GroupRepository
  46. GroupDuplicateRepository
  47. EmptyGroupDeleteRepository
  48. }
  49. // EmptyGroupDeleteRepository provides the guarded cascade used by simple mode.
  50. type EmptyGroupDeleteRepository interface {
  51. DeleteCascadeIfEmpty(ctx context.Context, id int64) ([]int64, error)
  52. }
  53. // GroupSortOrderUpdate 分组排序更新
  54. type GroupSortOrderUpdate struct {
  55. ID int64 `json:"id"`
  56. SortOrder int `json:"sort_order"`
  57. }
  58. // CreateGroupRequest 创建分组请求
  59. type CreateGroupRequest struct {
  60. Name string `json:"name"`
  61. Description string `json:"description"`
  62. RateMultiplier float64 `json:"rate_multiplier"`
  63. IsExclusive bool `json:"is_exclusive"`
  64. AllowImageGeneration bool `json:"allow_image_generation"`
  65. ImageRateIndependent bool `json:"image_rate_independent"`
  66. ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
  67. }
  68. // UpdateGroupRequest 更新分组请求
  69. type UpdateGroupRequest struct {
  70. Name *string `json:"name"`
  71. Description *string `json:"description"`
  72. RateMultiplier *float64 `json:"rate_multiplier"`
  73. IsExclusive *bool `json:"is_exclusive"`
  74. Status *string `json:"status"`
  75. AllowImageGeneration *bool `json:"allow_image_generation"`
  76. ImageRateIndependent *bool `json:"image_rate_independent"`
  77. ImageRateMultiplier *float64 `json:"image_rate_multiplier"`
  78. }
  79. // GroupService 分组管理服务
  80. type GroupService struct {
  81. groupRepo GroupRepository
  82. authCacheInvalidator APIKeyAuthCacheInvalidator
  83. }
  84. // NewGroupService 创建分组服务实例
  85. func NewGroupService(groupRepo GroupRepository, authCacheInvalidator APIKeyAuthCacheInvalidator) *GroupService {
  86. return &GroupService{
  87. groupRepo: groupRepo,
  88. authCacheInvalidator: authCacheInvalidator,
  89. }
  90. }
  91. // Create 创建分组
  92. func (s *GroupService) Create(ctx context.Context, req CreateGroupRequest) (*Group, error) {
  93. imageRateMultiplier := 1.0
  94. if req.ImageRateMultiplier != nil {
  95. if *req.ImageRateMultiplier < 0 {
  96. return nil, fmt.Errorf("image_rate_multiplier must be >= 0")
  97. }
  98. imageRateMultiplier = *req.ImageRateMultiplier
  99. }
  100. // 检查名称是否已存在
  101. exists, err := s.groupRepo.ExistsByName(ctx, req.Name)
  102. if err != nil {
  103. return nil, fmt.Errorf("check group exists: %w", err)
  104. }
  105. if exists {
  106. return nil, ErrGroupExists
  107. }
  108. // 创建分组
  109. group := &Group{
  110. Name: req.Name,
  111. Description: req.Description,
  112. Platform: PlatformAnthropic,
  113. RateMultiplier: req.RateMultiplier,
  114. IsExclusive: req.IsExclusive,
  115. Status: StatusActive,
  116. SubscriptionType: SubscriptionTypeStandard,
  117. AllowImageGeneration: req.AllowImageGeneration,
  118. ImageRateIndependent: req.ImageRateIndependent,
  119. ImageRateMultiplier: imageRateMultiplier,
  120. }
  121. if err := s.groupRepo.Create(ctx, group); err != nil {
  122. return nil, fmt.Errorf("create group: %w", err)
  123. }
  124. return group, nil
  125. }
  126. // GetByID 根据ID获取分组
  127. func (s *GroupService) GetByID(ctx context.Context, id int64) (*Group, error) {
  128. group, err := s.groupRepo.GetByID(ctx, id)
  129. if err != nil {
  130. return nil, fmt.Errorf("get group: %w", err)
  131. }
  132. return group, nil
  133. }
  134. // List 获取分组列表
  135. func (s *GroupService) List(ctx context.Context, params pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) {
  136. groups, pagination, err := s.groupRepo.List(ctx, params)
  137. if err != nil {
  138. return nil, nil, fmt.Errorf("list groups: %w", err)
  139. }
  140. return groups, pagination, nil
  141. }
  142. // ListActive 获取活跃分组列表
  143. func (s *GroupService) ListActive(ctx context.Context) ([]Group, error) {
  144. groups, err := s.groupRepo.ListActive(ctx)
  145. if err != nil {
  146. return nil, fmt.Errorf("list active groups: %w", err)
  147. }
  148. return groups, nil
  149. }
  150. // Update 更新分组
  151. func (s *GroupService) Update(ctx context.Context, id int64, req UpdateGroupRequest) (*Group, error) {
  152. group, err := s.groupRepo.GetByID(ctx, id)
  153. if err != nil {
  154. return nil, fmt.Errorf("get group: %w", err)
  155. }
  156. // 更新字段
  157. if req.Name != nil && *req.Name != group.Name {
  158. // 检查新名称是否已存在
  159. exists, err := s.groupRepo.ExistsByName(ctx, *req.Name)
  160. if err != nil {
  161. return nil, fmt.Errorf("check group exists: %w", err)
  162. }
  163. if exists {
  164. return nil, ErrGroupExists
  165. }
  166. group.Name = *req.Name
  167. }
  168. if req.Description != nil {
  169. group.Description = *req.Description
  170. }
  171. if req.RateMultiplier != nil {
  172. group.RateMultiplier = *req.RateMultiplier
  173. }
  174. if req.IsExclusive != nil {
  175. group.IsExclusive = *req.IsExclusive
  176. }
  177. if req.Status != nil {
  178. group.Status = *req.Status
  179. }
  180. if req.AllowImageGeneration != nil {
  181. group.AllowImageGeneration = *req.AllowImageGeneration
  182. }
  183. if req.ImageRateIndependent != nil {
  184. group.ImageRateIndependent = *req.ImageRateIndependent
  185. }
  186. if req.ImageRateMultiplier != nil {
  187. if *req.ImageRateMultiplier < 0 {
  188. return nil, fmt.Errorf("image_rate_multiplier must be >= 0")
  189. }
  190. group.ImageRateMultiplier = *req.ImageRateMultiplier
  191. }
  192. if err := s.groupRepo.Update(ctx, group); err != nil {
  193. return nil, fmt.Errorf("update group: %w", err)
  194. }
  195. if s.authCacheInvalidator != nil {
  196. s.authCacheInvalidator.InvalidateAuthCacheByGroupID(ctx, id)
  197. }
  198. return group, nil
  199. }
  200. // Delete 删除分组
  201. func (s *GroupService) Delete(ctx context.Context, id int64) error {
  202. // 检查分组是否存在
  203. _, err := s.groupRepo.GetByID(ctx, id)
  204. if err != nil {
  205. return fmt.Errorf("get group: %w", err)
  206. }
  207. if s.authCacheInvalidator != nil {
  208. s.authCacheInvalidator.InvalidateAuthCacheByGroupID(ctx, id)
  209. }
  210. if err := s.groupRepo.Delete(ctx, id); err != nil {
  211. return fmt.Errorf("delete group: %w", err)
  212. }
  213. return nil
  214. }
  215. // GetStats 获取分组统计信息
  216. func (s *GroupService) GetStats(ctx context.Context, id int64) (map[string]any, error) {
  217. group, err := s.groupRepo.GetByID(ctx, id)
  218. if err != nil {
  219. return nil, fmt.Errorf("get group: %w", err)
  220. }
  221. // 获取账号数量
  222. accountCount, _, err := s.groupRepo.GetAccountCount(ctx, id)
  223. if err != nil {
  224. return nil, fmt.Errorf("get account count: %w", err)
  225. }
  226. stats := map[string]any{
  227. "id": group.ID,
  228. "name": group.Name,
  229. "rate_multiplier": group.RateMultiplier,
  230. "is_exclusive": group.IsExclusive,
  231. "status": group.Status,
  232. "account_count": accountCount,
  233. }
  234. return stats, nil
  235. }