feat: key接口完善及错误处理
This commit is contained in:
@@ -147,21 +147,38 @@ func (s *KeyCronService) validateGroup(ctx context.Context, group *models.Group)
|
||||
func (s *KeyCronService) worker(ctx context.Context, wg *sync.WaitGroup, group *models.Group, jobs <-chan models.APIKey, results chan<- models.APIKey) {
|
||||
defer wg.Done()
|
||||
for key := range jobs {
|
||||
isValid, err := s.Validator.ValidateSingleKey(ctx, &key, group)
|
||||
// Only update status if there was no error during validation
|
||||
if err != nil {
|
||||
logrus.Warnf("KeyCronService: Failed to validate key ID %d for group %s: %v. Skipping status update.", key.ID, group.Name, err)
|
||||
continue
|
||||
isValid, validationErr := s.Validator.ValidateSingleKey(ctx, &key, group)
|
||||
|
||||
newStatus := key.Status
|
||||
newErrorReason := key.ErrorReason
|
||||
statusChanged := false
|
||||
|
||||
if validationErr != nil {
|
||||
// Validation failed, mark as inactive and record the reason
|
||||
newStatus = "inactive"
|
||||
newErrorReason = validationErr.Error()
|
||||
} else {
|
||||
// Validation succeeded
|
||||
if isValid {
|
||||
newStatus = "active"
|
||||
newErrorReason = "" // Clear reason on success
|
||||
} else {
|
||||
// This case might happen if the key is valid but has no quota, etc.
|
||||
// The error would be in validationErr, so this branch is less likely.
|
||||
// We still mark it as inactive but without a specific error from our side.
|
||||
newStatus = "inactive"
|
||||
newErrorReason = "Validation returned false without a specific error."
|
||||
}
|
||||
}
|
||||
|
||||
newStatus := "inactive"
|
||||
if isValid {
|
||||
newStatus = "active"
|
||||
// Check if status or error reason has changed
|
||||
if key.Status != newStatus || key.ErrorReason != newErrorReason {
|
||||
statusChanged = true
|
||||
}
|
||||
|
||||
// Only send to results if the status has changed
|
||||
if key.Status != newStatus {
|
||||
if statusChanged {
|
||||
key.Status = newStatus
|
||||
key.ErrorReason = newErrorReason
|
||||
results <- key
|
||||
}
|
||||
}
|
||||
@@ -171,36 +188,24 @@ func (s *KeyCronService) batchUpdateKeyStatus(keys []models.APIKey) {
|
||||
if len(keys) == 0 {
|
||||
return
|
||||
}
|
||||
logrus.Infof("KeyCronService: Batch updating status for %d keys.", len(keys))
|
||||
|
||||
activeIDs := []uint{}
|
||||
inactiveIDs := []uint{}
|
||||
|
||||
for _, key := range keys {
|
||||
if key.Status == "active" {
|
||||
activeIDs = append(activeIDs, key.ID)
|
||||
} else {
|
||||
inactiveIDs = append(inactiveIDs, key.ID)
|
||||
}
|
||||
}
|
||||
logrus.Infof("KeyCronService: Batch updating status/reason for %d keys.", len(keys))
|
||||
|
||||
err := s.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if len(activeIDs) > 0 {
|
||||
if err := tx.Model(&models.APIKey{}).Where("id IN ?", activeIDs).Update("status", "active").Error; err != nil {
|
||||
return err
|
||||
for _, key := range keys {
|
||||
updates := map[string]interface{}{
|
||||
"status": key.Status,
|
||||
"error_reason": key.ErrorReason,
|
||||
}
|
||||
logrus.Infof("KeyCronService: Set %d keys to 'active'.", len(activeIDs))
|
||||
}
|
||||
if len(inactiveIDs) > 0 {
|
||||
if err := tx.Model(&models.APIKey{}).Where("id IN ?", inactiveIDs).Update("status", "inactive").Error; err != nil {
|
||||
return err
|
||||
if err := tx.Model(&models.APIKey{}).Where("id = ?", key.ID).Updates(updates).Error; err != nil {
|
||||
// Log the error for this specific key but continue the transaction
|
||||
logrus.Errorf("KeyCronService: Failed to update key ID %d: %v", key.ID, err)
|
||||
}
|
||||
logrus.Infof("KeyCronService: Set %d keys to 'inactive'.", len(inactiveIDs))
|
||||
}
|
||||
return nil
|
||||
return nil // Commit the transaction even if some updates failed
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logrus.Errorf("KeyCronService: Failed to batch update key status: %v", err)
|
||||
// This error is for the transaction itself, not individual updates
|
||||
logrus.Errorf("KeyCronService: Transaction failed during batch update of key statuses: %v", err)
|
||||
}
|
||||
}
|
||||
|
@@ -17,6 +17,13 @@ type AddKeysResult struct {
|
||||
TotalInGroup int64 `json:"total_in_group"`
|
||||
}
|
||||
|
||||
// DeleteKeysResult holds the result of deleting multiple keys.
|
||||
type DeleteKeysResult struct {
|
||||
DeletedCount int `json:"deleted_count"`
|
||||
IgnoredCount int `json:"ignored_count"`
|
||||
TotalInGroup int64 `json:"total_in_group"`
|
||||
}
|
||||
|
||||
// KeyService provides services related to API keys.
|
||||
type KeyService struct {
|
||||
DB *gorm.DB
|
||||
@@ -30,7 +37,7 @@ func NewKeyService(db *gorm.DB) *KeyService {
|
||||
// AddMultipleKeys handles the business logic of creating new keys from a text block.
|
||||
func (s *KeyService) AddMultipleKeys(groupID uint, keysText string) (*AddKeysResult, error) {
|
||||
// 1. Parse keys from the text block
|
||||
keys := s.parseKeysFromText(keysText)
|
||||
keys := s.ParseKeysFromText(keysText)
|
||||
if len(keys) == 0 {
|
||||
return nil, fmt.Errorf("no valid keys found in the input text")
|
||||
}
|
||||
@@ -101,7 +108,9 @@ func (s *KeyService) AddMultipleKeys(groupID uint, keysText string) (*AddKeysRes
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *KeyService) parseKeysFromText(text string) []string {
|
||||
// ParseKeysFromText parses a string of keys from various formats into a string slice.
|
||||
// This function is exported to be shared with the handler layer.
|
||||
func (s *KeyService) ParseKeysFromText(text string) []string {
|
||||
var keys []string
|
||||
|
||||
// First, try to parse as a JSON array of strings
|
||||
@@ -162,45 +171,52 @@ func (s *KeyService) ClearAllInvalidKeys(groupID uint) (int64, error) {
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// DeleteSingleKey deletes a specific key from a group.
|
||||
func (s *KeyService) DeleteSingleKey(groupID, keyID uint) (int64, error) {
|
||||
result := s.DB.Where("group_id = ? AND id = ?", groupID, keyID).Delete(&models.APIKey{})
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
// ExportKeys returns a list of keys for a group, filtered by status.
|
||||
func (s *KeyService) ExportKeys(groupID uint, filter string) ([]string, error) {
|
||||
query := s.DB.Model(&models.APIKey{}).Where("group_id = ?", groupID)
|
||||
|
||||
switch filter {
|
||||
case "valid":
|
||||
query = query.Where("status = ?", "active")
|
||||
case "invalid":
|
||||
query = query.Where("status = ?", "inactive")
|
||||
case "all":
|
||||
// No status filter needed
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid filter value. Use 'all', 'valid', or 'invalid'")
|
||||
// DeleteMultipleKeys handles the business logic of deleting keys from a text block.
|
||||
func (s *KeyService) DeleteMultipleKeys(groupID uint, keysText string) (*DeleteKeysResult, error) {
|
||||
// 1. Parse keys from the text block
|
||||
keysToDelete := s.ParseKeysFromText(keysText)
|
||||
if len(keysToDelete) == 0 {
|
||||
return nil, fmt.Errorf("no valid keys found in the input text")
|
||||
}
|
||||
|
||||
var keys []string
|
||||
if err := query.Pluck("key_value", &keys).Error; err != nil {
|
||||
// 2. Perform the deletion
|
||||
// GORM's batch delete doesn't easily return which ones were deleted vs. ignored.
|
||||
// We perform a bulk delete and then count the remaining to calculate the result.
|
||||
result := s.DB.Where("group_id = ? AND key_value IN ?", groupID, keysToDelete).Delete(&models.APIKey{})
|
||||
if result.Error != nil {
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
deletedCount := int(result.RowsAffected)
|
||||
ignoredCount := len(keysToDelete) - deletedCount
|
||||
|
||||
// 3. Get the new total count
|
||||
var totalInGroup int64
|
||||
if err := s.DB.Model(&models.APIKey{}).Where("group_id = ?", groupID).Count(&totalInGroup).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return keys, nil
|
||||
|
||||
return &DeleteKeysResult{
|
||||
DeletedCount: deletedCount,
|
||||
IgnoredCount: ignoredCount,
|
||||
TotalInGroup: totalInGroup,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ListKeysInGroup lists all keys within a specific group, filtered by status.
|
||||
func (s *KeyService) ListKeysInGroup(groupID uint, statusFilter string) ([]models.APIKey, error) {
|
||||
var keys []models.APIKey
|
||||
query := s.DB.Where("group_id = ?", groupID)
|
||||
// ListKeysInGroupQuery builds a query to list all keys within a specific group, filtered by status.
|
||||
// It returns a GORM query builder, allowing the handler to apply pagination.
|
||||
func (s *KeyService) ListKeysInGroupQuery(groupID uint, statusFilter string, searchKeyword string) *gorm.DB {
|
||||
query := s.DB.Model(&models.APIKey{}).Where("group_id = ?", groupID)
|
||||
|
||||
if statusFilter != "" {
|
||||
query = query.Where("status = ?", statusFilter)
|
||||
}
|
||||
|
||||
if err := query.Find(&keys).Error; err != nil {
|
||||
return nil, err
|
||||
if searchKeyword != "" {
|
||||
// Use LIKE for fuzzy search on the key_value
|
||||
query = query.Where("key_value LIKE ?", "%"+searchKeyword+"%")
|
||||
}
|
||||
return keys, nil
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,13 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// KeyTestResult holds the validation result for a single key.
|
||||
type KeyTestResult struct {
|
||||
KeyValue string `json:"key_value"`
|
||||
IsValid bool `json:"is_valid"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// KeyValidatorService provides methods to validate API keys.
|
||||
type KeyValidatorService struct {
|
||||
DB *gorm.DB
|
||||
@@ -73,19 +80,45 @@ func (s *KeyValidatorService) ValidateSingleKey(ctx context.Context, key *models
|
||||
return isValid, nil
|
||||
}
|
||||
|
||||
// TestSingleKeyByID performs a synchronous validation test for a single API key by its ID.
|
||||
// It is intended for handling user-initiated "Test" actions.
|
||||
// It does not modify the key's state in the database.
|
||||
func (s *KeyValidatorService) TestSingleKeyByID(ctx context.Context, keyID uint) (bool, error) {
|
||||
var apiKey models.APIKey
|
||||
if err := s.DB.First(&apiKey, keyID).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to find api key with id %d: %w", keyID, err)
|
||||
// TestMultipleKeys performs a synchronous validation for a list of key values within a specific group.
|
||||
func (s *KeyValidatorService) TestMultipleKeys(ctx context.Context, group *models.Group, keyValues []string) ([]KeyTestResult, error) {
|
||||
results := make([]KeyTestResult, len(keyValues))
|
||||
ch, err := s.channelFactory.GetChannel(group)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get channel for group %s: %w", group.Name, err)
|
||||
}
|
||||
|
||||
var group models.Group
|
||||
if err := s.DB.First(&group, apiKey.GroupID).Error; err != nil {
|
||||
return false, fmt.Errorf("failed to find group with id %d: %w", apiKey.GroupID, err)
|
||||
// Find which of the provided keys actually exist in the database for this group
|
||||
var existingKeys []models.APIKey
|
||||
if err := s.DB.Where("group_id = ? AND key_value IN ?", group.ID, keyValues).Find(&existingKeys).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to query keys from DB: %w", err)
|
||||
}
|
||||
existingKeyMap := make(map[string]bool)
|
||||
for _, k := range existingKeys {
|
||||
existingKeyMap[k.KeyValue] = true
|
||||
}
|
||||
|
||||
return s.ValidateSingleKey(ctx, &apiKey, &group)
|
||||
for i, kv := range keyValues {
|
||||
// Pre-check: ensure the key belongs to the group to prevent unnecessary API calls
|
||||
if !existingKeyMap[kv] {
|
||||
results[i] = KeyTestResult{
|
||||
KeyValue: kv,
|
||||
IsValid: false,
|
||||
Error: "Key does not exist in this group or has been removed.",
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
isValid, validationErr := ch.ValidateKey(ctx, kv)
|
||||
results[i] = KeyTestResult{
|
||||
KeyValue: kv,
|
||||
IsValid: isValid,
|
||||
Error: "", // Explicitly set error to empty string on success
|
||||
}
|
||||
if validationErr != nil {
|
||||
results[i].Error = validationErr.Error()
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user