admin_group.go 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549
  1. package service
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "strings"
  8. "time"
  9. dbent "github.com/Wei-Shaw/sub2api/ent"
  10. "github.com/Wei-Shaw/sub2api/internal/config"
  11. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  12. "github.com/Wei-Shaw/sub2api/internal/pkg/claude"
  13. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/geminicli"
  15. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  16. "github.com/Wei-Shaw/sub2api/internal/pkg/openai"
  17. "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
  18. "github.com/Wei-Shaw/sub2api/internal/pkg/xai"
  19. )
  20. // Group management implementations
  21. func (s *adminServiceImpl) ValidateSimpleModeGroupOperation(operation AdminGroupOperation) error {
  22. return ValidateSimpleModeGroupOperation(s.cfg, operation)
  23. }
  24. func (s *adminServiceImpl) ListGroups(ctx context.Context, page, pageSize int, platform, status, search string, isExclusive *bool, sortBy, sortOrder string) ([]Group, int64, error) {
  25. params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder}
  26. var groups []Group
  27. var result *pagination.PaginationResult
  28. var err error
  29. if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
  30. repo, ok := s.groupRepo.(interface {
  31. ListBindableWithFilters(context.Context, pagination.PaginationParams, string, string, string, *bool) ([]Group, *pagination.PaginationResult, error)
  32. })
  33. if !ok {
  34. return nil, 0, errors.New("group repository does not support simple-mode filtering")
  35. }
  36. groups, result, err = repo.ListBindableWithFilters(ctx, params, platform, status, search, isExclusive)
  37. } else {
  38. groups, result, err = s.groupRepo.ListWithFilters(ctx, params, platform, status, search, isExclusive)
  39. }
  40. if err != nil {
  41. return nil, 0, err
  42. }
  43. return groups, result.Total, nil
  44. }
  45. func (s *adminServiceImpl) GetAllGroups(ctx context.Context) ([]Group, error) {
  46. return s.groupRepo.ListActive(ctx)
  47. }
  48. func (s *adminServiceImpl) GetAllGroupsByPlatform(ctx context.Context, platform string) ([]Group, error) {
  49. return s.groupRepo.ListActiveByPlatform(ctx, platform)
  50. }
  51. func (s *adminServiceImpl) GetAllGroupsIncludingInactive(ctx context.Context) ([]Group, error) {
  52. // ListWithFilters with empty status = no status filter, so active + disabled groups are returned.
  53. // PageSize 10000 is intentionally large; group count is O(dozens) in practice.
  54. groups, _, err := s.groupRepo.ListWithFilters(ctx, pagination.PaginationParams{Page: 1, PageSize: 10000}, "", "", "", nil)
  55. return groups, err
  56. }
  57. func (s *adminServiceImpl) GetGroup(ctx context.Context, id int64) (*Group, error) {
  58. group, err := s.groupRepo.GetByID(ctx, id)
  59. if err != nil {
  60. return nil, err
  61. }
  62. if err := s.validateSimpleModeGroupAccess(group); err != nil {
  63. return nil, err
  64. }
  65. return group, nil
  66. }
  67. func (s *adminServiceImpl) validateSimpleModeGroupAccess(group *Group) error {
  68. if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple && !IsGroupBindableInSimpleMode(group) {
  69. return infraerrors.BadRequest("SIMPLE_MODE_GROUP_NOT_BINDABLE", "composite groups are not supported in simple mode")
  70. }
  71. return nil
  72. }
  73. func (s *adminServiceImpl) GetGroupModelsListCandidates(ctx context.Context, id int64, platform string) ([]string, error) {
  74. platform = strings.TrimSpace(platform)
  75. if id > 0 {
  76. group, err := s.groupRepo.GetByIDLite(ctx, id)
  77. if err != nil {
  78. return nil, err
  79. }
  80. if platform == "" {
  81. platform = group.Platform
  82. }
  83. }
  84. if platform == "" {
  85. platform = PlatformAnthropic
  86. }
  87. candidates := defaultModelsListCandidateIDs(platform)
  88. if id <= 0 || s.accountRepo == nil {
  89. return candidates, nil
  90. }
  91. accounts, err := s.accountRepo.ListSchedulableByGroupID(ctx, id)
  92. if err != nil {
  93. return nil, err
  94. }
  95. seen := make(map[string]struct{}, len(candidates))
  96. for _, model := range candidates {
  97. seen[model] = struct{}{}
  98. }
  99. for _, acc := range accounts {
  100. if platform == PlatformComposite {
  101. if !isConcreteRequestPlatform(acc.Platform) {
  102. continue
  103. }
  104. } else if acc.Platform != platform {
  105. continue
  106. }
  107. for model := range acc.GetModelMapping() {
  108. model = strings.TrimSpace(model)
  109. if model == "" {
  110. continue
  111. }
  112. if _, ok := seen[model]; ok {
  113. continue
  114. }
  115. seen[model] = struct{}{}
  116. candidates = append(candidates, model)
  117. }
  118. }
  119. return candidates, nil
  120. }
  121. func (s *adminServiceImpl) ListCompositeRoutes(ctx context.Context, groupID int64) ([]CompositeModelRoute, error) {
  122. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationCompositeRoute); err != nil {
  123. return nil, err
  124. }
  125. if err := s.requireCompositeGroup(ctx, groupID); err != nil {
  126. return nil, err
  127. }
  128. if s.compositeRouteRepo == nil {
  129. return nil, fmt.Errorf("composite route repository is not configured")
  130. }
  131. return s.compositeRouteRepo.ListByGroup(ctx, groupID, true)
  132. }
  133. func (s *adminServiceImpl) CreateCompositeRoute(ctx context.Context, groupID int64, input CompositeRouteInput) (*CompositeModelRoute, error) {
  134. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationCompositeRoute); err != nil {
  135. return nil, err
  136. }
  137. if err := s.requireCompositeGroup(ctx, groupID); err != nil {
  138. return nil, err
  139. }
  140. if s.compositeRouteRepo == nil {
  141. return nil, fmt.Errorf("composite route repository is not configured")
  142. }
  143. route, err := compositeRouteFromInput(groupID, input)
  144. if err != nil {
  145. return nil, err
  146. }
  147. if err := s.compositeRouteRepo.Create(ctx, route); err != nil {
  148. return nil, err
  149. }
  150. return route, nil
  151. }
  152. func (s *adminServiceImpl) UpdateCompositeRoute(ctx context.Context, groupID, routeID int64, input CompositeRouteInput) (*CompositeModelRoute, error) {
  153. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationCompositeRoute); err != nil {
  154. return nil, err
  155. }
  156. if err := s.requireCompositeGroup(ctx, groupID); err != nil {
  157. return nil, err
  158. }
  159. if s.compositeRouteRepo == nil {
  160. return nil, fmt.Errorf("composite route repository is not configured")
  161. }
  162. if ok, err := s.compositeRouteBelongsToGroup(ctx, groupID, routeID); err != nil {
  163. return nil, err
  164. } else if !ok {
  165. return nil, ErrCompositeRouteNotFound
  166. }
  167. route, err := compositeRouteFromInput(groupID, input)
  168. if err != nil {
  169. return nil, err
  170. }
  171. route.ID = routeID
  172. if err := s.compositeRouteRepo.Update(ctx, route); err != nil {
  173. return nil, err
  174. }
  175. return route, nil
  176. }
  177. func (s *adminServiceImpl) DeleteCompositeRoute(ctx context.Context, groupID, routeID int64) error {
  178. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationCompositeRoute); err != nil {
  179. return err
  180. }
  181. if err := s.requireCompositeGroup(ctx, groupID); err != nil {
  182. return err
  183. }
  184. if s.compositeRouteRepo == nil {
  185. return fmt.Errorf("composite route repository is not configured")
  186. }
  187. if ok, err := s.compositeRouteBelongsToGroup(ctx, groupID, routeID); err != nil {
  188. return err
  189. } else if !ok {
  190. return ErrCompositeRouteNotFound
  191. }
  192. return s.compositeRouteRepo.Delete(ctx, routeID)
  193. }
  194. func (s *adminServiceImpl) PreviewCompositeRoute(ctx context.Context, groupID int64, input CompositeRoutePreviewRequest) (*CompositeRouteDecision, error) {
  195. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationCompositeRoute); err != nil {
  196. return nil, err
  197. }
  198. if err := s.requireCompositeGroup(ctx, groupID); err != nil {
  199. return nil, err
  200. }
  201. resolver := s.compositeResolver
  202. if resolver == nil {
  203. resolver = NewCompositeRouteResolver(s.compositeRouteRepo)
  204. }
  205. decision, err := resolver.Resolve(ctx, groupID, input.Model, input.Endpoint)
  206. if err != nil {
  207. return nil, err
  208. }
  209. return &decision, nil
  210. }
  211. func (s *adminServiceImpl) requireCompositeGroup(ctx context.Context, groupID int64) error {
  212. group, err := s.groupRepo.GetByIDLite(ctx, groupID)
  213. if err != nil {
  214. return err
  215. }
  216. if group.Platform != PlatformComposite {
  217. return fmt.Errorf("group %d is not a composite group", groupID)
  218. }
  219. return nil
  220. }
  221. func (s *adminServiceImpl) compositeRouteBelongsToGroup(ctx context.Context, groupID, routeID int64) (bool, error) {
  222. routes, err := s.compositeRouteRepo.ListByGroup(ctx, groupID, true)
  223. if err != nil {
  224. return false, err
  225. }
  226. for i := range routes {
  227. if routes[i].ID == routeID {
  228. return true, nil
  229. }
  230. }
  231. return false, nil
  232. }
  233. func compositeRouteFromInput(groupID int64, input CompositeRouteInput) (*CompositeModelRoute, error) {
  234. input = normalizeCompositeRouteInput(input)
  235. if input.PublicModel == "" {
  236. return nil, fmt.Errorf("public_model is required")
  237. }
  238. if !isConcreteRequestPlatform(input.TargetPlatform) {
  239. return nil, fmt.Errorf("target_platform must be a concrete provider")
  240. }
  241. if input.Priority == 0 {
  242. input.Priority = 100
  243. }
  244. return &CompositeModelRoute{
  245. GroupID: groupID,
  246. PublicModel: input.PublicModel,
  247. MatchType: input.MatchType,
  248. TargetPlatform: input.TargetPlatform,
  249. UpstreamModel: input.UpstreamModel,
  250. Endpoint: input.Endpoint,
  251. Priority: input.Priority,
  252. Enabled: input.Enabled,
  253. Notes: input.Notes,
  254. }, nil
  255. }
  256. func defaultModelsListCandidateIDs(platform string) []string {
  257. switch platform {
  258. case PlatformOpenAI:
  259. return openai.DefaultModelIDs()
  260. case PlatformGemini:
  261. ids := make([]string, 0, len(geminicli.DefaultModels))
  262. for _, model := range geminicli.DefaultModels {
  263. ids = append(ids, model.ID)
  264. }
  265. return ids
  266. case PlatformAntigravity:
  267. models := antigravity.DefaultModels()
  268. ids := make([]string, 0, len(models))
  269. for _, model := range models {
  270. ids = append(ids, model.ID)
  271. }
  272. return ids
  273. case PlatformGrok:
  274. return xai.DefaultModelIDs()
  275. case PlatformComposite:
  276. return compositeDefaultModelsListCandidateIDs()
  277. default:
  278. ids := make([]string, 0, len(claude.DefaultModels))
  279. for _, model := range claude.DefaultModels {
  280. ids = append(ids, model.ID)
  281. }
  282. return ids
  283. }
  284. }
  285. func defaultAllowImageGenerationForPlatform(platform string) bool {
  286. // Grok image and video generation routes share the legacy image-generation gate.
  287. // Older clients send the false zero value, so Grok groups must default enabled.
  288. return platform == PlatformGrok
  289. }
  290. func compositeDefaultModelsListCandidateIDs() []string {
  291. seen := make(map[string]struct{})
  292. ids := make([]string, 0)
  293. for _, platform := range []string{PlatformAnthropic, PlatformGemini, PlatformOpenAI, PlatformAntigravity, PlatformGrok, PlatformKimi, PlatformZhipu, PlatformDeepseek, PlatformMiniMax} {
  294. for _, id := range defaultModelsListCandidateIDs(platform) {
  295. if _, ok := seen[id]; ok {
  296. continue
  297. }
  298. seen[id] = struct{}{}
  299. ids = append(ids, id)
  300. }
  301. }
  302. return ids
  303. }
  304. func canCopyAccountsFromGroupPlatform(targetPlatform, sourcePlatform string) bool {
  305. if targetPlatform == PlatformComposite {
  306. return sourcePlatform == PlatformComposite || isConcreteRequestPlatform(sourcePlatform)
  307. }
  308. return sourcePlatform == targetPlatform
  309. }
  310. func groupSupportsOAuthOnlyFilter(platform string) bool {
  311. return platform == PlatformOpenAI ||
  312. platform == PlatformAntigravity ||
  313. platform == PlatformAnthropic ||
  314. platform == PlatformGemini ||
  315. platform == PlatformGrok ||
  316. platform == PlatformComposite
  317. }
  318. func groupSupportsOpenAIFast(platform string) bool {
  319. return platform == PlatformOpenAI || platform == PlatformComposite
  320. }
  321. func sanitizeGroupOpenAIFast(group *Group) {
  322. if group == nil || !groupSupportsOpenAIFast(group.Platform) {
  323. if group != nil {
  324. group.ForceOpenAIFast = false
  325. group.FreeOpenAIFast = false
  326. }
  327. }
  328. }
  329. func normalizeCreateGroupInputForSimpleMode(input *CreateGroupInput) {
  330. if input == nil {
  331. return
  332. }
  333. *input = CreateGroupInput{
  334. Name: input.Name, Description: input.Description, Platform: input.Platform,
  335. RateMultiplier: 1, SubscriptionType: SubscriptionTypeStandard,
  336. }
  337. }
  338. func normalizeUpdateGroupInputForSimpleMode(input *UpdateGroupInput) {
  339. if input == nil {
  340. return
  341. }
  342. *input = UpdateGroupInput{Name: input.Name, Description: input.Description}
  343. }
  344. func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupInput) (*Group, error) {
  345. if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple && NormalizeGroupPlatform(input.Platform) == PlatformComposite {
  346. return nil, infraerrors.BadRequest("SIMPLE_MODE_GROUP_NOT_BINDABLE", "composite groups are not supported in simple mode")
  347. }
  348. if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
  349. normalizeCreateGroupInputForSimpleMode(input)
  350. }
  351. if input.RateMultiplier <= 0 {
  352. return nil, errors.New("rate_multiplier must be > 0")
  353. }
  354. platform := NormalizeGroupPlatform(input.Platform)
  355. // 固定账号 manifest 配置:账号绑定发生在创建之后,创建时无法校验成员关系,
  356. // 拒绝开启并在创建后的编辑里配置。
  357. if normalizeCodexModelsManifestConfig(platform, input.CodexModelsManifestConfig).Enabled {
  358. return nil, infraerrors.New(http.StatusBadRequest, "INVALID_CODEX_MODELS_MANIFEST_CONFIG", "codex models manifest config cannot be enabled at group creation; configure it after creation in the group editor")
  359. }
  360. modelPricing, err := normalizeGroupModelPricing(platform, input.ModelPricing)
  361. if err != nil {
  362. return nil, err
  363. }
  364. maxReasoningEffort, err := normalizeMaxReasoningEffortForPlatform(platform, input.MaxReasoningEffort)
  365. if err != nil {
  366. return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_MAX_REASONING_EFFORT", "%v", err)
  367. }
  368. maxReasoningEffortOverLimit, err := normalizeMaxReasoningEffortOverLimitForPlatform(platform, input.MaxReasoningEffortOverLimit)
  369. if err != nil {
  370. return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_MAX_REASONING_EFFORT_OVER_LIMIT", "%v", err)
  371. }
  372. reasoningEffortMappings, err := NormalizeReasoningEffortMappings(platform, input.ReasoningEffortMappings)
  373. if err != nil {
  374. return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_REASONING_EFFORT_MAPPING", "%v", err)
  375. }
  376. subscriptionType := input.SubscriptionType
  377. if subscriptionType == "" {
  378. subscriptionType = SubscriptionTypeStandard
  379. }
  380. // 限额字段:nil/负数 表示"无限制",0 表示"不允许用量",正数表示具体限额
  381. dailyLimit := normalizeLimit(input.DailyLimitUSD)
  382. weeklyLimit := normalizeLimit(input.WeeklyLimitUSD)
  383. monthlyLimit := normalizeLimit(input.MonthlyLimitUSD)
  384. // 图片价格:负数表示清除(使用默认价格),0 保留(表示免费)
  385. imagePrice1K := normalizePrice(input.ImagePrice1K)
  386. imagePrice2K := normalizePrice(input.ImagePrice2K)
  387. imagePrice4K := normalizePrice(input.ImagePrice4K)
  388. videoPrice480P := normalizePrice(input.VideoPrice480P)
  389. videoPrice720P := normalizePrice(input.VideoPrice720P)
  390. videoPrice1080P := normalizePrice(input.VideoPrice1080P)
  391. webSearchPricePerCall := normalizePrice(input.WebSearchPricePerCall)
  392. searchPricePer1k := normalizePrice(input.SearchPricePer1k)
  393. audioRealtimePricePerMin := normalizePrice(input.AudioRealtimePricePerMin)
  394. audioTTSPricePerMillionChars := normalizePrice(input.AudioTTSPricePerMillionChars)
  395. audioSTTPricePerHour := normalizePrice(input.AudioSTTPricePerHour)
  396. imageRateMultiplier := 1.0
  397. if input.ImageRateMultiplier != nil {
  398. if *input.ImageRateMultiplier < 0 {
  399. return nil, errors.New("image_rate_multiplier must be >= 0")
  400. }
  401. imageRateMultiplier = *input.ImageRateMultiplier
  402. }
  403. batchImageDiscountMultiplier := defaultBatchImageDiscountMultiplier
  404. if input.BatchImageDiscountMultiplier != nil {
  405. if *input.BatchImageDiscountMultiplier < 0 {
  406. return nil, errors.New("batch_image_discount_multiplier must be >= 0")
  407. }
  408. batchImageDiscountMultiplier = *input.BatchImageDiscountMultiplier
  409. }
  410. batchImageHoldMultiplier := defaultBatchImageHoldMultiplier
  411. if input.BatchImageHoldMultiplier != nil {
  412. if *input.BatchImageHoldMultiplier < 0 {
  413. return nil, errors.New("batch_image_hold_multiplier must be >= 0")
  414. }
  415. batchImageHoldMultiplier = *input.BatchImageHoldMultiplier
  416. }
  417. // 不变式:hold 比例 >= discount 比例。否则批量任务成功率足够高时
  418. // 实际成本会超过冻结额,结算永远失败、用户冻结余额无法解冻。
  419. if batchImageHoldMultiplier < batchImageDiscountMultiplier {
  420. return nil, errors.New("batch_image_hold_multiplier must be >= batch_image_discount_multiplier")
  421. }
  422. videoRateMultiplier := 1.0
  423. if input.VideoRateMultiplier != nil {
  424. if *input.VideoRateMultiplier < 0 {
  425. return nil, errors.New("video_rate_multiplier must be >= 0")
  426. }
  427. videoRateMultiplier = *input.VideoRateMultiplier
  428. }
  429. peakRateMultiplier := 1.0
  430. if input.PeakRateMultiplier != nil {
  431. peakRateMultiplier = *input.PeakRateMultiplier
  432. }
  433. // 先归一化(非订阅分组清空高峰配置、清洗停用状态下的脏字段)再校验,与 UpdateGroup 同一收口。
  434. peakRateEnabled, peakStart, peakEnd, peakRateMultiplier := NormalizePeakRateConfig(subscriptionType, input.PeakRateEnabled, input.PeakStart, input.PeakEnd, peakRateMultiplier)
  435. if err := ValidatePeakRateConfig(subscriptionType, peakRateEnabled, peakStart, peakEnd, peakRateMultiplier); err != nil {
  436. return nil, err
  437. }
  438. profitMinMargin := 0.0
  439. if input.ProfitMinMargin != nil {
  440. profitMinMargin = *input.ProfitMinMargin
  441. }
  442. profitSafetyBuffer := 0.0
  443. if input.ProfitSafetyBuffer != nil {
  444. profitSafetyBuffer = *input.ProfitSafetyBuffer
  445. }
  446. // 利润控制与高峰倍率同一收口顺序:先按平台归一化(不支持的平台重置),再校验。
  447. profitControlEnabled, profitMinMargin, profitSafetyBuffer := NormalizeProfitControlConfig(platform, input.ProfitControlEnabled, profitMinMargin, profitSafetyBuffer)
  448. if err := ValidateProfitControlConfig(platform, profitControlEnabled, profitMinMargin, profitSafetyBuffer); err != nil {
  449. return nil, err
  450. }
  451. // 校验降级分组
  452. if input.FallbackGroupID != nil {
  453. if err := s.validateFallbackGroup(ctx, 0, *input.FallbackGroupID); err != nil {
  454. return nil, err
  455. }
  456. }
  457. fallbackOnInvalidRequest := input.FallbackGroupIDOnInvalidRequest
  458. if fallbackOnInvalidRequest != nil && *fallbackOnInvalidRequest <= 0 {
  459. fallbackOnInvalidRequest = nil
  460. }
  461. // 校验无效请求兜底分组
  462. if fallbackOnInvalidRequest != nil {
  463. if err := s.validateFallbackGroupOnInvalidRequest(ctx, 0, platform, subscriptionType, *fallbackOnInvalidRequest); err != nil {
  464. return nil, err
  465. }
  466. }
  467. securityPolicyMode, err := validateSecurityPolicyModeForWrite(input.SecurityPolicyMode)
  468. if err != nil {
  469. return nil, err
  470. }
  471. securityPolicyEmailEnabled := true
  472. if input.SecurityPolicyEmailEnabled != nil {
  473. securityPolicyEmailEnabled = *input.SecurityPolicyEmailEnabled
  474. }
  475. // MCPXMLInject:默认为 true,仅当显式传入 false 时关闭
  476. mcpXMLInject := true
  477. if input.MCPXMLInject != nil {
  478. mcpXMLInject = *input.MCPXMLInject
  479. }
  480. allowImageGeneration := input.AllowImageGeneration || defaultAllowImageGenerationForPlatform(platform)
  481. allowBatchImageGeneration := input.AllowBatchImageGeneration && allowImageGeneration && platform == PlatformGemini
  482. // 如果指定了复制账号的源分组,先获取账号 ID 列表
  483. var accountIDsToCopy []int64
  484. if len(input.CopyAccountsFromGroupIDs) > 0 {
  485. // 去重源分组 IDs
  486. seen := make(map[int64]struct{})
  487. uniqueSourceGroupIDs := make([]int64, 0, len(input.CopyAccountsFromGroupIDs))
  488. for _, srcGroupID := range input.CopyAccountsFromGroupIDs {
  489. if _, exists := seen[srcGroupID]; !exists {
  490. seen[srcGroupID] = struct{}{}
  491. uniqueSourceGroupIDs = append(uniqueSourceGroupIDs, srcGroupID)
  492. }
  493. }
  494. // 校验源分组的平台是否与新分组一致
  495. for _, srcGroupID := range uniqueSourceGroupIDs {
  496. srcGroup, err := s.groupRepo.GetByIDLite(ctx, srcGroupID)
  497. if err != nil {
  498. return nil, fmt.Errorf("source group %d not found: %w", srcGroupID, err)
  499. }
  500. if !canCopyAccountsFromGroupPlatform(platform, srcGroup.Platform) {
  501. return nil, fmt.Errorf("source group %d platform mismatch: expected %s, got %s", srcGroupID, platform, srcGroup.Platform)
  502. }
  503. }
  504. // 获取所有源分组的账号(去重)
  505. var err error
  506. accountIDsToCopy, err = s.groupRepo.GetAccountIDsByGroupIDs(ctx, uniqueSourceGroupIDs)
  507. if err != nil {
  508. return nil, fmt.Errorf("failed to get accounts from source groups: %w", err)
  509. }
  510. }
  511. // 白名单在创建路径同样收口:开启但为空、通配位置非法都会 400。
  512. modelAllowlist, err := normalizeGroupModelAllowlist(input.ModelAllowlist)
  513. if err != nil {
  514. return nil, err
  515. }
  516. group := &Group{
  517. Name: input.Name,
  518. Description: input.Description,
  519. Platform: platform,
  520. RateMultiplier: input.RateMultiplier,
  521. IsExclusive: input.IsExclusive,
  522. SecurityPolicyEnabled: input.SecurityPolicyEnabled,
  523. SecurityPolicyMode: securityPolicyMode,
  524. SecurityPolicyEmailEnabled: securityPolicyEmailEnabled,
  525. Status: StatusActive,
  526. SubscriptionType: subscriptionType,
  527. DailyLimitUSD: dailyLimit,
  528. WeeklyLimitUSD: weeklyLimit,
  529. MonthlyLimitUSD: monthlyLimit,
  530. LongContextPricingEnabled: input.LongContextPricingEnabled,
  531. ModelPricing: modelPricing,
  532. AllowImageGeneration: allowImageGeneration,
  533. AllowBatchImageGeneration: allowBatchImageGeneration,
  534. ImageRateIndependent: input.ImageRateIndependent,
  535. ImageRateMultiplier: imageRateMultiplier,
  536. BatchImageDiscountMultiplier: batchImageDiscountMultiplier,
  537. BatchImageHoldMultiplier: batchImageHoldMultiplier,
  538. VideoRateIndependent: input.VideoRateIndependent,
  539. VideoRateMultiplier: videoRateMultiplier,
  540. PeakRateEnabled: peakRateEnabled,
  541. PeakStart: peakStart,
  542. PeakEnd: peakEnd,
  543. PeakRateMultiplier: peakRateMultiplier,
  544. ProfitControlEnabled: profitControlEnabled,
  545. ProfitMinMargin: profitMinMargin,
  546. ProfitSafetyBuffer: profitSafetyBuffer,
  547. ImagePrice1K: imagePrice1K,
  548. ImagePrice2K: imagePrice2K,
  549. ImagePrice4K: imagePrice4K,
  550. VideoPrice480P: videoPrice480P,
  551. VideoPrice720P: videoPrice720P,
  552. VideoPrice1080P: videoPrice1080P,
  553. VideoModelPrices: NormalizeVideoModelPrices(input.VideoModelPrices),
  554. WebSearchPricePerCall: webSearchPricePerCall,
  555. SearchPricePer1k: searchPricePer1k,
  556. AudioRealtimePricePerMin: audioRealtimePricePerMin,
  557. AudioTTSPricePerMillionChars: audioTTSPricePerMillionChars,
  558. AudioSTTPricePerHour: audioSTTPricePerHour,
  559. ClaudeCodeOnly: input.ClaudeCodeOnly,
  560. FallbackGroupID: input.FallbackGroupID,
  561. FallbackGroupIDOnInvalidRequest: fallbackOnInvalidRequest,
  562. ModelRouting: input.ModelRouting,
  563. MCPXMLInject: mcpXMLInject,
  564. SupportedModelScopes: input.SupportedModelScopes,
  565. AllowMessagesDispatch: input.AllowMessagesDispatch,
  566. AllowLive: input.AllowLive,
  567. ForceOpenAIFast: input.ForceOpenAIFast,
  568. FreeOpenAIFast: input.FreeOpenAIFast,
  569. RequireOAuthOnly: input.RequireOAuthOnly,
  570. RequirePrivacySet: input.RequirePrivacySet,
  571. DefaultMappedModel: input.DefaultMappedModel,
  572. MessagesDispatchModelConfig: normalizeOpenAIMessagesDispatchModelConfig(input.MessagesDispatchModelConfig),
  573. ModelAllowlist: modelAllowlist,
  574. // 固定账号 manifest 配置:账号绑定发生在分组创建之后,创建路径禁止开启,
  575. // 成员关系无从校验(前端创建对话框也不展示)。
  576. CodexModelsManifestConfig: normalizeCodexModelsManifestConfig(platform, input.CodexModelsManifestConfig),
  577. RPMLimit: input.RPMLimit,
  578. MaxReasoningEffort: maxReasoningEffort,
  579. MaxReasoningEffortOverLimit: maxReasoningEffortOverLimit,
  580. ReasoningEffortMappings: reasoningEffortMappings,
  581. }
  582. sanitizeGroupMessagesDispatchFields(group)
  583. sanitizeGroupOpenAIFast(group)
  584. if group.Platform != PlatformOpenAI && group.Platform != PlatformComposite {
  585. group.AllowLive = false
  586. }
  587. sanitizeGroupReasoningEffortPolicy(group)
  588. if err := s.groupRepo.Create(ctx, group); err != nil {
  589. return nil, err
  590. }
  591. // require_oauth_only: 过滤掉 apikey 类型账号
  592. if group.RequireOAuthOnly && groupSupportsOAuthOnlyFilter(group.Platform) && len(accountIDsToCopy) > 0 {
  593. accounts, err := s.accountRepo.GetByIDs(ctx, accountIDsToCopy)
  594. if err != nil {
  595. return nil, fmt.Errorf("failed to fetch accounts for oauth filter: %w", err)
  596. }
  597. oauthIDs := make(map[int64]struct{}, len(accounts))
  598. for _, acc := range accounts {
  599. if acc.Type != AccountTypeAPIKey {
  600. oauthIDs[acc.ID] = struct{}{}
  601. }
  602. }
  603. var filtered []int64
  604. for _, aid := range accountIDsToCopy {
  605. if _, ok := oauthIDs[aid]; ok {
  606. filtered = append(filtered, aid)
  607. }
  608. }
  609. accountIDsToCopy = filtered
  610. }
  611. // 如果有需要复制的账号,绑定到新分组
  612. if len(accountIDsToCopy) > 0 {
  613. if err := s.groupRepo.BindAccountsToGroup(ctx, group.ID, accountIDsToCopy); err != nil {
  614. return nil, fmt.Errorf("failed to bind accounts to new group: %w", err)
  615. }
  616. group.AccountCount = int64(len(accountIDsToCopy))
  617. }
  618. return group, nil
  619. }
  620. // normalizeLimit 将负数转换为 nil(表示无限制),0 保留(表示限额为零)
  621. func normalizeLimit(limit *float64) *float64 {
  622. if limit == nil || *limit < 0 {
  623. return nil
  624. }
  625. return limit
  626. }
  627. // normalizePrice 将负数转换为 nil(表示使用默认价格),0 保留(表示免费)
  628. func normalizePrice(price *float64) *float64 {
  629. if price == nil || *price < 0 {
  630. return nil
  631. }
  632. return price
  633. }
  634. // validateFallbackGroup 校验降级分组的有效性
  635. // currentGroupID: 当前分组 ID(新建时为 0)
  636. // fallbackGroupID: 降级分组 ID
  637. func (s *adminServiceImpl) validateFallbackGroup(ctx context.Context, currentGroupID, fallbackGroupID int64) error {
  638. // 不能将自己设置为降级分组
  639. if currentGroupID > 0 && currentGroupID == fallbackGroupID {
  640. return fmt.Errorf("cannot set self as fallback group")
  641. }
  642. visited := map[int64]struct{}{}
  643. nextID := fallbackGroupID
  644. for {
  645. if _, seen := visited[nextID]; seen {
  646. return fmt.Errorf("fallback group cycle detected")
  647. }
  648. visited[nextID] = struct{}{}
  649. if currentGroupID > 0 && nextID == currentGroupID {
  650. return fmt.Errorf("fallback group cycle detected")
  651. }
  652. // 检查降级分组是否存在
  653. fallbackGroup, err := s.groupRepo.GetByIDLite(ctx, nextID)
  654. if err != nil {
  655. return fmt.Errorf("fallback group not found: %w", err)
  656. }
  657. // 降级分组不能启用 claude_code_only,否则会造成死循环
  658. if nextID == fallbackGroupID && fallbackGroup.ClaudeCodeOnly {
  659. return fmt.Errorf("fallback group cannot have claude_code_only enabled")
  660. }
  661. if fallbackGroup.FallbackGroupID == nil {
  662. return nil
  663. }
  664. nextID = *fallbackGroup.FallbackGroupID
  665. }
  666. }
  667. // validateFallbackGroupOnInvalidRequest 校验无效请求兜底分组的有效性
  668. // currentGroupID: 当前分组 ID(新建时为 0)
  669. // platform/subscriptionType: 当前分组的有效平台/订阅类型
  670. // fallbackGroupID: 兜底分组 ID
  671. func (s *adminServiceImpl) validateFallbackGroupOnInvalidRequest(ctx context.Context, currentGroupID int64, platform, subscriptionType string, fallbackGroupID int64) error {
  672. if platform != PlatformAnthropic && platform != PlatformAntigravity {
  673. return fmt.Errorf("invalid request fallback only supported for anthropic or antigravity groups")
  674. }
  675. if subscriptionType == SubscriptionTypeSubscription {
  676. return fmt.Errorf("subscription groups cannot set invalid request fallback")
  677. }
  678. if currentGroupID > 0 && currentGroupID == fallbackGroupID {
  679. return fmt.Errorf("cannot set self as invalid request fallback group")
  680. }
  681. fallbackGroup, err := s.groupRepo.GetByIDLite(ctx, fallbackGroupID)
  682. if err != nil {
  683. return fmt.Errorf("fallback group not found: %w", err)
  684. }
  685. if fallbackGroup.Platform != PlatformAnthropic {
  686. return fmt.Errorf("fallback group must be anthropic platform")
  687. }
  688. if fallbackGroup.SubscriptionType == SubscriptionTypeSubscription {
  689. return fmt.Errorf("fallback group cannot be subscription type")
  690. }
  691. if fallbackGroup.FallbackGroupIDOnInvalidRequest != nil {
  692. return fmt.Errorf("fallback group cannot have invalid request fallback configured")
  693. }
  694. return nil
  695. }
  696. func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *UpdateGroupInput) (*Group, error) {
  697. group, err := s.groupRepo.GetByID(ctx, id)
  698. if err != nil {
  699. return nil, err
  700. }
  701. if err := s.validateSimpleModeGroupAccess(group); err != nil {
  702. return nil, err
  703. }
  704. if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple && input.Platform == PlatformComposite {
  705. return nil, infraerrors.BadRequest("SIMPLE_MODE_GROUP_NOT_BINDABLE", "composite groups are not supported in simple mode")
  706. }
  707. if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
  708. normalizeUpdateGroupInputForSimpleMode(input)
  709. }
  710. // 渠道缓存里存了 groupID → platform 的映射,改了平台要让它失效(见函数末尾)
  711. previousPlatform := group.Platform
  712. if input.Name != "" {
  713. group.Name = input.Name
  714. }
  715. if input.Description != nil {
  716. group.Description = *input.Description
  717. }
  718. if input.Platform != "" {
  719. group.Platform = input.Platform
  720. }
  721. if input.RateMultiplier != nil {
  722. if *input.RateMultiplier <= 0 {
  723. return nil, errors.New("rate_multiplier must be > 0")
  724. }
  725. group.RateMultiplier = *input.RateMultiplier
  726. }
  727. if input.SecurityPolicyEnabled != nil {
  728. group.SecurityPolicyEnabled = *input.SecurityPolicyEnabled
  729. }
  730. if input.SecurityPolicyMode != nil {
  731. mode, err := validateSecurityPolicyModeForWrite(*input.SecurityPolicyMode)
  732. if err != nil {
  733. return nil, err
  734. }
  735. group.SecurityPolicyMode = mode
  736. }
  737. if input.SecurityPolicyEmailEnabled != nil {
  738. group.SecurityPolicyEmailEnabled = *input.SecurityPolicyEmailEnabled
  739. }
  740. if input.IsExclusive != nil {
  741. group.IsExclusive = *input.IsExclusive
  742. }
  743. if input.Status != "" {
  744. group.Status = input.Status
  745. }
  746. if input.LongContextPricingEnabled != nil {
  747. group.LongContextPricingEnabled = *input.LongContextPricingEnabled
  748. }
  749. if input.ModelPricing != nil {
  750. modelPricing, normalizeErr := normalizeGroupModelPricing(group.Platform, *input.ModelPricing)
  751. if normalizeErr != nil {
  752. return nil, normalizeErr
  753. }
  754. group.ModelPricing = modelPricing
  755. }
  756. // 订阅相关字段
  757. if input.SubscriptionType != "" {
  758. group.SubscriptionType = input.SubscriptionType
  759. }
  760. // 限额字段:nil 表示不修改,负数表示"无限制",0 表示"不允许用量",正数表示具体限额。
  761. if input.DailyLimitUSD != nil {
  762. group.DailyLimitUSD = normalizeLimit(input.DailyLimitUSD)
  763. }
  764. if input.WeeklyLimitUSD != nil {
  765. group.WeeklyLimitUSD = normalizeLimit(input.WeeklyLimitUSD)
  766. }
  767. if input.MonthlyLimitUSD != nil {
  768. group.MonthlyLimitUSD = normalizeLimit(input.MonthlyLimitUSD)
  769. }
  770. // 图片生成计费配置:负数表示清除(使用默认价格)
  771. if input.AllowImageGeneration != nil {
  772. group.AllowImageGeneration = *input.AllowImageGeneration
  773. }
  774. if input.AllowBatchImageGeneration != nil {
  775. group.AllowBatchImageGeneration = *input.AllowBatchImageGeneration
  776. }
  777. if !group.AllowImageGeneration || group.Platform != PlatformGemini {
  778. group.AllowBatchImageGeneration = false
  779. }
  780. if input.ImageRateIndependent != nil {
  781. group.ImageRateIndependent = *input.ImageRateIndependent
  782. }
  783. if input.ImageRateMultiplier != nil {
  784. if *input.ImageRateMultiplier < 0 {
  785. return nil, errors.New("image_rate_multiplier must be >= 0")
  786. }
  787. group.ImageRateMultiplier = *input.ImageRateMultiplier
  788. }
  789. if input.BatchImageDiscountMultiplier != nil {
  790. if *input.BatchImageDiscountMultiplier < 0 {
  791. return nil, errors.New("batch_image_discount_multiplier must be >= 0")
  792. }
  793. group.BatchImageDiscountMultiplier = *input.BatchImageDiscountMultiplier
  794. }
  795. if input.BatchImageHoldMultiplier != nil {
  796. if *input.BatchImageHoldMultiplier < 0 {
  797. return nil, errors.New("batch_image_hold_multiplier must be >= 0")
  798. }
  799. group.BatchImageHoldMultiplier = *input.BatchImageHoldMultiplier
  800. }
  801. // 仅在本次更新显式触碰任一比例时校验合并后的不变式(hold >= discount),
  802. // 避免存量脏数据阻塞其他字段的正常更新(提交侧另有钳制兜底)。
  803. if (input.BatchImageDiscountMultiplier != nil || input.BatchImageHoldMultiplier != nil) &&
  804. group.BatchImageHoldMultiplier < group.BatchImageDiscountMultiplier {
  805. return nil, errors.New("batch_image_hold_multiplier must be >= batch_image_discount_multiplier")
  806. }
  807. if input.VideoRateIndependent != nil {
  808. group.VideoRateIndependent = *input.VideoRateIndependent
  809. }
  810. if input.VideoRateMultiplier != nil {
  811. if *input.VideoRateMultiplier < 0 {
  812. return nil, errors.New("video_rate_multiplier must be >= 0")
  813. }
  814. group.VideoRateMultiplier = *input.VideoRateMultiplier
  815. }
  816. if input.PeakRateEnabled != nil {
  817. group.PeakRateEnabled = *input.PeakRateEnabled
  818. }
  819. if input.PeakStart != nil {
  820. group.PeakStart = *input.PeakStart
  821. }
  822. if input.PeakEnd != nil {
  823. group.PeakEnd = *input.PeakEnd
  824. }
  825. if input.PeakRateMultiplier != nil {
  826. group.PeakRateMultiplier = *input.PeakRateMultiplier
  827. }
  828. // 先归一化(非订阅分组——含本次更新转为非订阅——静默清空高峰配置,清洗停用状态下的脏字段),
  829. // 再收敛校验:Update 可能只传部分 peak 字段,需对合并后的最终配置统一校验,
  830. // 防止单独修改 start/end 导致最终 start>=end 等非法配置入库。与 CreateGroup 同一收口。
  831. group.PeakRateEnabled, group.PeakStart, group.PeakEnd, group.PeakRateMultiplier = NormalizePeakRateConfig(group.SubscriptionType, group.PeakRateEnabled, group.PeakStart, group.PeakEnd, group.PeakRateMultiplier)
  832. if err := ValidatePeakRateConfig(group.SubscriptionType, group.PeakRateEnabled, group.PeakStart, group.PeakEnd, group.PeakRateMultiplier); err != nil {
  833. return nil, err
  834. }
  835. if input.ProfitControlEnabled != nil {
  836. group.ProfitControlEnabled = *input.ProfitControlEnabled
  837. }
  838. if input.ProfitMinMargin != nil {
  839. group.ProfitMinMargin = *input.ProfitMinMargin
  840. }
  841. if input.ProfitSafetyBuffer != nil {
  842. group.ProfitSafetyBuffer = *input.ProfitSafetyBuffer
  843. }
  844. // 利润控制与高峰同一收口:按合并后的最终平台归一化(转到不支持平台时静默重置),
  845. // 再对合并后的最终配置统一校验,防止部分字段更新拼出非法组合入库。
  846. group.ProfitControlEnabled, group.ProfitMinMargin, group.ProfitSafetyBuffer = NormalizeProfitControlConfig(group.Platform, group.ProfitControlEnabled, group.ProfitMinMargin, group.ProfitSafetyBuffer)
  847. if err := ValidateProfitControlConfig(group.Platform, group.ProfitControlEnabled, group.ProfitMinMargin, group.ProfitSafetyBuffer); err != nil {
  848. return nil, err
  849. }
  850. if input.ImagePrice1K != nil {
  851. group.ImagePrice1K = normalizePrice(input.ImagePrice1K)
  852. }
  853. if input.ImagePrice2K != nil {
  854. group.ImagePrice2K = normalizePrice(input.ImagePrice2K)
  855. }
  856. if input.ImagePrice4K != nil {
  857. group.ImagePrice4K = normalizePrice(input.ImagePrice4K)
  858. }
  859. if input.VideoPrice480P != nil {
  860. group.VideoPrice480P = normalizePrice(input.VideoPrice480P)
  861. }
  862. if input.VideoPrice720P != nil {
  863. group.VideoPrice720P = normalizePrice(input.VideoPrice720P)
  864. }
  865. if input.VideoPrice1080P != nil {
  866. group.VideoPrice1080P = normalizePrice(input.VideoPrice1080P)
  867. }
  868. // nil = leave unchanged; empty map = clear per-model prices.
  869. if input.VideoModelPrices != nil {
  870. group.VideoModelPrices = NormalizeVideoModelPrices(input.VideoModelPrices)
  871. }
  872. if input.WebSearchPricePerCall != nil {
  873. group.WebSearchPricePerCall = normalizePrice(input.WebSearchPricePerCall)
  874. }
  875. if input.SearchPricePer1k != nil {
  876. group.SearchPricePer1k = normalizePrice(input.SearchPricePer1k)
  877. }
  878. if input.AudioRealtimePricePerMin != nil {
  879. group.AudioRealtimePricePerMin = normalizePrice(input.AudioRealtimePricePerMin)
  880. }
  881. if input.AudioTTSPricePerMillionChars != nil {
  882. group.AudioTTSPricePerMillionChars = normalizePrice(input.AudioTTSPricePerMillionChars)
  883. }
  884. if input.AudioSTTPricePerHour != nil {
  885. group.AudioSTTPricePerHour = normalizePrice(input.AudioSTTPricePerHour)
  886. }
  887. // Claude Code 客户端限制
  888. if input.ClaudeCodeOnly != nil {
  889. group.ClaudeCodeOnly = *input.ClaudeCodeOnly
  890. }
  891. if input.FallbackGroupID != nil {
  892. // 校验降级分组
  893. if *input.FallbackGroupID > 0 {
  894. if err := s.validateFallbackGroup(ctx, id, *input.FallbackGroupID); err != nil {
  895. return nil, err
  896. }
  897. group.FallbackGroupID = input.FallbackGroupID
  898. } else {
  899. // 传入 0 或负数表示清除降级分组
  900. group.FallbackGroupID = nil
  901. }
  902. }
  903. fallbackOnInvalidRequest := group.FallbackGroupIDOnInvalidRequest
  904. if input.FallbackGroupIDOnInvalidRequest != nil {
  905. if *input.FallbackGroupIDOnInvalidRequest > 0 {
  906. fallbackOnInvalidRequest = input.FallbackGroupIDOnInvalidRequest
  907. } else {
  908. fallbackOnInvalidRequest = nil
  909. }
  910. }
  911. if fallbackOnInvalidRequest != nil {
  912. if err := s.validateFallbackGroupOnInvalidRequest(ctx, id, group.Platform, group.SubscriptionType, *fallbackOnInvalidRequest); err != nil {
  913. return nil, err
  914. }
  915. }
  916. group.FallbackGroupIDOnInvalidRequest = fallbackOnInvalidRequest
  917. // 模型路由配置
  918. if input.ModelRouting != nil {
  919. group.ModelRouting = input.ModelRouting
  920. }
  921. if input.ModelRoutingEnabled != nil {
  922. group.ModelRoutingEnabled = *input.ModelRoutingEnabled
  923. }
  924. if input.MCPXMLInject != nil {
  925. group.MCPXMLInject = *input.MCPXMLInject
  926. }
  927. // 支持的模型系列(仅 antigravity 平台使用)
  928. if input.SupportedModelScopes != nil {
  929. group.SupportedModelScopes = *input.SupportedModelScopes
  930. }
  931. // OpenAI Messages 调度配置
  932. if input.AllowMessagesDispatch != nil {
  933. group.AllowMessagesDispatch = *input.AllowMessagesDispatch
  934. }
  935. if input.AllowLive != nil {
  936. group.AllowLive = *input.AllowLive
  937. }
  938. if input.ForceOpenAIFast != nil {
  939. group.ForceOpenAIFast = *input.ForceOpenAIFast
  940. }
  941. if input.FreeOpenAIFast != nil {
  942. group.FreeOpenAIFast = *input.FreeOpenAIFast
  943. }
  944. if input.RequireOAuthOnly != nil {
  945. group.RequireOAuthOnly = *input.RequireOAuthOnly
  946. }
  947. if input.RequirePrivacySet != nil {
  948. group.RequirePrivacySet = *input.RequirePrivacySet
  949. }
  950. if input.DefaultMappedModel != nil {
  951. group.DefaultMappedModel = *input.DefaultMappedModel
  952. }
  953. if input.MessagesDispatchModelConfig != nil {
  954. group.MessagesDispatchModelConfig = normalizeOpenAIMessagesDispatchModelConfig(*input.MessagesDispatchModelConfig)
  955. }
  956. if input.ModelAllowlist != nil {
  957. modelAllowlist, err := normalizeGroupModelAllowlist(*input.ModelAllowlist)
  958. if err != nil {
  959. return nil, err
  960. }
  961. group.ModelAllowlist = modelAllowlist
  962. }
  963. if input.CodexModelsManifestConfig != nil {
  964. group.CodexModelsManifestConfig = *input.CodexModelsManifestConfig
  965. }
  966. if input.RPMLimit != nil {
  967. group.RPMLimit = *input.RPMLimit
  968. }
  969. if input.MaxReasoningEffort != nil {
  970. maxReasoningEffort, err := normalizeMaxReasoningEffortForPlatform(group.Platform, *input.MaxReasoningEffort)
  971. if err != nil {
  972. return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_MAX_REASONING_EFFORT", "%v", err)
  973. }
  974. group.MaxReasoningEffort = maxReasoningEffort
  975. }
  976. if input.MaxReasoningEffortOverLimit != nil {
  977. maxReasoningEffortOverLimit, err := normalizeMaxReasoningEffortOverLimitForPlatform(group.Platform, *input.MaxReasoningEffortOverLimit)
  978. if err != nil {
  979. return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_MAX_REASONING_EFFORT_OVER_LIMIT", "%v", err)
  980. }
  981. group.MaxReasoningEffortOverLimit = maxReasoningEffortOverLimit
  982. }
  983. if input.ReasoningEffortMappings != nil {
  984. reasoningEffortMappings, err := NormalizeReasoningEffortMappings(group.Platform, *input.ReasoningEffortMappings)
  985. if err != nil {
  986. return nil, infraerrors.Newf(http.StatusBadRequest, "INVALID_REASONING_EFFORT_MAPPING", "%v", err)
  987. }
  988. group.ReasoningEffortMappings = reasoningEffortMappings
  989. }
  990. sanitizeGroupMessagesDispatchFields(group)
  991. sanitizeGroupOpenAIFast(group)
  992. if group.Platform != PlatformOpenAI && group.Platform != PlatformComposite {
  993. group.AllowLive = false
  994. }
  995. sanitizeGroupReasoningEffortPolicy(group)
  996. // 固定账号 manifest 配置:按最终平台归一化(切出 openai 平台时静默归零,
  997. // 与 ForceOpenAIFast 同一收口);校验仅在本次显式携带配置时进行,
  998. // 避免脏 ID 阻塞无关字段更新。
  999. group.CodexModelsManifestConfig = normalizeCodexModelsManifestConfig(group.Platform, group.CodexModelsManifestConfig)
  1000. if input.CodexModelsManifestConfig != nil {
  1001. if err := s.validateCodexModelsManifestConfig(ctx, id, group.CodexModelsManifestConfig); err != nil {
  1002. return nil, err
  1003. }
  1004. }
  1005. if err := s.groupRepo.Update(ctx, group); err != nil {
  1006. return nil, err
  1007. }
  1008. if s.authCacheInvalidator != nil {
  1009. s.authCacheInvalidator.InvalidateAuthCacheByGroupID(ctx, id)
  1010. }
  1011. // 平台变了就失效渠道缓存:该缓存持有 groupID → platform,而渠道定价 / 模型映射 /
  1012. // 模型白名单都按平台严格隔离。不失效的话,缓存最长 10 分钟仍按旧平台匹配,
  1013. // 期间定价查不到会静默回落到 LiteLLM 价格表、映射与白名单也不生效。
  1014. if group.Platform != previousPlatform && s.channelCacheInvalidator != nil {
  1015. s.channelCacheInvalidator.InvalidateCache()
  1016. }
  1017. // 如果指定了复制账号的源分组,同步绑定(替换当前分组的账号)
  1018. if len(input.CopyAccountsFromGroupIDs) > 0 {
  1019. // 去重源分组 IDs
  1020. seen := make(map[int64]struct{})
  1021. uniqueSourceGroupIDs := make([]int64, 0, len(input.CopyAccountsFromGroupIDs))
  1022. for _, srcGroupID := range input.CopyAccountsFromGroupIDs {
  1023. // 校验:源分组不能是自身
  1024. if srcGroupID == id {
  1025. return nil, fmt.Errorf("cannot copy accounts from self")
  1026. }
  1027. // 去重
  1028. if _, exists := seen[srcGroupID]; !exists {
  1029. seen[srcGroupID] = struct{}{}
  1030. uniqueSourceGroupIDs = append(uniqueSourceGroupIDs, srcGroupID)
  1031. }
  1032. }
  1033. // 校验源分组的平台是否与当前分组一致
  1034. for _, srcGroupID := range uniqueSourceGroupIDs {
  1035. srcGroup, err := s.groupRepo.GetByIDLite(ctx, srcGroupID)
  1036. if err != nil {
  1037. return nil, fmt.Errorf("source group %d not found: %w", srcGroupID, err)
  1038. }
  1039. if !canCopyAccountsFromGroupPlatform(group.Platform, srcGroup.Platform) {
  1040. return nil, fmt.Errorf("source group %d platform mismatch: expected %s, got %s", srcGroupID, group.Platform, srcGroup.Platform)
  1041. }
  1042. }
  1043. // 获取所有源分组的账号(去重)
  1044. accountIDsToCopy, err := s.groupRepo.GetAccountIDsByGroupIDs(ctx, uniqueSourceGroupIDs)
  1045. if err != nil {
  1046. return nil, fmt.Errorf("failed to get accounts from source groups: %w", err)
  1047. }
  1048. // 先清空当前分组的所有账号绑定
  1049. if _, err := s.groupRepo.DeleteAccountGroupsByGroupID(ctx, id); err != nil {
  1050. return nil, fmt.Errorf("failed to clear existing account bindings: %w", err)
  1051. }
  1052. // require_oauth_only: 过滤掉 apikey 类型账号
  1053. if group.RequireOAuthOnly && groupSupportsOAuthOnlyFilter(group.Platform) && len(accountIDsToCopy) > 0 {
  1054. accounts, err := s.accountRepo.GetByIDs(ctx, accountIDsToCopy)
  1055. if err != nil {
  1056. return nil, fmt.Errorf("failed to fetch accounts for oauth filter: %w", err)
  1057. }
  1058. oauthIDs := make(map[int64]struct{}, len(accounts))
  1059. for _, acc := range accounts {
  1060. if acc.Type != AccountTypeAPIKey {
  1061. oauthIDs[acc.ID] = struct{}{}
  1062. }
  1063. }
  1064. var filtered []int64
  1065. for _, aid := range accountIDsToCopy {
  1066. if _, ok := oauthIDs[aid]; ok {
  1067. filtered = append(filtered, aid)
  1068. }
  1069. }
  1070. accountIDsToCopy = filtered
  1071. }
  1072. // 再绑定源分组的账号
  1073. if len(accountIDsToCopy) > 0 {
  1074. if err := s.groupRepo.BindAccountsToGroup(ctx, id, accountIDsToCopy); err != nil {
  1075. return nil, fmt.Errorf("failed to bind accounts to group: %w", err)
  1076. }
  1077. }
  1078. }
  1079. return group, nil
  1080. }
  1081. func normalizeGroupModelPricing(platform string, pricing []ChannelModelPricing) ([]ChannelModelPricing, error) {
  1082. out := make([]ChannelModelPricing, len(pricing))
  1083. for i := range pricing {
  1084. out[i] = pricing[i].Clone()
  1085. out[i].ID = 0
  1086. out[i].ChannelID = 0
  1087. if out[i].TimePricing != nil && len(out[i].TimePricing.Periods) > 0 {
  1088. return nil, infraerrors.BadRequest(
  1089. "GROUP_MODEL_TIME_PRICING_UNSUPPORTED",
  1090. "group model pricing does not support time pricing",
  1091. )
  1092. }
  1093. if strings.TrimSpace(out[i].Platform) == "" {
  1094. out[i].Platform = platform
  1095. }
  1096. for j := range out[i].Models {
  1097. out[i].Models[j] = strings.TrimSpace(out[i].Models[j])
  1098. }
  1099. if len(out[i].Models) == 0 {
  1100. return nil, infraerrors.New(http.StatusBadRequest, "GROUP_MODEL_PRICING_MODELS_REQUIRED", "group model pricing entry requires at least one model")
  1101. }
  1102. }
  1103. if err := validatePricingEntries(out); err != nil {
  1104. return nil, err
  1105. }
  1106. return out, nil
  1107. }
  1108. func (s *adminServiceImpl) DeleteGroup(ctx context.Context, id int64) error {
  1109. return s.deleteGroup(ctx, id, false)
  1110. }
  1111. func (s *adminServiceImpl) DeleteGroupIfEmpty(ctx context.Context, id int64) error {
  1112. return s.deleteGroup(ctx, id, true)
  1113. }
  1114. func (s *adminServiceImpl) deleteGroup(ctx context.Context, id int64, requireEmpty bool) error {
  1115. if s.cfg != nil && s.cfg.RunMode == config.RunModeSimple {
  1116. group, err := s.groupRepo.GetByIDLite(ctx, id)
  1117. if err != nil {
  1118. return err
  1119. }
  1120. if err := s.validateSimpleModeGroupAccess(group); err != nil {
  1121. return err
  1122. }
  1123. }
  1124. if requireEmpty && s.emptyGroupDeleteRepo == nil {
  1125. return fmt.Errorf("guarded group deletion is unavailable")
  1126. }
  1127. var groupKeys []string
  1128. if s.authCacheInvalidator != nil {
  1129. keys, err := s.apiKeyRepo.ListKeysByGroupID(ctx, id)
  1130. if err == nil {
  1131. groupKeys = keys
  1132. }
  1133. }
  1134. var affectedUserIDs []int64
  1135. var err error
  1136. if requireEmpty {
  1137. affectedUserIDs, err = s.emptyGroupDeleteRepo.DeleteCascadeIfEmpty(ctx, id)
  1138. } else {
  1139. affectedUserIDs, err = s.groupRepo.DeleteCascade(ctx, id)
  1140. }
  1141. if err != nil {
  1142. return err
  1143. }
  1144. // 注意:user_group_rate_multipliers 表通过外键 ON DELETE CASCADE 自动清理
  1145. // 事务成功后,异步失效受影响用户的订阅缓存
  1146. if len(affectedUserIDs) > 0 && s.billingCacheService != nil {
  1147. groupID := id
  1148. go func() {
  1149. cacheCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  1150. defer cancel()
  1151. for _, userID := range affectedUserIDs {
  1152. if err := s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID); err != nil {
  1153. logger.LegacyPrintf("service.admin", "invalidate subscription cache failed: user_id=%d group_id=%d err=%v", userID, groupID, err)
  1154. }
  1155. }
  1156. }()
  1157. }
  1158. if s.authCacheInvalidator != nil {
  1159. for _, key := range groupKeys {
  1160. s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, key)
  1161. }
  1162. }
  1163. return nil
  1164. }
  1165. func (s *adminServiceImpl) GetGroupAPIKeys(ctx context.Context, groupID int64, page, pageSize int) ([]APIKey, int64, error) {
  1166. params := pagination.PaginationParams{Page: page, PageSize: pageSize}
  1167. keys, result, err := s.apiKeyRepo.ListByGroupID(ctx, groupID, params)
  1168. if err != nil {
  1169. return nil, 0, err
  1170. }
  1171. return keys, result.Total, nil
  1172. }
  1173. func (s *adminServiceImpl) GetGroupRateMultipliers(ctx context.Context, groupID int64) ([]UserGroupRateEntry, error) {
  1174. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationMultiplier); err != nil {
  1175. return nil, err
  1176. }
  1177. if s.userGroupRateRepo == nil {
  1178. return nil, nil
  1179. }
  1180. return s.userGroupRateRepo.GetByGroupID(ctx, groupID)
  1181. }
  1182. func (s *adminServiceImpl) ClearGroupRateMultipliers(ctx context.Context, groupID int64) error {
  1183. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationMultiplier); err != nil {
  1184. return err
  1185. }
  1186. if s.userGroupRateRepo == nil {
  1187. return nil
  1188. }
  1189. return s.userGroupRateRepo.DeleteByGroupID(ctx, groupID)
  1190. }
  1191. func (s *adminServiceImpl) BatchSetGroupRateMultipliers(ctx context.Context, groupID int64, entries []GroupRateMultiplierInput) error {
  1192. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationMultiplier); err != nil {
  1193. return err
  1194. }
  1195. if s.userGroupRateRepo == nil {
  1196. return nil
  1197. }
  1198. for _, e := range entries {
  1199. if e.RateMultiplier <= 0 {
  1200. return fmt.Errorf("rate_multiplier must be > 0 (user_id=%d)", e.UserID)
  1201. }
  1202. }
  1203. return s.userGroupRateRepo.SyncGroupRateMultipliers(ctx, groupID, entries)
  1204. }
  1205. func (s *adminServiceImpl) ClearGroupRPMOverrides(ctx context.Context, groupID int64) error {
  1206. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationRPMOverride); err != nil {
  1207. return err
  1208. }
  1209. if s.userGroupRateRepo == nil {
  1210. return nil
  1211. }
  1212. if err := s.userGroupRateRepo.ClearGroupRPMOverrides(ctx, groupID); err != nil {
  1213. return err
  1214. }
  1215. // RPM override 已嵌入 auth cache snapshot (v7),变更后必须失效相关缓存。
  1216. if s.authCacheInvalidator != nil {
  1217. s.authCacheInvalidator.InvalidateAuthCacheByGroupID(ctx, groupID)
  1218. }
  1219. return nil
  1220. }
  1221. func (s *adminServiceImpl) BatchSetGroupRPMOverrides(ctx context.Context, groupID int64, entries []GroupRPMOverrideInput) error {
  1222. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationRPMOverride); err != nil {
  1223. return err
  1224. }
  1225. if s.userGroupRateRepo == nil {
  1226. return nil
  1227. }
  1228. for _, e := range entries {
  1229. if e.RPMOverride != nil && *e.RPMOverride < 0 {
  1230. return infraerrors.BadRequest("INVALID_RPM_OVERRIDE", fmt.Sprintf("rpm_override must be >= 0 (user_id=%d)", e.UserID))
  1231. }
  1232. }
  1233. if err := s.userGroupRateRepo.SyncGroupRPMOverrides(ctx, groupID, entries); err != nil {
  1234. return err
  1235. }
  1236. // RPM override 已嵌入 auth cache snapshot (v7),变更后必须失效相关缓存。
  1237. if s.authCacheInvalidator != nil {
  1238. s.authCacheInvalidator.InvalidateAuthCacheByGroupID(ctx, groupID)
  1239. }
  1240. return nil
  1241. }
  1242. func (s *adminServiceImpl) UpdateGroupSortOrders(ctx context.Context, updates []GroupSortOrderUpdate) error {
  1243. if err := s.ValidateSimpleModeGroupOperation(AdminGroupOperationSort); err != nil {
  1244. return err
  1245. }
  1246. return s.groupRepo.UpdateSortOrders(ctx, updates)
  1247. }
  1248. // AdminUpdateAPIKeyGroupID 管理员修改 API Key 分组绑定
  1249. // groupID: nil=不修改, 指向0=解绑, 指向正整数=绑定到目标分组
  1250. func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID int64, groupID *int64) (*AdminUpdateAPIKeyGroupIDResult, error) {
  1251. apiKey, err := s.apiKeyRepo.GetByID(ctx, keyID)
  1252. if err != nil {
  1253. return nil, err
  1254. }
  1255. if err := s.authorizeManagedUserResource(ctx, apiKey.UserID); err != nil {
  1256. return nil, err
  1257. }
  1258. if groupID == nil {
  1259. // nil 表示不修改,直接返回
  1260. return &AdminUpdateAPIKeyGroupIDResult{APIKey: apiKey}, nil
  1261. }
  1262. if *groupID < 0 {
  1263. return nil, infraerrors.BadRequest("INVALID_GROUP_ID", "group_id must be non-negative")
  1264. }
  1265. result := &AdminUpdateAPIKeyGroupIDResult{}
  1266. if *groupID == 0 {
  1267. // 0 表示解绑分组(不修改 user_allowed_groups,避免影响用户其他 Key)
  1268. apiKey.GroupID = nil
  1269. apiKey.Group = nil
  1270. } else {
  1271. // 验证目标分组存在且状态为 active
  1272. group, err := s.groupRepo.GetByID(ctx, *groupID)
  1273. if err != nil {
  1274. return nil, err
  1275. }
  1276. if group.Status != StatusActive {
  1277. return nil, infraerrors.BadRequest("GROUP_NOT_ACTIVE", "target group is not active")
  1278. }
  1279. // 订阅类型分组:用户须持有该分组的有效订阅才可绑定
  1280. if group.IsSubscriptionType() {
  1281. if s.userSubRepo == nil {
  1282. return nil, infraerrors.InternalServer("SUBSCRIPTION_REPOSITORY_UNAVAILABLE", "subscription repository is not configured")
  1283. }
  1284. if _, err := s.userSubRepo.GetActiveByUserIDAndGroupID(ctx, apiKey.UserID, *groupID); err != nil {
  1285. if errors.Is(err, ErrSubscriptionNotFound) {
  1286. return nil, infraerrors.BadRequest("SUBSCRIPTION_REQUIRED", "user does not have an active subscription for this group")
  1287. }
  1288. return nil, err
  1289. }
  1290. }
  1291. gid := *groupID
  1292. apiKey.GroupID = &gid
  1293. apiKey.Group = group
  1294. // 专属标准分组:使用事务保证「添加分组权限」与「更新 API Key」的原子性
  1295. if group.IsExclusive && !group.IsSubscriptionType() {
  1296. opCtx := ctx
  1297. var tx *dbent.Tx
  1298. if s.entClient == nil {
  1299. logger.LegacyPrintf("service.admin", "Warning: entClient is nil, skipping transaction protection for exclusive group binding")
  1300. } else {
  1301. var txErr error
  1302. tx, txErr = s.entClient.Tx(ctx)
  1303. if txErr != nil {
  1304. return nil, fmt.Errorf("begin transaction: %w", txErr)
  1305. }
  1306. defer func() { _ = tx.Rollback() }()
  1307. opCtx = dbent.NewTxContext(ctx, tx)
  1308. }
  1309. if addErr := s.userRepo.AddGroupToAllowedGroups(opCtx, apiKey.UserID, gid); addErr != nil {
  1310. return nil, fmt.Errorf("add group to user allowed groups: %w", addErr)
  1311. }
  1312. if err := s.apiKeyRepo.Update(opCtx, apiKey, APIKeyUpdateFields{GroupID: true}); err != nil {
  1313. return nil, fmt.Errorf("update api key: %w", err)
  1314. }
  1315. if tx != nil {
  1316. if err := tx.Commit(); err != nil {
  1317. return nil, fmt.Errorf("commit transaction: %w", err)
  1318. }
  1319. }
  1320. result.AutoGrantedGroupAccess = true
  1321. result.GrantedGroupID = &gid
  1322. result.GrantedGroupName = group.Name
  1323. // 失效认证缓存(在事务提交后执行)
  1324. if s.authCacheInvalidator != nil {
  1325. s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, apiKey.AuthCacheInvalidationKey())
  1326. }
  1327. result.APIKey = apiKey
  1328. return result, nil
  1329. }
  1330. }
  1331. // 非专属分组 / 解绑:无需事务,单步更新即可
  1332. if err := s.apiKeyRepo.Update(ctx, apiKey, APIKeyUpdateFields{GroupID: true}); err != nil {
  1333. return nil, fmt.Errorf("update api key: %w", err)
  1334. }
  1335. // 失效认证缓存
  1336. if s.authCacheInvalidator != nil {
  1337. s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, apiKey.AuthCacheInvalidationKey())
  1338. }
  1339. result.APIKey = apiKey
  1340. return result, nil
  1341. }
  1342. // AdminResetAPIKeyRateLimitUsage resets all API key rate-limit usage windows.
  1343. func (s *adminServiceImpl) AdminResetAPIKeyRateLimitUsage(ctx context.Context, keyID int64) (*APIKey, error) {
  1344. apiKey, err := s.apiKeyRepo.GetByID(ctx, keyID)
  1345. if err != nil {
  1346. return nil, err
  1347. }
  1348. if err := s.authorizeManagedUserResource(ctx, apiKey.UserID); err != nil {
  1349. return nil, err
  1350. }
  1351. apiKey.Usage5h = 0
  1352. apiKey.Usage1d = 0
  1353. apiKey.Usage7d = 0
  1354. apiKey.Window5hStart = nil
  1355. apiKey.Window1dStart = nil
  1356. apiKey.Window7dStart = nil
  1357. if err := s.apiKeyRepo.Update(ctx, apiKey, APIKeyUpdateFields{RateLimitUsage: true}); err != nil {
  1358. return nil, fmt.Errorf("reset api key rate limit usage: %w", err)
  1359. }
  1360. if s.authCacheInvalidator != nil {
  1361. s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, apiKey.AuthCacheInvalidationKey())
  1362. }
  1363. if s.billingCacheService != nil {
  1364. _ = s.billingCacheService.InvalidateAPIKeyRateLimit(ctx, apiKey.ID)
  1365. }
  1366. return apiKey, nil
  1367. }
  1368. // ReplaceUserGroup 替换用户的专属分组
  1369. func (s *adminServiceImpl) ReplaceUserGroup(ctx context.Context, userID, oldGroupID, newGroupID int64) (*ReplaceUserGroupResult, error) {
  1370. if oldGroupID == newGroupID {
  1371. return nil, infraerrors.BadRequest("SAME_GROUP", "old and new group must be different")
  1372. }
  1373. // 验证新分组存在且为活跃的专属标准分组
  1374. newGroup, err := s.groupRepo.GetByID(ctx, newGroupID)
  1375. if err != nil {
  1376. return nil, err
  1377. }
  1378. if newGroup.Status != StatusActive {
  1379. return nil, infraerrors.BadRequest("GROUP_NOT_ACTIVE", "target group is not active")
  1380. }
  1381. if !newGroup.IsExclusive {
  1382. return nil, infraerrors.BadRequest("GROUP_NOT_EXCLUSIVE", "target group is not exclusive")
  1383. }
  1384. if newGroup.IsSubscriptionType() {
  1385. return nil, infraerrors.BadRequest("GROUP_IS_SUBSCRIPTION", "subscription groups are not supported for replacement")
  1386. }
  1387. // 事务保证原子性
  1388. if s.entClient == nil {
  1389. return nil, fmt.Errorf("entClient is nil, cannot perform group replacement")
  1390. }
  1391. tx, err := s.entClient.Tx(ctx)
  1392. if err != nil {
  1393. return nil, fmt.Errorf("begin transaction: %w", err)
  1394. }
  1395. defer func() { _ = tx.Rollback() }()
  1396. opCtx := dbent.NewTxContext(ctx, tx)
  1397. // 1. 授予新分组权限
  1398. if err := s.userRepo.AddGroupToAllowedGroups(opCtx, userID, newGroupID); err != nil {
  1399. return nil, fmt.Errorf("add new group to allowed groups: %w", err)
  1400. }
  1401. // 2. 迁移绑定旧分组的 Key 到新分组
  1402. migrated, err := s.apiKeyRepo.UpdateGroupIDByUserAndGroup(opCtx, userID, oldGroupID, newGroupID)
  1403. if err != nil {
  1404. return nil, fmt.Errorf("migrate api keys: %w", err)
  1405. }
  1406. // 3. 移除旧分组权限
  1407. if err := s.userRepo.RemoveGroupFromUserAllowedGroups(opCtx, userID, oldGroupID); err != nil {
  1408. return nil, fmt.Errorf("remove old group from allowed groups: %w", err)
  1409. }
  1410. if err := tx.Commit(); err != nil {
  1411. return nil, fmt.Errorf("commit transaction: %w", err)
  1412. }
  1413. // 失效该用户所有 Key 的认证缓存
  1414. if s.authCacheInvalidator != nil {
  1415. keys, keyErr := s.apiKeyRepo.ListKeysByUserID(ctx, userID)
  1416. if keyErr == nil {
  1417. for _, k := range keys {
  1418. s.authCacheInvalidator.InvalidateAuthCacheByKey(ctx, k)
  1419. }
  1420. }
  1421. }
  1422. return &ReplaceUserGroupResult{MigratedKeys: migrated}, nil
  1423. }
  1424. func validateSecurityPolicyModeForWrite(mode string) (string, error) {
  1425. trimmed := strings.TrimSpace(mode)
  1426. if trimmed == "" {
  1427. return SecurityPolicyModeBlockSession, nil
  1428. }
  1429. if trimmed == SecurityPolicyModeBlockSession || trimmed == SecurityPolicyModeBlockRequest {
  1430. return trimmed, nil
  1431. }
  1432. return "", infraerrors.Newf(http.StatusBadRequest, "INVALID_SECURITY_POLICY_MODE", "security_policy_mode must be block_session or block_request, got %q", mode)
  1433. }