99 "strings"
1010 "time"
1111
12+ dbent "github.com/Wei-Shaw/sub2api/ent"
1213 infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
1314 "github.com/Wei-Shaw/sub2api/internal/pkg/httpclient"
1415 "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
@@ -44,7 +45,7 @@ type AdminService interface {
4445 UpdateGroupSortOrders (ctx context.Context , updates []GroupSortOrderUpdate ) error
4546
4647 // API Key management (admin)
47- AdminUpdateAPIKeyGroupID (ctx context.Context , keyID int64 , groupID * int64 ) (* APIKey , error )
48+ AdminUpdateAPIKeyGroupID (ctx context.Context , keyID int64 , groupID * int64 ) (* AdminUpdateAPIKeyGroupIDResult , error )
4849
4950 // Account management
5051 ListAccounts (ctx context.Context , page , pageSize int , platform , accountType , status , search string , groupID int64 ) ([]Account , int64 , error )
@@ -246,6 +247,14 @@ type BulkUpdateAccountResult struct {
246247 Error string `json:"error,omitempty"`
247248}
248249
250+ // AdminUpdateAPIKeyGroupIDResult is the result of AdminUpdateAPIKeyGroupID.
251+ type AdminUpdateAPIKeyGroupIDResult struct {
252+ APIKey * APIKey
253+ AutoGrantedGroupAccess bool // true if a new exclusive group permission was auto-added
254+ GrantedGroupID * int64 // the group ID that was auto-granted
255+ GrantedGroupName string // the group name that was auto-granted
256+ }
257+
249258// BulkUpdateAccountsResult is the aggregated response for bulk updates.
250259type BulkUpdateAccountsResult struct {
251260 Success int `json:"success"`
@@ -410,6 +419,7 @@ type adminServiceImpl struct {
410419 proxyProber ProxyExitInfoProber
411420 proxyLatencyCache ProxyLatencyCache
412421 authCacheInvalidator APIKeyAuthCacheInvalidator
422+ entClient * dbent.Client // 用于开启数据库事务
413423}
414424
415425type userGroupRateBatchReader interface {
@@ -434,6 +444,7 @@ func NewAdminService(
434444 proxyProber ProxyExitInfoProber ,
435445 proxyLatencyCache ProxyLatencyCache ,
436446 authCacheInvalidator APIKeyAuthCacheInvalidator ,
447+ entClient * dbent.Client ,
437448) AdminService {
438449 return & adminServiceImpl {
439450 userRepo : userRepo ,
@@ -448,6 +459,7 @@ func NewAdminService(
448459 proxyProber : proxyProber ,
449460 proxyLatencyCache : proxyLatencyCache ,
450461 authCacheInvalidator : authCacheInvalidator ,
462+ entClient : entClient ,
451463 }
452464}
453465
@@ -1191,23 +1203,25 @@ func (s *adminServiceImpl) UpdateGroupSortOrders(ctx context.Context, updates []
11911203
11921204// AdminUpdateAPIKeyGroupID 管理员修改 API Key 分组绑定
11931205// groupID: nil=不修改, 指向0=解绑, 指向正整数=绑定到目标分组
1194- func (s * adminServiceImpl ) AdminUpdateAPIKeyGroupID (ctx context.Context , keyID int64 , groupID * int64 ) (* APIKey , error ) {
1206+ func (s * adminServiceImpl ) AdminUpdateAPIKeyGroupID (ctx context.Context , keyID int64 , groupID * int64 ) (* AdminUpdateAPIKeyGroupIDResult , error ) {
11951207 apiKey , err := s .apiKeyRepo .GetByID (ctx , keyID )
11961208 if err != nil {
11971209 return nil , err
11981210 }
11991211
12001212 if groupID == nil {
12011213 // nil 表示不修改,直接返回
1202- return apiKey , nil
1214+ return & AdminUpdateAPIKeyGroupIDResult { APIKey : apiKey } , nil
12031215 }
12041216
12051217 if * groupID < 0 {
12061218 return nil , infraerrors .BadRequest ("INVALID_GROUP_ID" , "group_id must be non-negative" )
12071219 }
12081220
1221+ result := & AdminUpdateAPIKeyGroupIDResult {}
1222+
12091223 if * groupID == 0 {
1210- // 0 表示解绑分组
1224+ // 0 表示解绑分组(不修改 user_allowed_groups,避免影响用户其他 Key)
12111225 apiKey .GroupID = nil
12121226 apiKey .Group = nil
12131227 } else {
@@ -1219,11 +1233,58 @@ func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i
12191233 if group .Status != StatusActive {
12201234 return nil , infraerrors .BadRequest ("GROUP_NOT_ACTIVE" , "target group is not active" )
12211235 }
1236+ // 订阅类型分组:不允许通过此 API 直接绑定,需通过订阅管理流程
1237+ if group .IsSubscriptionType () {
1238+ return nil , infraerrors .BadRequest ("SUBSCRIPTION_GROUP_NOT_ALLOWED" , "subscription groups must be managed through the subscription workflow" )
1239+ }
1240+
12221241 gid := * groupID
12231242 apiKey .GroupID = & gid
12241243 apiKey .Group = group
1244+
1245+ // 专属标准分组:使用事务保证「添加分组权限」与「更新 API Key」的原子性
1246+ if group .IsExclusive {
1247+ opCtx := ctx
1248+ var tx * dbent.Tx
1249+ if s .entClient == nil {
1250+ logger .LegacyPrintf ("service.admin" , "Warning: entClient is nil, skipping transaction protection for exclusive group binding" )
1251+ } else {
1252+ var txErr error
1253+ tx , txErr = s .entClient .Tx (ctx )
1254+ if txErr != nil {
1255+ return nil , fmt .Errorf ("begin transaction: %w" , txErr )
1256+ }
1257+ defer func () { _ = tx .Rollback () }()
1258+ opCtx = dbent .NewTxContext (ctx , tx )
1259+ }
1260+
1261+ if addErr := s .userRepo .AddGroupToAllowedGroups (opCtx , apiKey .UserID , gid ); addErr != nil {
1262+ return nil , fmt .Errorf ("add group to user allowed groups: %w" , addErr )
1263+ }
1264+ if err := s .apiKeyRepo .Update (opCtx , apiKey ); err != nil {
1265+ return nil , fmt .Errorf ("update api key: %w" , err )
1266+ }
1267+ if tx != nil {
1268+ if err := tx .Commit (); err != nil {
1269+ return nil , fmt .Errorf ("commit transaction: %w" , err )
1270+ }
1271+ }
1272+
1273+ result .AutoGrantedGroupAccess = true
1274+ result .GrantedGroupID = & gid
1275+ result .GrantedGroupName = group .Name
1276+
1277+ // 失效认证缓存(在事务提交后执行)
1278+ if s .authCacheInvalidator != nil {
1279+ s .authCacheInvalidator .InvalidateAuthCacheByKey (ctx , apiKey .Key )
1280+ }
1281+
1282+ result .APIKey = apiKey
1283+ return result , nil
1284+ }
12251285 }
12261286
1287+ // 非专属分组 / 解绑:无需事务,单步更新即可
12271288 if err := s .apiKeyRepo .Update (ctx , apiKey ); err != nil {
12281289 return nil , fmt .Errorf ("update api key: %w" , err )
12291290 }
@@ -1233,7 +1294,8 @@ func (s *adminServiceImpl) AdminUpdateAPIKeyGroupID(ctx context.Context, keyID i
12331294 s .authCacheInvalidator .InvalidateAuthCacheByKey (ctx , apiKey .Key )
12341295 }
12351296
1236- return apiKey , nil
1297+ result .APIKey = apiKey
1298+ return result , nil
12371299}
12381300
12391301// Account management implementations
0 commit comments