feat: key接口完善及错误处理

This commit is contained in:
tbphp
2025-07-05 14:50:58 +08:00
parent 8d7b60875e
commit d64ada4181
12 changed files with 487 additions and 208 deletions

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}