diff --git a/internal/handler/group_handler.go b/internal/handler/group_handler.go index e02278f..4a6681d 100644 --- a/internal/handler/group_handler.go +++ b/internal/handler/group_handler.go @@ -19,6 +19,7 @@ import ( "gpt-load/internal/channel" "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" "gorm.io/datatypes" ) @@ -345,20 +346,20 @@ func (s *Server) UpdateGroup(c *gin.Context) { // GroupResponse defines the structure for a group response, excluding sensitive or large fields. type GroupResponse struct { - ID uint `json:"id"` - Name string `json:"name"` - Endpoint string `json:"endpoint"` - DisplayName string `json:"display_name"` - Description string `json:"description"` - Upstreams datatypes.JSON `json:"upstreams"` - ChannelType string `json:"channel_type"` - Sort int `json:"sort"` - TestModel string `json:"test_model"` - ParamOverrides datatypes.JSONMap `json:"param_overrides"` - Config datatypes.JSONMap `json:"config"` - LastValidatedAt *time.Time `json:"last_validated_at"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `json:"id"` + Name string `json:"name"` + Endpoint string `json:"endpoint"` + DisplayName string `json:"display_name"` + Description string `json:"description"` + Upstreams datatypes.JSON `json:"upstreams"` + ChannelType string `json:"channel_type"` + Sort int `json:"sort"` + TestModel string `json:"test_model"` + ParamOverrides datatypes.JSONMap `json:"param_overrides"` + Config datatypes.JSONMap `json:"config"` + LastValidatedAt *time.Time `json:"last_validated_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // newGroupResponse creates a new GroupResponse from a models.Group. @@ -391,7 +392,6 @@ func (s *Server) newGroupResponse(group *models.Group) *GroupResponse { } } - // DeleteGroup handles deleting a group. func (s *Server) DeleteGroup(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) @@ -400,6 +400,19 @@ func (s *Server) DeleteGroup(c *gin.Context) { return } + // First, get all API keys for this group to clean up from memory store + var apiKeys []models.APIKey + if err := s.DB.Where("group_id = ?", id).Find(&apiKeys).Error; err != nil { + response.Error(c, app_errors.ParseDBError(err)) + return + } + + // Extract key IDs for memory store cleanup + var keyIDs []uint + for _, key := range apiKeys { + keyIDs = append(keyIDs, key.ID) + } + // Use a transaction to ensure atomicity tx := s.DB.Begin() if tx.Error != nil { @@ -412,20 +425,25 @@ func (s *Server) DeleteGroup(c *gin.Context) { } }() - // Also delete associated API keys + // First check if the group exists + var group models.Group + if err := tx.First(&group, id).Error; err != nil { + tx.Rollback() + response.Error(c, app_errors.ParseDBError(err)) + return + } + + // Delete associated API keys first due to foreign key constraint if err := tx.Where("group_id = ?", id).Delete(&models.APIKey{}).Error; err != nil { tx.Rollback() response.Error(c, app_errors.ErrDatabase) return } - if result := tx.Delete(&models.Group{}, id); result.Error != nil { + // Then delete the group + if err := tx.Delete(&models.Group{}, id).Error; err != nil { tx.Rollback() - response.Error(c, app_errors.ParseDBError(result.Error)) - return - } else if result.RowsAffected == 0 { - tx.Rollback() - response.Error(c, app_errors.ErrResourceNotFound) + response.Error(c, app_errors.ParseDBError(err)) return } @@ -435,15 +453,34 @@ func (s *Server) DeleteGroup(c *gin.Context) { return } + // Clean up memory store (Redis) - this is done after successful DB transaction + // to maintain consistency. If this fails, the keys will be cleaned up during + // the next key pool reload. + if len(keyIDs) > 0 { + if err := s.KeyService.KeyProvider.RemoveKeysFromStore(uint(id), keyIDs); err != nil { + logrus.WithFields(logrus.Fields{ + "groupID": id, + "keyCount": len(keyIDs), + "error": err, + }).Error("Failed to remove keys from memory store") + + response.Success(c, gin.H{ + "message": "Group and associated keys deleted successfully", + "warning": "Some keys may remain in memory cache and will be cleaned up during next restart", + }) + return + } + } + response.Success(c, gin.H{"message": "Group and associated keys deleted successfully"}) } // ConfigOption represents a single configurable option for a group. type ConfigOption struct { - Key string `json:"key"` - Name string `json:"name"` - Description string `json:"description"` - DefaultValue any `json:"default_value"` + Key string `json:"key"` + Name string `json:"name"` + Description string `json:"description"` + DefaultValue any `json:"default_value"` } // GetGroupConfigOptions returns a list of available configuration options for groups. diff --git a/internal/keypool/provider.go b/internal/keypool/provider.go index 225642a..e9edeab 100644 --- a/internal/keypool/provider.go +++ b/internal/keypool/provider.go @@ -459,6 +459,44 @@ func (p *KeyProvider) RemoveInvalidKeys(groupID uint) (int64, error) { return removedCount, err } +// RemoveKeysFromStore 直接从内存存储中移除指定的键,不涉及数据库操作 +// 这个方法适用于数据库已经删除但需要清理内存存储的场景 +func (p *KeyProvider) RemoveKeysFromStore(groupID uint, keyIDs []uint) error { + if len(keyIDs) == 0 { + return nil + } + + activeKeysListKey := fmt.Sprintf("group:%d:active_keys", groupID) + + // 第一步:直接删除整个 active_keys 列表 + if err := p.store.Delete(activeKeysListKey); err != nil { + logrus.WithFields(logrus.Fields{ + "groupID": groupID, + "error": err, + }).Error("Failed to delete active keys list") + // 继续执行hash删除,因为即使列表删除失败,hash仍然需要清理 + } + + // 第二步:批量删除所有相关的key hash + for _, keyID := range keyIDs { + keyHashKey := fmt.Sprintf("key:%d", keyID) + if err := p.store.Delete(keyHashKey); err != nil { + logrus.WithFields(logrus.Fields{ + "keyID": keyID, + "error": err, + }).Error("Failed to delete key hash") + // 继续删除其他keys,不因单个失败而中断 + } + } + + logrus.WithFields(logrus.Fields{ + "groupID": groupID, + "keyCount": len(keyIDs), + }).Info("Successfully cleaned up group keys from store") + + return nil +} + // addKeyToStore is a helper to add a single key to the cache. func (p *KeyProvider) addKeyToStore(key *models.APIKey) error { // 1. Store key details in HASH