package commands import ( "context" "fmt" "os" "strings" "github.com/goravel/framework/contracts/console" "github.com/goravel/framework/contracts/console/command" "github.com/goravel/framework/facades" "goravel/app/utils" "goravel/app/utils/errorlog" ) type QueueClear struct { } // Signature The name and signature of the console command. func (r *QueueClear) Signature() string { return "queue:clear" } // Description The console command description. func (r *QueueClear) Description() string { return "清理队列中的任务(仅支持 Redis 驱动)" } // Extend The console command extend. func (r *QueueClear) Extend() command.Extend { return command.Extend{ Category: "queue", Flags: []command.Flag{ &command.StringFlag{ Name: "queue", Aliases: []string{"q"}, Usage: "队列名称(可选,默认清理默认队列)", }, &command.StringFlag{ Name: "connection", Aliases: []string{"c"}, Usage: "队列连接名称(可选,默认使用默认连接)", }, &command.BoolFlag{ Name: "force", Usage: "强制清理,不提示确认", }, }, } } // Handle Execute the console command. func (r *QueueClear) Handle(ctx console.Context) error { queueName := ctx.Option("queue") connectionName := ctx.Option("connection") // 布尔标志:检查命令行参数中是否包含 --force // 在 Goravel 框架中,BoolFlag 存在时 ctx.Option 返回空字符串,所以需要检查命令行参数 force := r.hasForceFlag() if connectionName == "" { connectionName = facades.Config().GetString("queue.default", "sync") } // 判断队列驱动类型 driver := facades.Config().GetString(fmt.Sprintf("queue.connections.%s.driver", connectionName), "") // 检查是否是 Redis 驱动 isRedis := r.isRedisDriver(connectionName) if !isRedis { if driver == "sync" { ctx.Info("同步驱动:任务立即执行,无需清理") return nil } if driver == "database" { ctx.Warning("数据库驱动暂不支持清理命令,请直接操作数据库 jobs 表") return nil } ctx.Warning(fmt.Sprintf("驱动 %s 暂不支持清理命令", driver)) return nil } // Redis 驱动:清理队列 defaultQueue := facades.Config().GetString(fmt.Sprintf("queue.connections.%s.queue", connectionName), "default") if queueName == "" { queueName = defaultQueue } // 获取 Redis 连接名称 redisConnectionName := r.getRedisConnectionName(connectionName) if redisConnectionName == "" { ctx.Warning("无法确定 Redis 连接名称") return nil } // 查询当前队列统计 stats, err := r.getRedisQueueStats(redisConnectionName, connectionName, queueName) if err != nil { ctx.Error(fmt.Sprintf("查询队列统计失败: %v", err)) return err } totalCount := stats.Pending + stats.Reserved + stats.Delayed if totalCount == 0 { ctx.Info(fmt.Sprintf("队列 '%s' 中没有任何任务", queueName)) return nil } // 显示当前统计 ctx.Info(fmt.Sprintf("队列 '%s' 当前状态:", queueName)) ctx.Info(fmt.Sprintf(" 待执行任务: %d", stats.Pending)) ctx.Info(fmt.Sprintf(" 正在执行任务: %d", stats.Reserved)) ctx.Info(fmt.Sprintf(" 延迟任务: %d", stats.Delayed)) ctx.Info(fmt.Sprintf(" 总计: %d", totalCount)) ctx.Info("") // 确认清理 if !force { ctx.Warning(fmt.Sprintf("警告:此操作将删除队列 '%s' 中的所有任务(共 %d 个)", queueName, totalCount)) ctx.Info("如果确定要继续,请使用 --force 参数") ctx.Info(fmt.Sprintf(" go run . artisan queue:clear --queue=%s --connection=%s --force", queueName, connectionName)) return nil } // 执行清理 ctx.Info("开始清理队列...") redisClient, err := utils.GetRedisClient(redisConnectionName) if err != nil { ctx.Error(fmt.Sprintf("获取 Redis 客户端失败: %v", err)) return err } // 注意:使用公共 Redis 客户端池,不需要手动关闭 ctxRedis := context.Background() clearedCount := int64(0) // 清理待执行队列 pendingKey := r.redisQueueKey(connectionName, queueName) pendingLen, _ := redisClient.LLen(ctxRedis, pendingKey).Result() if pendingLen > 0 { if err := redisClient.Del(ctxRedis, pendingKey).Err(); err != nil { ctx.Error(fmt.Sprintf("清理待执行队列失败: %v", err)) } else { clearedCount += pendingLen ctx.Info(fmt.Sprintf("已清理待执行队列: %d 个任务", pendingLen)) } } // 清理正在执行队列 reservedKey := r.redisReservedKey(connectionName, queueName) reservedLen, _ := redisClient.ZCard(ctxRedis, reservedKey).Result() if reservedLen > 0 { if err := redisClient.Del(ctxRedis, reservedKey).Err(); err != nil { ctx.Error(fmt.Sprintf("清理正在执行队列失败: %v", err)) } else { clearedCount += reservedLen ctx.Info(fmt.Sprintf("已清理正在执行队列: %d 个任务", reservedLen)) } } // 清理延迟队列 delayedKey := r.redisDelayedKey(connectionName, queueName) delayedLen, _ := redisClient.ZCard(ctxRedis, delayedKey).Result() if delayedLen > 0 { if err := redisClient.Del(ctxRedis, delayedKey).Err(); err != nil { ctx.Error(fmt.Sprintf("清理延迟队列失败: %v", err)) } else { clearedCount += delayedLen ctx.Info(fmt.Sprintf("已清理延迟队列: %d 个任务", delayedLen)) } } ctx.Info(fmt.Sprintf("队列清理完成!共清理 %d 个任务", clearedCount)) return nil } // isRedisDriver 判断是否是 Redis 驱动 func (r *QueueClear) isRedisDriver(connectionName string) bool { via := facades.Config().Get(fmt.Sprintf("queue.connections.%s.via", connectionName)) return via != nil || strings.Contains(connectionName, "redis") } // getRedisConnectionName 从队列连接配置中获取 Redis 连接名称 func (r *QueueClear) getRedisConnectionName(queueConnectionName string) string { connection := facades.Config().GetString(fmt.Sprintf("queue.connections.%s.connection", queueConnectionName), "default") if strings.Contains(queueConnectionName, "redis") { redisHost := facades.Config().GetString(fmt.Sprintf("database.redis.%s.host", queueConnectionName), "") if redisHost != "" { return queueConnectionName } } return connection } // getRedisQueueStats 获取 Redis 队列统计信息 func (r *QueueClear) getRedisQueueStats(redisConnectionName, queueConnectionName, queueName string) (*RedisQueueStatsInfo, error) { redisClient, err := utils.GetRedisClient(redisConnectionName) if err != nil { errorlog.Record(context.Background(), "queue", "获取 Redis 客户端失败", map[string]any{ "connection": redisConnectionName, "error": err.Error(), }, "获取 Redis 客户端失败: %v", err) return nil, fmt.Errorf("获取 Redis 客户端失败: %v", err) } // 注意:使用公共 Redis 客户端池,不需要手动关闭 ctx := context.Background() stats := &RedisQueueStatsInfo{} pendingKey := r.redisQueueKey(queueConnectionName, queueName) pendingLen, err := redisClient.LLen(ctx, pendingKey).Result() if err != nil { errorlog.Record(context.Background(), "queue", "查询待执行队列失败", map[string]any{ "queue_name": queueName, "key": pendingKey, "error": err.Error(), }, "查询待执行队列失败: %v", err) return nil, fmt.Errorf("查询待执行队列失败: %v", err) } stats.Pending = pendingLen reservedKey := r.redisReservedKey(queueConnectionName, queueName) reservedLen, err := redisClient.ZCard(ctx, reservedKey).Result() if err != nil { errorlog.Record(context.Background(), "queue", "查询正在执行队列失败", map[string]any{ "queue_name": queueName, "key": reservedKey, "error": err.Error(), }, "查询正在执行队列失败: %v", err) return nil, fmt.Errorf("查询正在执行队列失败: %v", err) } stats.Reserved = reservedLen delayedKey := r.redisDelayedKey(queueConnectionName, queueName) delayedLen, err := redisClient.ZCard(ctx, delayedKey).Result() if err != nil { errorlog.Record(context.Background(), "queue", "查询延迟队列失败", map[string]any{ "queue_name": queueName, "key": delayedKey, "error": err.Error(), }, "查询延迟队列失败: %v", err) return nil, fmt.Errorf("查询延迟队列失败: %v", err) } stats.Delayed = delayedLen // 失败任务:从数据库 failed_jobs 表查询 var failedCount int64 if queueName != "" { failedCount, err = facades.Orm().Query().Table("failed_jobs"). Where("queue = ?", queueName). Count() } else { failedCount, err = facades.Orm().Query().Table("failed_jobs").Count() } if err != nil { stats.Failed = 0 } else { stats.Failed = failedCount } stats.Total = stats.Pending + stats.Reserved return stats, nil } // redisQueueKey Goravel Redis queue key format: // {appName}_queues:{queueConnection}_{queue} func (r *QueueClear) redisQueueKey(queueConnectionName, queueName string) string { appName := facades.Config().GetString("app.name", "goravel") return fmt.Sprintf("%s_queues:%s_%s", appName, queueConnectionName, queueName) } func (r *QueueClear) redisReservedKey(queueConnectionName, queueName string) string { return fmt.Sprintf("%s:reserved", r.redisQueueKey(queueConnectionName, queueName)) } func (r *QueueClear) redisDelayedKey(queueConnectionName, queueName string) string { return fmt.Sprintf("%s:delayed", r.redisQueueKey(queueConnectionName, queueName)) } // hasForceFlag 检查命令行参数中是否包含 --force 标志 func (r *QueueClear) hasForceFlag() bool { for _, arg := range os.Args { if arg == "--force" || arg == "-force" { return true } } return false }