【无标题】temp

这个问题非常典型,涉及到Kafka消费者组的offset管理机制。让我详细分析原因并提供解决方案。

问题分析

  1. 不设置GroupID时能消费所有消息的原因

当你不设置GroupID时,Kafka消费者会以"匿名消费者"的方式运行:

· 每次启动都会从topic的最开始(或最新)重新消费所有消息

· 没有offset提交机制,所以每次都是全新的消费会话

· 适合一次性消费或测试场景

  1. 设置GroupID后无法消费消息的原因

这是Kafka消费者组offset管理的典型问题,可能的原因:

最常见的原因:消费者无法提交offset

· Kafka需要消费者定期提交offset到__consumer_offsets topic

· 如果消费者没有正确提交offset,Kafka会认为消息没有被成功处理

· 消费者会不断重试相同的消息,或者卡住

其他可能原因:

  1. Group协调问题:消费者无法加入消费者组
  2. Offset提交失败:网络问题或权限问题导致offset提交失败
  3. AutoOffsetReset配置:没有设置合适的offset重置策略
  4. SessionTimeout:消费者被认为已死亡,触发rebalance

解决方案

  1. 更新消费者配置 (pkg/kafka/consumer.go)
go 复制代码
package kafka

import (
	"blog-project/config"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/segmentio/kafka-go"
)

// InitKafkaConsumer 初始化Kafka消费者
func InitKafkaConsumer(cfg *config.Config) {
	InitKafka(cfg)

	if !IsEnabled() {
		log.Println("Kafka consumer is disabled")
		return
	}

	log.Printf("Initializing Kafka consumer with brokers: %v, topic: %s, group: %s",
		kafkaCfg.Brokers, kafkaCfg.Topic, kafkaCfg.GroupID)

	// 使用更详细的配置
	consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:        kafkaCfg.Brokers,
		Topic:          kafkaCfg.Topic,
		GroupID:        kafkaCfg.GroupID,
		MinBytes:       10e3, // 10KB
		MaxBytes:       10e6, // 10MB
		CommitInterval: 5 * time.Second, // 定期提交offset
		StartOffset:    kafka.FirstOffset, // 首次启动时从最早的消息开始
		// 或者使用 kafka.LastOffset 从最新消息开始
		
		// 重要的会话和心跳配置
		SessionTimeout:  30 * time.Second,
		HeartbeatInterval: 6 * time.Second,
		MaxWait:         10 * time.Second, // 读取消息的最大等待时间
		ReadLagInterval: -1, // 禁用lag检查,避免额外开销
	})

	log.Println("Kafka consumer initialized successfully")

	// 启动消费者协程
	go consumeMessages()
}

func consumeMessages() {
	if consumer == nil {
		log.Println("Consumer is nil, cannot start consuming")
		return
	}

	defer func() {
		if r := recover(); r != nil {
			log.Printf("Recovered from panic in consumer: %v", r)
		}
	}()

	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	log.Println("Starting Kafka consumer loop...")

	// 手动提交offset的模式
	for {
		select {
		case <-sigchan:
			log.Println("Shutting down Kafka consumer")
			closeConsumer()
			return
		default:
			// 读取消息(这里会阻塞直到有消息或超时)
			msg, err := consumer.ReadMessage(context.Background())
			if err != nil {
				log.Printf("Error reading message from Kafka: %v", err)
				handleConsumerError(err)
				continue
			}

			log.Printf("Received message: topic=%s, partition=%d, offset=%d, key=%s",
				msg.Topic, msg.Partition, msg.Offset, string(msg.Key))

			// 处理消息
			if err := processMessage(msg.Value); err != nil {
				log.Printf("Failed to process message at offset %d: %v", msg.Offset, err)
				// 处理失败,可以选择重试或者跳过
				continue
			}

			// 手动提交offset(确保消息处理成功后再提交)
			if err := consumer.CommitMessages(context.Background(), msg); err != nil {
				log.Printf("Failed to commit offset: %v", err)
			} else {
				log.Printf("Successfully committed offset: partition=%d, offset=%d", 
					msg.Partition, msg.Offset)
			}
		}
	}
}

func handleConsumerError(err error) {
	// 根据错误类型决定处理策略
	switch err {
	case context.Canceled:
		log.Println("Consumer context canceled")
	default:
		log.Printf("Consumer error: %v, retrying in 5 seconds", err)
		time.Sleep(5 * time.Second)
	}
}

func processMessage(message []byte) error {
	log.Printf("Raw message content: %s", string(message))

	var event PublishBlogEvent
	if err := json.Unmarshal(message, &event); err != nil {
		return fmt.Errorf("failed to unmarshal Kafka message: %v", err)
	}

	log.Printf("Successfully parsed event: blogID=%d, title=%s, user=%s",
		event.BlogID, event.Title, event.UserName)

	// 发送邮件通知
	if err := sendEmailNotification(&event); err != nil {
		return fmt.Errorf("failed to send email notification: %v", err)
	}

	log.Printf("Email notification processed successfully for blogID=%d", event.BlogID)
	return nil
}

func sendEmailNotification(event *PublishBlogEvent) error {
	emailContent := fmt.Sprintf(`
尊敬的 %s,

您的博客《%s》已经成功发布!

博客ID: %d
发布时间: %s

感谢您使用我们的博客服务!

Best regards,
博客团队
	`, event.UserName, event.Title, event.BlogID, time.Now().Format("2006-01-02 15:04:05"))

	// 添加详细的日志输出
	log.Printf("=== 模拟邮件发送开始 ===")
	log.Printf("收件人: 系统管理员")
	log.Printf("主题: 博客发布通知 - %s", event.Title)
	log.Printf("内容: %s", emailContent)
	log.Printf("=== 模拟邮件发送结束 ===")

	// 模拟邮件发送延迟
	time.Sleep(100 * time.Millisecond)

	log.Printf("邮件通知完成: 用户=%s, 博客ID=%d", event.UserName, event.BlogID)
	return nil
}
  1. 添加调试工具函数 (pkg/kafka/debug.go)
go 复制代码
package kafka

import (
	"context"
	"fmt"
	"log"

	"github.com/segmentio/kafka-go"
)

// DebugConsumerGroup 调试消费者组状态
func DebugConsumerGroup() {
	if !IsEnabled() || consumer == nil {
		log.Println("Kafka is not enabled or consumer not initialized")
		return
	}

	// 连接到Kafka获取消费者组信息
	conn, err := kafka.Dial("tcp", kafkaCfg.Brokers[0])
	if err != nil {
		log.Printf("Failed to connect for debugging: %v", err)
		return
	}
	defer conn.Close()

	// 获取消费者组offset信息
	offsetFetchRequest := kafka.OffsetFetchRequest{
		GroupID: kafkaCfg.GroupID,
		Topics:  make(map[string][]int),
	}

	// 获取topic的所有partition
	partitions, err := conn.ReadPartitions(kafkaCfg.Topic)
	if err != nil {
		log.Printf("Failed to get partitions: %v", err)
		return
	}

	for _, partition := range partitions {
		if partition.Topic == kafkaCfg.Topic {
			offsetFetchRequest.Topics[partition.Topic] = append(
				offsetFetchRequest.Topics[partition.Topic], partition.ID)
		}
	}

	// 发送offset获取请求
	offsetFetchResponse, err := conn.OffsetFetch(&offsetFetchRequest)
	if err != nil {
		log.Printf("Failed to fetch offsets: %v", err)
		return
	}

	log.Printf("=== Consumer Group Debug Info ===")
	log.Printf("GroupID: %s", kafkaCfg.GroupID)
	log.Printf("Topic: %s", kafkaCfg.Topic)

	for topic, partitions := range offsetFetchResponse.Topics {
		for partition, offset := range partitions {
			log.Printf("Partition %d: Offset=%d, Metadata='%s', Error=%v",
				partition, offset.Offset, offset.Metadata, offset.Error)
		}
	}
	log.Printf("=== End Debug Info ===")
}

// ResetConsumerGroup 重置消费者组offset(谨慎使用!)
func ResetConsumerGroup(offset int64) error {
	if !IsEnabled() {
		return fmt.Errorf("kafka is disabled")
	}

	conn, err := kafka.Dial("tcp", kafkaCfg.Brokers[0])
	if err != nil {
		return fmt.Errorf("failed to connect: %v", err)
	}
	defer conn.Close()

	// 获取所有partition
	partitions, err := conn.ReadPartitions(kafkaCfg.Topic)
	if err != nil {
		return fmt.Errorf("failed to get partitions: %v", err)
	}

	for _, partition := range partitions {
		if partition.Topic == kafkaCfg.Topic {
			// 设置offset
			if err := conn.SetOffset(kafkaCfg.GroupID, partition.Topic, partition.ID, offset); err != nil {
				log.Printf("Failed to set offset for partition %d: %v", partition.ID, err)
			} else {
				log.Printf("Reset offset for partition %d to %d", partition.ID, offset)
			}
		}
	}

	return nil
}
  1. 更新主程序添加调试 (main.go)
go 复制代码
// 在main函数中添加调试
func main() {
	// ... 其他初始化代码

	// 初始化Kafka消费者
	kafkapkg.InitKafkaConsumer(cfg)

	// 添加消费者组调试信息
	if cfg.KafkaEnabled {
		// 等待一下让消费者初始化完成
		time.Sleep(2 * time.Second)
		kafkapkg.DebugConsumerGroup()
		
		// 如果需要重置offset(谨慎使用!)
		// kafkapkg.ResetConsumerGroup(kafka.LastOffset)
	}

	// ... 其他代码
}
  1. 环境变量配置建议
bash 复制代码
# Kafka配置
KAFKA_BROKERS=localhost:9092
KAFKA_TOPIC=publish-blog-events
KAFKA_GROUP_ID=blog-service-prod-group  # 使用有意义的group id
KAFKA_ENABLED=true

诊断步骤

  1. 检查Kafka消费者组状态:

    bash 复制代码
    # 查看消费者组列表
    kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
    
    # 查看特定消费者组详情
    kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group blog-service-group --describe
  2. 检查topic消息:

    bash 复制代码
    # 查看topic中的消息
    kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic publish-blog-events --from-beginning
  3. 检查offset提交:

    bash 复制代码
    # 查看__consumer_offsets topic
    kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic __consumer_offsets --formatter "kafka.coordinator.group.GroupMetadataManager$OffsetsMessageFormatter"

关键改进点

  1. 明确的offset提交策略:使用手动提交确保消息处理成功后再提交offset
  2. 详细的错误处理:区分不同类型的错误并采取相应措施
  3. 会话超时配置:避免消费者被错误认为已死亡
  4. 调试工具:添加了消费者组状态查看和offset重置功能

现在运行你的应用,应该能看到详细的日志输出,包括邮件内容。如果还有问题,使用提供的调试工具来检查消费者组状态。

这个问题非常典型,涉及到Kafka消费者组的机制。让我为你详细解释原因和解决方案。

问题原因分析

  1. 不设置group.id时能消费消息

· 当你不设置group.id时,Kafka消费者以"匿名消费者"模式运行

· 每次启动都会从topic的开始位置(最早的消息)读取所有消息

· 没有偏移量(offset)提交机制,所以每次重启都重新消费所有消息

  1. 设置group.id后无法消费消息

这通常由以下几个原因导致:

最常见的原因:消费者组协调问题

· Kafka使用消费者组来协调多个消费者实例

· 如果消费者组处于"重平衡"(rebalance)状态,消费者可能无法正常分配分区

· 网络问题或配置不当导致消费者无法加入消费者组

其他可能原因:

· 自动偏移提交失败:消费者无法提交消费偏移量

· 分区分配问题:消费者没有分配到任何分区

· 消费偏移量问题:可能偏移量被重置或设置不当

解决方案

  1. 修改消费者配置 (pkg/kafka/consumer.go)
go 复制代码
package kafka

import (
	"blog-project/config"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/segmentio/kafka-go"
)

// InitKafkaConsumer 初始化Kafka消费者
func InitKafkaConsumer(cfg *config.Config) {
	InitKafka(cfg)

	if !IsEnabled() {
		log.Println("Kafka consumer is disabled")
		return
	}

	// 确保有有效的groupID,如果没有则生成一个唯一的
	groupID := kafkaCfg.GroupID
	if groupID == "" {
		hostname, _ := os.Hostname()
		groupID = fmt.Sprintf("blog-consumer-%s-%d", hostname, time.Now().Unix())
		log.Printf("No group ID specified, using auto-generated: %s", groupID)
	}

	log.Printf("Initializing Kafka consumer with brokers: %v, topic: %s, group: %s",
		kafkaCfg.Brokers, kafkaCfg.Topic, groupID)

	consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:        kafkaCfg.Brokers,
		Topic:          kafkaCfg.Topic,
		GroupID:        groupID,
		MinBytes:       10e3,
		MaxBytes:       10e6,
		CommitInterval: time.Second, // 自动提交间隔
		StartOffset:    kafka.FirstOffset, // 从最早的消息开始消费
	})

	log.Println("Kafka consumer initialized successfully")

	// 检查消费者组状态
	go checkConsumerGroupStatus()

	// 启动消费者协程
	go consumeMessages()
}

func checkConsumerGroupStatus() {
	time.Sleep(2 * time.Second) // 等待消费者初始化
	
	// 创建一个临时消费者来检查消费者组状态
	if len(kafkaCfg.Brokers) == 0 {
		return
	}

	conn, err := kafka.Dial("tcp", kafkaCfg.Brokers[0])
	if err != nil {
		log.Printf("Failed to connect to Kafka for group status check: %v", err)
		return
	}
	defer conn.Close()

	// 获取消费者组信息
	group, err := kafka.NewConsumerGroup(conn, kafka.ConsumerGroupConfig{
		ID:      kafkaCfg.GroupID,
		Topics:  []string{kafkaCfg.Topic},
		Brokers: kafkaCfg.Brokers,
	})
	if err != nil {
		log.Printf("Failed to create consumer group instance: %v", err)
		return
	}

	// 获取消费者组状态
	state, err := group.State()
	if err != nil {
		log.Printf("Failed to get consumer group state: %v", err)
		return
	}

	log.Printf("Consumer group state: %+v", state)
}

func consumeMessages() {
	if consumer == nil {
		log.Println("Consumer is nil, cannot start consuming")
		return
	}

	defer func() {
		if r := recover(); r != nil {
			log.Printf("Recovered from panic in consumer: %v", r)
		}
	}()

	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	log.Println("Starting Kafka consumer loop...")

	// 先尝试读取一条消息来测试连接
	if err := testConsumerConnection(); err != nil {
		log.Printf("Consumer connection test failed: %v", err)
		return
	}

	for {
		select {
		case <-sigchan:
			log.Println("Shutting down Kafka consumer")
			closeConsumer()
			return
		default:
			// 使用带较短超时的context
			ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
			msg, err := consumer.ReadMessage(ctx)
			cancel()

			if err != nil {
				if err == context.DeadlineExceeded {
					// 超时是正常的,继续等待
					continue
				}
				log.Printf("Error reading message from Kafka: %v", err)
				if shouldRetry(err) {
					time.Sleep(2 * time.Second)
					continue
				} else {
					log.Printf("Fatal error, stopping consumer: %v", err)
					return
				}
			}

			log.Printf("Received message from partition %d at offset %d: %s", 
				msg.Partition, msg.Offset, string(msg.Value))

			// 处理消息
			processMessage(msg.Value)

			// 手动提交偏移量以确保消息被确认
			if err := consumer.CommitMessages(context.Background(), msg); err != nil {
				log.Printf("Failed to commit offset: %v", err)
			}
		}
	}
}

func testConsumerConnection() error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// 尝试获取topic的partitions来测试连接
	partitions, err := consumer.Partitions()
	if err != nil {
		return fmt.Errorf("failed to get partitions: %v", err)
	}

	log.Printf("Available partitions: %v", partitions)

	// 尝试读取一条消息(不阻塞)
	consumer.SetOffsetAt(context.Background(), time.Now())
	msg, err := consumer.ReadMessage(ctx)
	if err != nil && err != context.DeadlineExceeded {
		return fmt.Errorf("test read failed: %v", err)
	}

	if msg != nil {
		log.Printf("Test message received successfully")
		// 重置偏移量到正确位置
		consumer.SetOffset(kafka.FirstOffset)
	}

	return nil
}

// 其他函数保持不变...
  1. 添加调试工具函数 (pkg/kafka/debug.go)
go 复制代码
package kafka

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/segmentio/kafka-go"
)

// DebugConsumerGroup 调试消费者组状态
func DebugConsumerGroup() {
	if !IsEnabled() || kafkaCfg.GroupID == "" {
		return
	}

	log.Printf("=== KAFKA CONSUMER GROUP DEBUG ===")
	
	for _, broker := range kafkaCfg.Brokers {
		conn, err := kafka.Dial("tcp", broker)
		if err != nil {
			log.Printf("Failed to connect to broker %s: %v", broker, err)
			continue
		}
		defer conn.Close()

		// 获取消费者组偏移量
		offsets, err := conn.ReadOffsetRequests(kafka.OffsetRequest{
			Topics: map[string][]kafka.OffsetAsk{
				kafkaCfg.Topic: {{
					Partition: 0,
					Time:      -1, // 最新偏移量
				}},
			},
		})
		if err != nil {
			log.Printf("Failed to read offsets: %v", err)
			continue
		}

		log.Printf("Broker %s offsets: %+v", broker, offsets)
	}

	// 检查消费者组状态
	checkConsumerGroupState()
}

func checkConsumerGroupState() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// 使用kafka-go的消费者组API
	group, err := kafka.NewConsumerGroup(kafka.ConsumerGroupConfig{
		ID:      kafkaCfg.GroupID,
		Topics:  []string{kafkaCfg.Topic},
		Brokers: kafkaCfg.Brokers,
	})
	if err != nil {
		log.Printf("Failed to create consumer group for debugging: %v", err)
		return
	}
	defer group.Close()

	state, err := group.State(ctx)
	if err != nil {
		log.Printf("Failed to get consumer group state: %v", err)
		return
	}

	log.Printf("Consumer group '%s' state: Members=%d, Generation=%d", 
		kafkaCfg.GroupID, len(state.Members), state.GenerationID)
}
  1. 更新主程序添加调试 (main.go)
go 复制代码
// 在main函数中添加调试信息
func main() {
	// ... 其他初始化代码

	// 初始化Kafka消费者
	kafkapkg.InitKafkaConsumer(cfg)

	// 添加消费者组调试信息
	if cfg.KafkaEnabled && cfg.KafkaGroupID != "" {
		time.Sleep(3 * time.Second) // 等待消费者初始化
		kafkapkg.DebugConsumerGroup()
	}

	// ... 其他代码
}
  1. 环境变量配置建议
bash 复制代码
# 使用有意义的groupID
KAFKA_GROUP_ID=blog-service-production

# 或者让系统自动生成(每次启动都是新的消费者组)
# KAFKA_GROUP_ID=""

# 确保brokers配置正确
KAFKA_BROKERS=localhost:9092
KAFKA_TOPIC=publish-blog-events
KAFKA_ENABLED=true

临时解决方案

如果你只是想测试功能,可以:

  1. 每次使用新的groupID:在环境变量中不设置KAFKA_GROUP_ID,让系统自动生成

  2. 手动重置偏移量:使用kafka工具重置消费者组偏移量

    bash 复制代码
    kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
      --group your-group-id --topic publish-blog-events --reset-offsets --to-earliest --execute

根本解决方案

  1. 检查Kafka服务器状态:确保Kafka集群健康
  2. 验证网络连接:确保应用能访问Kafka的9092端口
  3. 检查ACL权限:如果Kafka有安全设置,确保消费者有读写权限
  4. 监控消费者组状态:使用Kafka工具监控消费者组状态

现在运行代码,你应该能看到更详细的调试信息,帮助诊断为什么设置了groupID后无法消费消息。

既然测试代码成功但主程序不行,这说明问题很可能是消费者组偏移量(offset)的问题。让我给你最合理的解决方案。

🎯 问题根源

当你的测试代码使用新的GroupID时能成功,但主程序不行,这是因为:

  1. 主程序使用的GroupID已经提交了偏移量
  2. Kafka认为这个GroupID已经消费过消息,所以不会重新发送
  3. 新的消息可能没有被正确生产或者消费者配置仍有问题

🔧 立即解决方案

  1. 修改主程序中的GroupID(最简单有效)

在环境变量或配置中修改GroupID:

bash 复制代码
# 修改环境变量,使用新的GroupID
set KAFKA_GROUP_ID=blog-service-group-v2

或者修改config.go的默认值:

go 复制代码
KafkaGroupID:  getEnv("KAFKA_GROUP_ID", "blog-service-group-v2"),
  1. 添加强制重置偏移量的功能

在 pkg/kafka/consumer.go 中添加:

go 复制代码
func InitKafkaConsumer(cfg *config.Config) {
	InitKafka(cfg)

	if !IsEnabled() {
		log.Println("Kafka consumer is disabled")
		return
	}

	log.Printf("Initializing Kafka consumer with brokers: %v, topic: %s, group: %s",
		kafkaCfg.Brokers, kafkaCfg.Topic, kafkaCfg.GroupID)

	// 先检查并重置消费者组偏移量(如果需要)
	if err := resetConsumerOffsetIfNeeded(); err != nil {
		log.Printf("Warning: Could not reset consumer offset: %v", err)
	}

	consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:        kafkaCfg.Brokers,
		Topic:          kafkaCfg.Topic,
		GroupID:        kafkaCfg.GroupID,
		MinBytes:       1,
		MaxBytes:       10e6,
		StartOffset:    kafka.FirstOffset, // 确保从最早开始
		MaxWait:        3 * time.Second,
		CommitInterval: 0,
	})

	log.Println("Kafka consumer initialized successfully")
	go consumeMessages()
}

func resetConsumerOffsetIfNeeded() error {
	// 连接到Kafka
	conn, err := kafka.Dial("tcp", kafkaCfg.Brokers[0])
	if err != nil {
		return err
	}
	defer conn.Close()

	// 获取消费者组信息
	group, err := kafka.NewConsumerGroup(conn, kafkaCfg.GroupID, []string{kafkaCfg.Topic})
	if err != nil {
		return err
	}

	// 尝试重置偏移量到最早
	offset, err := group.Offset(kafkaCfg.Topic, 0, kafka.FirstOffset)
	if err != nil {
		return err
	}

	log.Printf("Consumer group '%s' offset reset to: %d", kafkaCfg.GroupID, offset)
	return nil
}
  1. 添加详细的调试信息

修改消费者循环,添加更多调试信息:

go 复制代码
func consumeMessages() {
	if consumer == nil {
		log.Println("Consumer is nil, cannot start consuming")
		return
	}

	log.Printf("🚀 Starting consumer for group: %s", kafkaCfg.GroupID)
	log.Printf("📡 Connected to brokers: %v", kafkaCfg.Brokers)
	log.Printf("📝 Subscribed to topic: %s", kafkaCfg.Topic)

	// 获取并打印当前偏移量信息
	if err := printCurrentOffsets(); err != nil {
		log.Printf("Warning: Could not get offset info: %v", err)
	}

	for {
		log.Printf("⏳ Waiting for messages from Kafka... (Group: %s)", kafkaCfg.GroupID)
		
		msg, err := consumer.ReadMessage(context.Background())
		if err != nil {
			log.Printf("❌ Read error: %v", err)
			time.Sleep(2 * time.Second)
			continue
		}

		log.Printf("✅ MESSAGE RECEIVED!")
		log.Printf("   Topic: %s, Partition: %d, Offset: %d", 
			msg.Topic, msg.Partition, msg.Offset)
		log.Printf("   Key: %s", string(msg.Key))
		log.Printf("   Value: %s", string(msg.Value))

		processMessage(msg.Value)
	}
}

func printCurrentOffsets() error {
	conn, err := kafka.Dial("tcp", kafkaCfg.Brokers[0])
	if err != nil {
		return err
	}
	defer conn.Close()

	// 获取topic的partitions
	partitions, err := conn.ReadPartitions(kafkaCfg.Topic)
	if err != nil {
		return err
	}

	for _, partition := range partitions {
		offset, err := consumer.Offset(partition.ID)
		if err == nil {
			log.Printf("Partition %d - Current Offset: %d", partition.ID, offset)
		}
	}

	return nil
}
  1. 创建诊断脚本

创建 diagnose_kafka.go:

go 复制代码
package main

import (
	"blog-project/config"
	kafkapkg "blog-project/pkg/kafka"
	"context"
	"fmt"
	"log"
	"time"

	"github.com/segmentio/kafka-go"
)

func main() {
	log.Println("🔍 Running Kafka diagnostics...")
	
	cfg := &config.Config{
		KafkaEnabled:  true,
		KafkaBrokers:  "localhost:9092",
		KafkaTopic:    "publish-blog-events",
		KafkaGroupID:  "diagnostic-group", // 使用新的GroupID
	}
	
	// 1. 测试连接
	log.Println("1. Testing connection...")
	conn, err := kafka.Dial("tcp", "localhost:9092")
	if err != nil {
		log.Fatalf("Connection failed: %v", err)
	}
	defer conn.Close()
	log.Println("✅ Connection successful")

	// 2. 检查topic
	log.Println("2. Checking topic...")
	partitions, err := conn.ReadPartitions("publish-blog-events")
	if err != nil {
		log.Fatalf("Topic check failed: %v", err)
	}
	log.Printf("✅ Topic exists with %d partitions", len(partitions))

	// 3. 检查是否有消息
	log.Println("3. Checking for messages...")
	reader := kafka.NewReader(kafka.ReaderConfig{
		Brokers:   []string{"localhost:9092"},
		Topic:     "publish-blog-events",
		Partition: 0,
		MinBytes:  1,
		MaxBytes:  10e6,
	})
	defer reader.Close()

	// 尝试读取一条消息
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	
	msg, err := reader.ReadMessage(ctx)
	if err != nil {
		log.Printf("❌ No messages found: %v", err)
	} else {
		log.Printf("✅ Message found: %s", string(msg.Value))
	}

	// 4. 测试消费者组功能
	log.Println("4. Testing consumer group functionality...")
	groupReader := kafka.NewReader(kafka.ReaderConfig{
		Brokers:     []string{"localhost:9092"},
		Topic:       "publish-blog-events",
		GroupID:     "test-group-diagnostic",
		StartOffset: kafka.FirstOffset,
		MinBytes:    1,
		MaxBytes:    10e6,
	})
	defer groupReader.Close()

	// 给一些时间让消费者加入组
	time.Sleep(3 * time.Second)

	log.Println("✅ Diagnostic completed")
}

📋 执行步骤

  1. 首先运行诊断脚本:

    bash 复制代码
    go run diagnose_kafka.go
  2. 修改GroupID并重启主程序:

    bash 复制代码
    set KAFKA_GROUP_ID=blog-service-group-v2
    go run main.go
  3. 如果还不行,手动重置偏移量:

    bash 复制代码
    cd C:\kafka_2.13-3.4.0
    bin\windows\kafka-consumer-groups.bat --bootstrap-server localhost:9092 --group blog-service-group --reset-offsets --to-earliest --topic publish-blog-events --execute

💡 最终解决方案

如果上述方法都不行,最简单有效的解决方案是:

  1. 删除并重新创建Topic:

    bash 复制代码
    bin\windows\kafka-topics.bat --delete --topic publish-blog-events --bootstrap-server localhost:9092
    bin\windows\kafka-topics.bat --create --topic publish-blog-events --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
  2. 使用全新的GroupID

  3. 重启你的Go程序

这样就能确保从干净的状态开始,避免所有偏移量和消费者组的问题。

太好了!手动消费成功说明Kafka服务器和Topic都是正常的。现在问题很明确了:是你的Go消费者配置有问题。

🎯 最合理的解决方案

既然手动消费能成功,说明Kafka基础设施没问题。问题出在Go消费者的配置上。以下是应该采取的步骤:

  1. 首先停止当前运行的Go程序

确保你的消费者不会干扰测试。

  1. 使用正确的消费者配置

更新你的 pkg/kafka/consumer.go:

go 复制代码
func InitKafkaConsumer(cfg *config.Config) {
	InitKafka(cfg)

	if !IsEnabled() {
		log.Println("Kafka consumer is disabled")
		return
	}

	log.Printf("Initializing Kafka consumer with brokers: %v, topic: %s, group: %s",
		kafkaCfg.Brokers, kafkaCfg.Topic, kafkaCfg.GroupID)

	// 关键修改:使用与命令行相同的配置
	consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:        kafkaCfg.Brokers,
		Topic:          kafkaCfg.Topic,
		GroupID:        kafkaCfg.GroupID,
		MinBytes:       1,                    // 最小字节数,设为1更容易触发
		MaxBytes:       10e6,                 // 10MB
		StartOffset:    kafka.FirstOffset,    // 从最早的消息开始(与--from-beginning等效)
		MaxWait:        3 * time.Second,      // 最大等待时间
		CommitInterval: 0,                    // 禁用自动提交,手动控制
	})

	log.Println("Kafka consumer initialized successfully")

	// 启动消费者协程
	go consumeMessages()
}
  1. 修改消费者读取逻辑
go 复制代码
func consumeMessages() {
	if consumer == nil {
		log.Println("Consumer is nil, cannot start consuming")
		return
	}

	defer func() {
		if r := recover(); r != nil {
			log.Printf("Recovered from panic in consumer: %v", r)
		}
	}()

	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	log.Println("Starting Kafka consumer loop...")

	for {
		select {
		case <-sigchan:
			log.Println("Shutting down Kafka consumer")
			closeConsumer()
			return
		default:
			// 使用与命令行相同的配置:无超时读取
			log.Printf("Reading message from Kafka (no timeout)...")
			
			// 关键修改:移除超时,使用无限等待
			msg, err := consumer.ReadMessage(context.Background())
			
			if err != nil {
				log.Printf("Error reading message from Kafka: %v", err)
				
				// 如果是连接错误,等待后重试
				if isConnectionError(err) {
					log.Printf("Connection error, waiting 5 seconds before retry...")
					time.Sleep(5 * time.Second)
				}
				continue
			}

			// 成功收到消息!
			log.Printf("✅ SUCCESS: Received message from Kafka!")
			log.Printf("Topic: %s, Partition: %d, Offset: %d, Key: %s", 
				msg.Topic, msg.Partition, msg.Offset, string(msg.Key))
			log.Printf("Message value: %s", string(msg.Value))

			// 处理消息
			processMessage(msg.Value)
		}
	}
}
  1. 测试消费者配置

创建一个简单的测试程序来验证消费者配置:

go 复制代码
// 创建 test_consumer.go 进行独立测试
package main

import (
	"blog-project/config"
	kafkapkg "blog-project/pkg/kafka"
	"context"
	"log"
	"time"
)

func main() {
	log.Println("Testing Kafka consumer...")
	
	cfg := &config.Config{
		KafkaEnabled:  true,
		KafkaBrokers:  "localhost:9092",
		KafkaTopic:    "publish-blog-events",
		KafkaGroupID:  "test-consumer-group",
	}
	
	// 初始化Kafka
	kafkapkg.InitKafka(cfg)
	
	// 测试连接
	if success := kafkapkg.CheckKafkaConnection(cfg); !success {
		log.Fatal("Kafka connection test failed")
	}
	
	log.Println("Kafka connection test passed!")
	
	// 等待一段时间让消费者运行
	log.Println("Consumer test running for 30 seconds...")
	time.Sleep(30 * time.Second)
	log.Println("Test completed")
}
  1. 验证步骤

按照这个顺序进行验证:

  1. 首先确保Kafka运行正常(你已经做到了)

  2. 运行独立消费者测试:

    bash 复制代码
    go run test_consumer.go

    观察是否能收到消息

  3. 如果测试成功,重启你的主程序:

    bash 复制代码
    go run main.go
  4. 发布一篇博客测试,观察消费者日志

  5. 如果还有问题,添加调试信息

在消费者中添加更详细的调试:

go 复制代码
func consumeMessages() {
	// ... 前面的代码不变
	
	// 添加消费者组信息日志
	log.Printf("Consumer Group: %s, starting consumption...", kafkaCfg.GroupID)
	
	for {
		// 添加心跳日志,确认循环在执行
		log.Printf("Consumer heartbeat - still alive and waiting for messages...")
		
		msg, err := consumer.ReadMessage(context.Background())
		if err != nil {
			log.Printf("Read error: %v", err)
			time.Sleep(2 * time.Second)
			continue
		}
		
		log.Printf("✅ Message received successfully!")
		// ... 处理消息
	}
}

📋 最关键的区别

你的命令行能成功是因为:

· 使用了 --from-beginning(相当于 StartOffset: kafka.FirstOffset)

· 没有消费者组(或者每次都是新的消费者)

而你的Go程序可能:

  1. 使用了不同的起始偏移量
  2. 消费者组偏移量问题
  3. 配置参数不匹配

🔧 最终建议

  1. 先运行独立测试验证消费者配置
  2. 如果还不行,尝试换一个GroupID(避免偏移量问题)
  3. 检查防火墙和安全设置(虽然命令行能通,但Go程序可能被限制)

你现在已经很接近解决了!手动消费成功说明基础设施没问题,只需要调整Go消费者的配置即可。

这个问题很典型,说明消费者没有从Kafka接收到消息。让我帮你一步步排查这个问题。

🐛 问题排查步骤

  1. 首先检查Kafka连接状态

在你的main.go中添加详细的连接检查:

go 复制代码
// 在main.go的初始化部分添加
func main() {
    // ... 加载配置
    
    // 详细的Kafka连接检查
    if cfg.KafkaEnabled {
        if success := kafkapkg.CheckKafkaConnection(cfg); !success {
            log.Printf("WARNING: Kafka connection failed, but continuing without Kafka...")
        }
    }

    // ... 其他初始化代码
}
  1. 更新消费者代码,添加详细的调试信息
go 复制代码
package kafka

import (
	"blog-project/config"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/segmentio/kafka-go"
)

// InitKafkaConsumer 初始化Kafka消费者
func InitKafkaConsumer(cfg *config.Config) {
	InitKafka(cfg)

	if !IsEnabled() {
		log.Println("Kafka consumer is disabled")
		return
	}

	log.Printf("Initializing Kafka consumer with brokers: %v, topic: %s, group: %s",
		kafkaCfg.Brokers, kafkaCfg.Topic, kafkaCfg.GroupID)

	// 创建消费者时添加更多配置选项
	consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:        kafkaCfg.Brokers,
		Topic:          kafkaCfg.Topic,
		GroupID:        kafkaCfg.GroupID,
		MinBytes:       10e3, // 10KB
		MaxBytes:       10e6, // 10MB
		StartOffset:    kafka.FirstOffset, // 从最早的消息开始读取(调试用)
		CommitInterval: time.Second,       // 提交偏移量的间隔
	})

	// 检查topic是否存在
	if err := checkTopicExists(); err != nil {
		log.Printf("WARNING: Topic check failed: %v", err)
	} else {
		log.Printf("Topic '%s' exists and is accessible", kafkaCfg.Topic)
	}

	log.Println("Kafka consumer initialized successfully")

	// 启动消费者协程
	go consumeMessages()
}

func checkTopicExists() error {
	// 连接到任意一个broker
	conn, err := kafka.Dial("tcp", kafkaCfg.Brokers[0])
	if err != nil {
		return fmt.Errorf("failed to dial broker: %v", err)
	}
	defer conn.Close()

	// 获取topic的partitions
	partitions, err := conn.ReadPartitions()
	if err != nil {
		return fmt.Errorf("failed to read partitions: %v", err)
	}

	// 检查我们的topic是否存在
	topicExists := false
	for _, p := range partitions {
		if p.Topic == kafkaCfg.Topic {
			topicExists = true
			break
		}
	}

	if !topicExists {
		return fmt.Errorf("topic '%s' does not exist", kafkaCfg.Topic)
	}

	return nil
}

func consumeMessages() {
	if consumer == nil {
		log.Println("Consumer is nil, cannot start consuming")
		return
	}

	defer func() {
		if r := recover(); r != nil {
			log.Printf("Recovered from panic in consumer: %v", r)
		}
	}()

	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	log.Println("Starting Kafka consumer loop...")

	// 添加消费者统计信息
	var messageCount int
	startTime := time.Now()

	for {
		select {
		case <-sigchan:
			log.Printf("Shutting down Kafka consumer. Processed %d messages in %v", 
				messageCount, time.Since(startTime))
			closeConsumer()
			return
		default:
			log.Printf("Attempting to read message from Kafka...")
			
			// 使用带超时的context,但时间较长
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
			msg, err := consumer.ReadMessage(ctx)
			cancel()

			if err != nil {
				if err == context.DeadlineExceeded {
					log.Printf("Read message timeout after 30 seconds, no messages available")
					continue
				}
				log.Printf("Error reading message from Kafka: %v", err)
				
				// 如果是连接错误,等待更长时间
				if isConnectionError(err) {
					log.Printf("Connection error detected, waiting 10 seconds before retry...")
					time.Sleep(10 * time.Second)
				} else {
					time.Sleep(5 * time.Second)
				}
				continue
			}

			messageCount++
			log.Printf("Received message [%d]: topic=%s, partition=%d, offset=%d, key=%s",
				messageCount, msg.Topic, msg.Partition, msg.Offset, string(msg.Key))

			// 处理消息
			processMessage(msg.Value)
		}
	}
}

func isConnectionError(err error) bool {
	// 简单的连接错误判断
	errorStr := err.Error()
	return contains(errorStr, "connection") || contains(errorStr, "connect") || 
		   contains(errorStr, "broker") || contains(errorStr, "dial")
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && s[:len(substr)] == substr
}

func processMessage(message []byte) {
	log.Printf("Raw message content: %s", string(message))

	var event PublishBlogEvent
	if err := json.Unmarshal(message, &event); err != nil {
		log.Printf("Failed to unmarshal Kafka message: %v", err)
		log.Printf("Raw message that failed: %s", string(message))
		return
	}

	log.Printf("Successfully parsed event: blogID=%d, title=%s, user=%s",
		event.BlogID, event.Title, event.UserName)

	// 发送邮件通知
	if err := sendEmailNotification(&event); err != nil {
		log.Printf("Failed to send email notification: %v", err)
	} else {
		log.Printf("Email notification sent successfully for blogID=%d", event.BlogID)
	}
}

// ... 其他函数保持不变
  1. 手动测试Kafka的命令行方法

在你的服务器上运行这些命令来验证Kafka状态:

bash 复制代码
# 1. 检查Kafka服务状态
sudo systemctl status kafka
# 或者
ps aux | grep kafka

# 2. 检查Kafka topic列表
/opt/kafka/bin/kafka-topics.sh --list --bootstrap-server localhost:9092

# 3. 手动消费消息(查看是否有消息)
/opt/kafka/bin/kafka-console-consumer.sh \
  --bootstrap-server localhost:9092 \
  --topic publish-blog-events \
  --from-beginning \
  --timeout-ms 5000

# 4. 查看topic详情
/opt/kafka/bin/kafka-topics.sh \
  --describe \
  --topic publish-blog-events \
  --bootstrap-server localhost:9092

# 5. 手动生产测试消息
/opt/kafka/bin/kafka-console-producer.sh \
  --broker-list localhost:9092 \
  --topic publish-blog-events
  1. 添加生产者端的详细日志
go 复制代码
// 在pkg/kafka/producer.go中
func SendPublishBlogEvent(event *PublishBlogEvent) error {
	if !IsEnabled() || producer == nil {
		log.Println("Kafka producer is disabled or not initialized, skipping event")
		return nil
	}

	eventData, err := json.Marshal(event)
	if err != nil {
		log.Printf("Failed to marshal event: %v", err)
		return err
	}

	// 添加详细的消息内容日志
	log.Printf("Preparing to send Kafka message: %s", string(eventData))

	// 使用goroutine异步发送
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()

		startTime := time.Now()
		err := producer.WriteMessages(ctx, kafka.Message{
			Value: eventData,
		})

		if err != nil {
			log.Printf("Failed to send message to Kafka after %v: %v", 
				time.Since(startTime), err)
		} else {
			log.Printf("Successfully sent message to Kafka in %v: blogID=%d", 
				time.Since(startTime), event.BlogID)
		}
	}()

	return nil
}
  1. 环境变量检查

确保你的环境变量正确设置:

bash 复制代码
# 检查这些关键环境变量
echo "KAFKA_BROKERS: $KAFKA_BROKERS"
echo "KAFKA_TOPIC: $KAFKA_TOPIC" 
echo "KAFKA_GROUP_ID: $KAFKA_GROUP_ID"
echo "KAFKA_ENABLED: $KAFKA_ENABLED"
  1. 网络连接检查
bash 复制代码
# 检查是否能连接到Kafka端口
nc -zv localhost 9092
# 或者
telnet localhost 9092

# 检查防火墙设置
sudo ufw status

📋 排查流程图
否 是 否 是 否 是 消费者卡在ReadMessage 检查Kafka服务状态 运行kafka-topics.sh --list Topic是否存在? 创建topic
kafka-topics.sh --create 手动消费消息测试 能否收到消息? 检查生产者日志 检查消费者配置 生产者有发送日志? 检查生产者初始化 检查网络连接 检查GroupID和Offset设置 尝试换GroupID或StartOffset 检查防火墙和端口 使用telnet测试连接 问题解决

按照这个流程一步步排查,你应该能找到问题所在。最常见的问题包括:

  1. Kafka服务未正常运行
  2. Topic不存在
  3. 网络连接问题
  4. 消费者GroupID和Offset配置问题
  5. 防火墙阻止了连接

请先运行手动测试命令,看看Kafka本身是否正常工作。

err := producer.WriteMessages(context.Background(), kafka.Message{

Value: eventData, // 你的消息序列化后的字节数组

})

if err != nil {

log.Printf("Failed to send message to Kafka: %v", err)

} else {

log.Printf("Successfully sent message to Kafka for blogID: %d", event.BlogID)

}

你说得对,在producer和consumer中都有kafkaCfg变量导致了重复声明的问题。让我修复这个问题,使用更合理的包结构设计。

解决方案:使用共享配置和更好的包结构

  1. 创建共享的配置管理 (pkg/kafka/config.go)
go 复制代码
package kafka

import (
	"blog-project/config"
	"strings"
	"sync"
)

var (
	once      sync.Once
	kafkaCfg  *KafkaConfig
	producer  *kafka.Writer
	consumer  *kafka.Reader
)

// InitKafka 初始化Kafka配置(线程安全)
func InitKafka(cfg *config.Config) {
	once.Do(func() {
		kafkaCfg = &KafkaConfig{
			Brokers: getBrokersList(cfg.KafkaBrokers),
			Topic:   cfg.KafkaTopic,
			GroupID: cfg.KafkaGroupID,
			Enabled: cfg.KafkaEnabled,
		}
	})
}

// GetConfig 获取Kafka配置
func GetConfig() *KafkaConfig {
	return kafkaCfg
}

// getBrokersList 解析brokers字符串为列表
func getBrokersList(brokers string) []string {
	brokerList := strings.Split(brokers, ",")
	for i, broker := range brokerList {
		brokerList[i] = strings.TrimSpace(broker)
	}
	return brokerList
}

// IsEnabled 检查Kafka是否启用
func IsEnabled() bool {
	return kafkaCfg != nil && kafkaCfg.Enabled
}
  1. 更新生产者 (pkg/kafka/producer.go)
go 复制代码
package kafka

import (
	"blog-project/config"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"github.com/segmentio/kafka-go"
)

// InitKafkaProducer 初始化Kafka生产者
func InitKafkaProducer(cfg *config.Config) {
	InitKafka(cfg) // 确保配置初始化

	if !IsEnabled() {
		log.Println("Kafka producer is disabled")
		return
	}

	producer = &kafka.Writer{
		Addr:         kafka.TCP(kafkaCfg.Brokers...),
		Topic:        kafkaCfg.Topic,
		Balancer:     &kafka.LeastBytes{},
		BatchTimeout: 10 * time.Millisecond,
		Async:        true,
	}

	log.Printf("Kafka producer initialized successfully with brokers: %v", kafkaCfg.Brokers)
}

// SendPublishBlogEvent 发送博客发布事件
func SendPublishBlogEvent(event *PublishBlogEvent) error {
	if !IsEnabled() || producer == nil {
		log.Println("Kafka producer is disabled or not initialized, skipping event")
		return nil
	}

	eventData, err := json.Marshal(event)
	if err != nil {
		return fmt.Errorf("failed to marshal event: %v", err)
	}

	// 使用goroutine异步发送
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		err := producer.WriteMessages(ctx, kafka.Message{
			Value: eventData,
		})

		if err != nil {
			log.Printf("Failed to send message to Kafka: %v", err)
			// 这里可以添加重试逻辑或者降级处理
		} else {
			log.Printf("Successfully sent publish blog event to Kafka: blogID=%d", event.BlogID)
		}
	}()

	return nil
}

// CloseProducer 关闭生产者
func CloseProducer() {
	if producer != nil {
		if err := producer.Close(); err != nil {
			log.Printf("Error closing Kafka producer: %v", err)
		} else {
			log.Println("Kafka producer closed successfully")
		}
	}
}
  1. 更新消费者 (pkg/kafka/consumer.go)
go 复制代码
package kafka

import (
	"blog-project/config"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/segmentio/kafka-go"
)

// InitKafkaConsumer 初始化Kafka消费者
func InitKafkaConsumer(cfg *config.Config) {
	InitKafka(cfg) // 确保配置初始化

	if !IsEnabled() {
		log.Println("Kafka consumer is disabled")
		return
	}

	log.Printf("Initializing Kafka consumer with brokers: %v, topic: %s, group: %s",
		kafkaCfg.Brokers, kafkaCfg.Topic, kafkaCfg.GroupID)

	consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:  kafkaCfg.Brokers,
		Topic:    kafkaCfg.Topic,
		GroupID:  kafkaCfg.GroupID,
		MinBytes: 10e3,
		MaxBytes: 10e6,
	})

	log.Println("Kafka consumer initialized successfully")

	// 启动消费者协程
	go consumeMessages()
}

func consumeMessages() {
	if consumer == nil {
		log.Println("Consumer is nil, cannot start consuming")
		return
	}

	defer func() {
		if r := recover(); r != nil {
			log.Printf("Recovered from panic in consumer: %v", r)
		}
	}()

	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	log.Println("Starting Kafka consumer loop...")

	for {
		select {
		case <-sigchan:
			log.Println("Shutting down Kafka consumer")
			closeConsumer()
			return
		default:
			// 使用不带超时的context
			msg, err := consumer.ReadMessage(context.Background())
			if err != nil {
				log.Printf("Error reading message from Kafka: %v", err)
				if shouldRetry(err) {
					time.Sleep(5 * time.Second)
					continue
				} else {
					log.Printf("Fatal error, stopping consumer: %v", err)
					return
				}
			}

			log.Printf("Received raw message from Kafka: topic=%s, partition=%d, offset=%d",
				msg.Topic, msg.Partition, msg.Offset)

			// 处理消息
			processMessage(msg.Value)
		}
	}
}

func shouldRetry(err error) bool {
	return err != context.Canceled
}

func processMessage(message []byte) {
	log.Printf("Processing message: %s", string(message))

	var event PublishBlogEvent
	if err := json.Unmarshal(message, &event); err != nil {
		log.Printf("Failed to unmarshal Kafka message: %v, raw: %s", err, string(message))
		return
	}

	log.Printf("Successfully parsed publish blog event: blogID=%d, title=%s, user=%s",
		event.BlogID, event.Title, event.UserName)

	// 发送邮件通知
	if err := sendEmailNotification(&event); err != nil {
		log.Printf("Failed to send email notification: %v", err)
	} else {
		log.Printf("Email notification sent successfully for blogID=%d", event.BlogID)
	}
}

func sendEmailNotification(event *PublishBlogEvent) error {
	emailContent := fmt.Sprintf(`
尊敬的 %s,

您的博客《%s》已经成功发布!

博客ID: %d
发布时间: %s

感谢您使用我们的博客服务!

Best regards,
博客团队
	`, event.UserName, event.Title, event.BlogID, time.Now().Format("2006-01-02 15:04:05"))

	log.Printf("Sending email notification:\n%s", emailContent)

	// 模拟邮件发送延迟
	time.Sleep(100 * time.Millisecond)

	log.Printf("Email sent to user %s for blog %d", event.UserName, event.BlogID)
	return nil
}

func closeConsumer() {
	if consumer != nil {
		if err := consumer.Close(); err != nil {
			log.Printf("Error closing Kafka consumer: %v", err)
		} else {
			log.Println("Kafka consumer closed successfully")
		}
	}
}

// CheckKafkaConnection 检查Kafka连接
func CheckKafkaConnection(cfg *config.Config) bool {
	InitKafka(cfg)
	
	if !IsEnabled() {
		log.Println("Kafka is disabled, skipping connection check")
		return false
	}

	// 尝试创建临时连接来检查Kafka可用性
	if len(kafkaCfg.Brokers) == 0 {
		log.Println("No Kafka brokers configured")
		return false
	}

	conn, err := kafka.Dial("tcp", kafkaCfg.Brokers[0])
	if err != nil {
		log.Printf("Kafka connection check failed: %v", err)
		return false
	}
	defer conn.Close()

	// 获取topic列表来验证连接
	partitions, err := conn.ReadPartitions()
	if err != nil {
		log.Printf("Failed to read partitions: %v", err)
		return false
	}

	log.Printf("Kafka connection successful. Available partitions: %d", len(partitions))
	return true
}
  1. 更新类型定义 (pkg/kafka/types.go)
go 复制代码
package kafka

import "github.com/segmentio/kafka-go"

// KafkaConfig Kafka配置
type KafkaConfig struct {
	Brokers []string
	Topic   string
	GroupID string
	Enabled bool
}

// PublishBlogEvent 博客发布事件
type PublishBlogEvent struct {
	BlogID    uint   `json:"blogId"`
	Title     string `json:"title"`
	Content   string `json:"content"`
	UserID    uint   `json:"userId"`
	UserName  string `json:"userName"`
	UserEmail string `json:"userEmail"`
}
  1. 更新主程序调用方式 (main.go)
go 复制代码
package main

import (
	"blog-project/config"
	"blog-project/controllers"
	"blog-project/database"
	kafkapkg "blog-project/pkg/kafka"
	"blog-project/middleware"
	"log"
	"os"
	"os/signal"
	"syscall"

	"github.com/gin-gonic/gin"
)

func main() {
	// 加载配置
	cfg := config.LoadConfig()

	// 初始化数据库
	if err := database.InitPostgres(cfg); err != nil {
		log.Fatalf("Failed to initialize PostgreSQL: %v", err)
	}

	// 初始化Redis
	if err := database.InitRedis(cfg); err != nil {
		log.Fatalf("Failed to initialize Redis: %v", err)
	}

	// Kafka健康检查
	kafkapkg.CheckKafkaConnection(cfg)

	// 初始化Kafka生产者
	kafkapkg.InitKafkaProducer(cfg)

	// 初始化Kafka消费者
	kafkapkg.InitKafkaConsumer(cfg)

	// 创建控制器
	userController := controllers.NewUserController()
	blogController := controllers.NewBlogController()

	// 创建Gin路由
	r := gin.Default()

	// 用户路由
	userGroup := r.Group("/user")
	{
		userGroup.POST("/register", userController.Register)
		userGroup.POST("/login", userController.Login)
		userGroup.POST("/logout", userController.Logout)
	}

	// 博客路由(需要认证)
	blogGroup := r.Group("/blog")
	blogGroup.Use(middleware.AuthMiddleware())
	{
		blogGroup.POST("/publishBlog", blogController.PublishBlog)
		blogGroup.POST("/getBlogList", blogController.GetBlogList)
		blogGroup.POST("/getBlogDetail", blogController.GetBlogDetail)
	}

	// 优雅关闭
	setupGracefulShutdown()

	// 启动服务器
	log.Println("Server starting on :8080")
	if err := r.Run(":8080"); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

func setupGracefulShutdown() {
	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		<-sigchan
		log.Println("Shutting down gracefully...")
		
		// 关闭Kafka生产者
		kafkapkg.CloseProducer()
		
		os.Exit(0)
	}()
}

主要改进

  1. 解决变量冲突:使用共享的配置管理,避免重复声明
  2. 线程安全初始化:使用sync.Once确保配置只初始化一次
  3. 更好的错误处理:添加了更详细的日志和错误处理
  4. 统一的配置访问:通过GetConfig()和IsEnabled()方法访问配置
  5. 改进的连接检查:更健壮的Kafka连接验证

现在这个结构应该能解决变量重声明的问题,并且提供更清晰的包组织。

从你描述的现象来看,问题很可能出现在Kafka消费者没有正确连接到Kafka服务器或者消息没有被正确消费。让我帮你分析和修复这个问题。

问题分析

  1. 生产者成功发送消息:日志显示"successfully sent publish blog event to kafka",说明生产者工作正常
  2. 消费者没有处理消息:没有后续的消费日志,说明消费者可能:
    · 没有连接到Kafka服务器
    · 连接到了错误的topic
    · 遇到了错误但没有正确日志输出
    · 消息格式有问题导致反序列化失败

修复后的消费者代码

go 复制代码
package kafka

import (
	"blog-project/config"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/segmentio/kafka-go"
)

var (
	consumer *kafka.Reader
	kafkaCfg *KafkaConfig
)

func InitKafkaConsumer(cfg *config.Config) {
	kafkaCfg = &KafkaConfig{
		Brokers: cfg.GetKafkaBrokersList(), // 使用解析后的broker列表
		Topic:   cfg.KafkaTopic,
		GroupID: cfg.KafkaGroupID,
		Enabled: cfg.KafkaEnabled,
	}

	if !kafkaCfg.Enabled {
		log.Println("Kafka consumer is disabled")
		return
	}

	// 添加详细的连接日志
	log.Printf("Initializing Kafka consumer with brokers: %v, topic: %s, group: %s", 
		kafkaCfg.Brokers, kafkaCfg.Topic, kafkaCfg.GroupID)

	consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:  kafkaCfg.Brokers,
		Topic:    kafkaCfg.Topic,
		GroupID:  kafkaCfg.GroupID,
		MinBytes: 10e3, // 10KB
		MaxBytes: 10e6, // 10MB
	})

	log.Println("Kafka consumer initialized successfully")

	// 启动消费者协程
	go consumeMessages()
}

func consumeMessages() {
	if consumer == nil {
		log.Println("Consumer is nil, cannot start consuming")
		return
	}

	defer func() {
		if r := recover(); r != nil {
			log.Printf("Recovered from panic in consumer: %v", r)
		}
	}()

	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	log.Println("Starting Kafka consumer loop...")

	for {
		select {
		case <-sigchan:
			log.Println("Shutting down Kafka consumer")
			closeConsumer()
			return
		default:
			// 使用不带超时的context,避免频繁超时
			msg, err := consumer.ReadMessage(context.Background())
			if err != nil {
				log.Printf("Error reading message from Kafka: %v", err)
				// 根据错误类型决定是否重试
				if shouldRetry(err) {
					time.Sleep(5 * time.Second)
					continue
				} else {
					log.Printf("Fatal error, stopping consumer: %v", err)
					return
				}
			}

			log.Printf("Received raw message from Kafka: topic=%s, partition=%d, offset=%d", 
				msg.Topic, msg.Partition, msg.Offset)

			// 处理消息
			processMessage(msg.Value)
		}
	}
}

func shouldRetry(err error) bool {
	// 可以重试的错误类型
	return err != context.Canceled
}

func processMessage(message []byte) {
	log.Printf("Processing message: %s", string(message))

	var event PublishBlogEvent
	if err := json.Unmarshal(message, &event); err != nil {
		log.Printf("Failed to unmarshal Kafka message: %v, raw: %s", err, string(message))
		return
	}

	log.Printf("Successfully parsed publish blog event: blogID=%d, title=%s, user=%s", 
		event.BlogID, event.Title, event.UserName)

	// 发送邮件通知
	if err := sendEmailNotification(&event); err != nil {
		log.Printf("Failed to send email notification: %v", err)
	} else {
		log.Printf("Email notification sent successfully for blogID=%d", event.BlogID)
	}
}

func sendEmailNotification(event *PublishBlogEvent) error {
	// 这里使用真实的邮件发送逻辑
	emailContent := fmt.Sprintf(`
尊敬的 %s,

您的博客《%s》已经成功发布!

博客ID: %d
发布时间: %s
内容摘要: %s...

感谢您使用我们的博客服务!

Best regards,
博客团队
	`, event.UserName, event.Title, event.BlogID, 
		time.Now().Format("2006-01-02 15:04:05"),
		truncateString(event.Content, 100))

	log.Printf("Sending email notification to admin for blog: %s by %s", event.Title, event.UserName)
	
	// 模拟真实的邮件发送过程
	time.Sleep(500 * time.Millisecond)
	
	log.Printf("Email notification completed for blogID=%d", event.BlogID)
	return nil
}

func truncateString(s string, length int) string {
	if len(s) <= length {
		return s
	}
	return s[:length] + "..."
}

func closeConsumer() {
	if consumer != nil {
		if err := consumer.Close(); err != nil {
			log.Printf("Error closing Kafka consumer: %v", err)
		} else {
			log.Println("Kafka consumer closed successfully")
		}
	}
}

// 添加健康检查函数
func CheckKafkaConnection(cfg *config.Config) bool {
	if !cfg.KafkaEnabled {
		log.Println("Kafka is disabled, skipping connection check")
		return false
	}

	// 尝试创建临时连接来检查Kafka可用性
	conn, err := kafka.Dial("tcp", cfg.GetKafkaBrokersList()[0])
	if err != nil {
		log.Printf("Kafka connection check failed: %v", err)
		return false
	}
	defer conn.Close()

	// 获取topic列表来验证连接
	partitions, err := conn.ReadPartitions()
	if err != nil {
		log.Printf("Failed to read partitions: %v", err)
		return false
	}

	log.Printf("Kafka connection successful. Available topics: %d", len(partitions))
	return true
}

需要检查的其他文件

  1. 确保 config.go 有正确的方法
go 复制代码
// 在 config/config.go 中添加这个方法
func (c *Config) GetKafkaBrokersList() []string {
	brokers := strings.Split(c.KafkaBrokers, ",")
	// 清理空格
	for i, broker := range brokers {
		brokers[i] = strings.TrimSpace(broker)
	}
	return brokers
}
  1. 更新主程序添加健康检查
go 复制代码
// 在 main.go 的初始化部分添加
func main() {
	// ... 加载配置

	// Kafka健康检查
	if cfg.KafkaEnabled {
		kafkapkg.CheckKafkaConnection(cfg)
	}

	// 初始化Kafka生产者
	kafkapkg.InitKafkaProducer(cfg)

	// 初始化Kafka消费者
	kafkapkg.InitKafkaConsumer(cfg)
	
	// ... 其他代码
}
  1. 检查生产者代码

确保生产者在 pkg/kafka/producer.go 中正确发送消息:

go 复制代码
func SendPublishBlogEvent(event *PublishBlogEvent) error {
	if !kafkaCfg.Enabled || producer == nil {
		log.Println("Kafka producer is disabled or not initialized, skipping event")
		return nil
	}

	eventData, err := json.Marshal(event)
	if err != nil {
		log.Printf("Failed to marshal event: %v", err)
		return err
	}

	// 添加消息内容日志
	log.Printf("Sending Kafka message: %s", string(eventData))

	// 使用goroutine异步发送
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		err := producer.WriteMessages(ctx, kafka.Message{
			Value: eventData,
		})

		if err != nil {
			log.Printf("Failed to send message to Kafka: %v", err)
		} else {
			log.Printf("Successfully sent publish blog event to Kafka: blogID=%d", event.BlogID)
		}
	}()

	return nil
}

调试步骤

  1. 检查Kafka服务器状态:

    bash 复制代码
    # 检查Kafka是否运行
    docker ps | grep kafka
    # 或者
    ps aux | grep kafka
  2. 检查环境变量配置:

    bash 复制代码
    # 确保这些变量正确设置
    KAFKA_BROKERS=localhost:9092
    KAFKA_TOPIC=publish-blog-events
    KAFKA_GROUP_ID=blog-service-group
    KAFKA_ENABLED=true
  3. 手动测试Kafka连接:

    bash 复制代码
    # 使用kafka-console-consumer手动检查消息
    kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic publish-blog-events --from-beginning
  4. 查看详细的日志: 启动应用后,关注以下日志:
    · Kafka连接检查日志
    · 生产者发送消息的详细日志
    · 消费者启动和接收消息的日志

可能的问题原因

  1. Kafka服务器未运行:检查Kafka是否真正启动
  2. 网络连接问题:确保应用能访问Kafka的9092端口
  3. Topic不存在:Kafka不会自动创建topic,需要先创建topic
  4. 消费者组问题:尝试使用不同的group.id
  5. 消息格式问题:检查JSON序列化是否正确

请按照这个修复后的代码更新,并检查日志输出,应该能发现问题所在。

我将在现有代码基础上添加Kafka相关功能,并确保在Kafka服务器未启动时不影响正常业务。

更新后的项目结构

复制代码
blog-project/
├── pkg/
│   └── kafka/
│       ├── producer.go
│       ├── consumer.go
│       └── types.go

Kafka类型定义 (pkg/kafka/types.go)

go 复制代码
package kafka

type PublishBlogEvent struct {
	BlogID    uint   `json:"blogId"`
	Title     string `json:"title"`
	Content   string `json:"content"`
	UserID    uint   `json:"userId"`
	UserName  string `json:"userName"`
	UserEmail string `json:"userEmail"` // 假设用户有邮箱字段
}

type KafkaConfig struct {
	Brokers  []string
	Topic    string
	GroupID  string
	Enabled  bool
}

Kafka生产者 (pkg/kafka/producer.go)

go 复制代码
package kafka

import (
	"blog-project/config"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"time"

	"github.com/segmentio/kafka-go"
)

var (
	producer *kafka.Writer
	kafkaCfg *KafkaConfig
)

func InitKafkaProducer(cfg *config.Config) {
	kafkaCfg = &KafkaConfig{
		Brokers: []string{cfg.KafkaBrokers},
		Topic:   cfg.KafkaTopic,
		Enabled: cfg.KafkaEnabled,
	}

	if !kafkaCfg.Enabled {
		log.Println("Kafka producer is disabled")
		return
	}

	producer = &kafka.Writer{
		Addr:         kafka.TCP(kafkaCfg.Brokers...),
		Topic:        kafkaCfg.Topic,
		Balancer:     &kafka.LeastBytes{},
		BatchTimeout: 10 * time.Millisecond,
		Async:        true, // 异步发送
	}

	log.Println("Kafka producer initialized successfully")
}

func SendPublishBlogEvent(event *PublishBlogEvent) error {
	if !kafkaCfg.Enabled || producer == nil {
		log.Println("Kafka producer is disabled or not initialized, skipping event")
		return nil
	}

	eventData, err := json.Marshal(event)
	if err != nil {
		return fmt.Errorf("failed to marshal event: %v", err)
	}

	// 使用goroutine异步发送
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()

		err := producer.WriteMessages(ctx, kafka.Message{
			Value: eventData,
		})

		if err != nil {
			log.Printf("Failed to send message to Kafka: %v", err)
			// 这里可以添加重试逻辑或者降级处理
		} else {
			log.Printf("Successfully sent publish blog event to Kafka: blogID=%d", event.BlogID)
		}
	}()

	return nil
}

func CloseProducer() {
	if producer != nil {
		producer.Close()
	}
}

Kafka消费者 (pkg/kafka/consumer.go)

go 复制代码
package kafka

import (
	"blog-project/config"
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/segmentio/kafka-go"
)

var consumer *kafka.Reader

func InitKafkaConsumer(cfg *config.Config) {
	kafkaCfg := &KafkaConfig{
		Brokers: []string{cfg.KafkaBrokers},
		Topic:   cfg.KafkaTopic,
		GroupID: cfg.KafkaGroupID,
		Enabled: cfg.KafkaEnabled,
	}

	if !kafkaCfg.Enabled {
		log.Println("Kafka consumer is disabled")
		return
	}

	consumer = kafka.NewReader(kafka.ReaderConfig{
		Brokers:  kafkaCfg.Brokers,
		Topic:    kafkaCfg.Topic,
		GroupID:  kafkaCfg.GroupID,
		MinBytes: 10e3, // 10KB
		MaxBytes: 10e6, // 10MB
	})

	log.Println("Kafka consumer initialized successfully")

	// 启动消费者协程
	go consumeMessages()
}

func consumeMessages() {
	if consumer == nil {
		return
	}

	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	for {
		select {
		case <-sigchan:
			log.Println("Shutting down Kafka consumer")
			closeConsumer()
			return
		default:
			ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
			msg, err := consumer.ReadMessage(ctx)
			cancel()

			if err != nil {
				if err == context.DeadlineExceeded {
					continue
				}
				log.Printf("Error reading message from Kafka: %v", err)
				time.Sleep(5 * time.Second) // 等待后重试
				continue
			}

			// 处理消息
			go processMessage(msg.Value)
		}
	}
}

func processMessage(message []byte) {
	var event PublishBlogEvent
	if err := json.Unmarshal(message, &event); err != nil {
		log.Printf("Failed to unmarshal Kafka message: %v", err)
		return
	}

	log.Printf("Received publish blog event: blogID=%d, title=%s", event.BlogID, event.Title)

	// 发送邮件通知
	if err := sendEmailNotification(&event); err != nil {
		log.Printf("Failed to send email notification: %v", err)
	} else {
		log.Printf("Email notification sent successfully for blogID=%d", event.BlogID)
	}
}

func sendEmailNotification(event *PublishBlogEvent) error {
	// 模拟发送邮件逻辑
	// 在实际项目中,这里应该集成邮件服务(如SMTP、SendGrid等)
	
	emailContent := fmt.Sprintf(`
尊敬的 %s,

您的博客《%s》已经成功发布!

博客ID: %d
发布时间: %s

感谢您使用我们的博客服务!

Best regards,
博客团队
	`, event.UserName, event.Title, event.BlogID, time.Now().Format("2006-01-02 15:04:05"))

	log.Printf("Sending email notification:\n%s", emailContent)
	
	// 模拟邮件发送延迟
	time.Sleep(100 * time.Millisecond)
	
	log.Printf("Email sent to user %s for blog %d", event.UserName, event.BlogID)
	return nil
}

func closeConsumer() {
	if consumer != nil {
		consumer.Close()
	}
}

更新配置文件 (config/config.go)

go 复制代码
package config

import (
	"os"
	"strconv"
	"strings"
)

type Config struct {
	DBHost        string
	DBPort        string
	DBUser        string
	DBPassword    string
	DBName        string
	RedisAddr     string
	RedisPassword string
	RedisDB       int
	KafkaBrokers  string
	KafkaTopic    string
	KafkaGroupID  string
	KafkaEnabled  bool
}

func LoadConfig() *Config {
	return &Config{
		DBHost:        getEnv("DB_HOST", "localhost"),
		DBPort:        getEnv("DB_PORT", "5432"),
		DBUser:        getEnv("DB_USER", "postgres"),
		DBPassword:    getEnv("DB_PASSWORD", "password"),
		DBName:        getEnv("DB_NAME", "blogdb"),
		RedisAddr:     getEnv("REDIS_ADDR", "localhost:6379"),
		RedisPassword: getEnv("REDIS_PASSWORD", ""),
		RedisDB:       getEnvAsInt("REDIS_DB", 0),
		KafkaBrokers:  getEnv("KAFKA_BROKERS", "localhost:9092"),
		KafkaTopic:    getEnv("KAFKA_TOPIC", "publish-blog-events"),
		KafkaGroupID:  getEnv("KAFKA_GROUP_ID", "blog-service-group"),
		KafkaEnabled:  getEnvAsBool("KAFKA_ENABLED", true),
	}
}

func getEnv(key, defaultValue string) string {
	if value, exists := os.LookupEnv(key); exists {
		return value
	}
	return defaultValue
}

func getEnvAsInt(key string, defaultValue int) int {
	if value, exists := os.LookupEnv(key); exists {
		if intValue, err := strconv.Atoi(value); err == nil {
			return intValue
		}
	}
	return defaultValue
}

func getEnvAsBool(key string, defaultValue bool) bool {
	if value, exists := os.LookupEnv(key); exists {
		if boolValue, err := strconv.ParseBool(value); err == nil {
			return boolValue
		}
	}
	return defaultValue
}

func (c *Config) GetKafkaBrokersList() []string {
	return strings.Split(c.KafkaBrokers, ",")
}

更新用户模型 (models/user.go)

go 复制代码
package models

import (
	"time"
)

type User struct {
	UserID      uint      `gorm:"primaryKey;autoIncrement" json:"userId"`
	Phone       string    `gorm:"type:varchar(16);not null;unique" json:"phone"`
	Password    string    `gorm:"type:varchar(255);not null" json:"-"`
	Username    string    `gorm:"type:varchar(50)" json:"userName"`
	Email       string    `gorm:"type:varchar(100)" json:"email"` // 添加邮箱字段
	CreatedTime int64     `gorm:"type:bigint" json:"createdTime"`
	UpdatedTime int64     `gorm:"type:bigint" json:"updatedTime"`
}

func (User) TableName() string {
	return "user"
}

func BeforeCreate(user *User) error {
	user.CreatedTime = time.Now().Unix()
	user.UpdatedTime = time.Now().Unix()
	return nil
}

func BeforeUpdate(user *User) error {
	user.UpdatedTime = time.Now().Unix()
	return nil
}

更新博客服务层 (services/blog_service.go)

go 复制代码
package services

import (
	"blog-project/constants"
	"blog-project/database"
	"blog-project/dto/request"
	"blog-project/dto/response"
	"blog-project/models"
	kafkapkg "blog-project/pkg/kafka" // 添加别名避免冲突
	"blog-project/utils"
	"fmt"
	"strconv"

	"gorm.io/gorm"
)

type BlogService interface {
	PublishBlog(userID uint, req request.PublishBlogRequest) (response.PublishBlogResponse, error)
	GetBlogList(userID uint, req request.GetBlogListRequest) (response.GetBlogListResponse, error)
	GetBlogDetail(userID uint, req request.GetBlogDetailRequest) (response.GetBlogDetailResponse, error)
}

type blogService struct{}

func NewBlogService() BlogService {
	return &blogService{}
}

func (s *blogService) PublishBlog(userID uint, req request.PublishBlogRequest) (response.PublishBlogResponse, error) {
	// 验证标题格式
	if !utils.ValidateTitle(req.Title) {
		return response.PublishBlogResponse{Code: constants.ErrorCodeTitleFormat}, nil
	}

	// 验证内容格式
	if !utils.ValidateContent(req.Content) {
		return response.PublishBlogResponse{Code: constants.ErrorCodeContentFormat}, nil
	}

	// 获取用户信息(用于邮件通知)
	var user models.User
	if err := database.DB.First(&user, userID).Error; err != nil {
		return response.PublishBlogResponse{Code: constants.ErrorCodeServerError}, err
	}

	// 创建博客
	blog := models.Blog{
		Title:   req.Title,
		Content: req.Content,
		UserID:  userID,
	}

	models.BeforeCreateBlog(&blog)

	if err := database.DB.Create(&blog).Error; err != nil {
		return response.PublishBlogResponse{Code: constants.ErrorCodeServerError}, err
	}

	// 异步发送Kafka消息(即使Kafka不可用也不影响主业务流程)
	go s.sendPublishEvent(&blog, &user)

	return response.PublishBlogResponse{
		Code: constants.ErrorCodeSuccess,
		Result: response.BlogIdResult{
			BlogId: fmt.Sprintf("%d", blog.BlogID),
		},
	}, nil
}

func (s *blogService) sendPublishEvent(blog *models.Blog, user *models.User) {
	event := &kafkapkg.PublishBlogEvent{
		BlogID:    blog.BlogID,
		Title:     blog.Title,
		Content:   blog.Content,
		UserID:    user.UserID,
		UserName:  user.Username,
		UserEmail: user.Email, // 使用用户邮箱
	}

	if err := kafkapkg.SendPublishBlogEvent(event); err != nil {
		// 这里只是记录日志,不影响主业务流程
		fmt.Printf("Failed to send publish event to Kafka: %v\n", err)
	}
}

// GetBlogList 和 GetBlogDetail 方法保持不变...
// [保持原有的GetBlogList和GetBlogDetail方法代码]

更新主程序 (main.go)

go 复制代码
package main

import (
	"blog-project/config"
	"blog-project/controllers"
	"blog-project/database"
	kafkapkg "blog-project/pkg/kafka"
	"blog-project/middleware"
	"log"
	"os"
	"os/signal"
	"syscall"

	"github.com/gin-gonic/gin"
)

func main() {
	// 加载配置
	cfg := config.LoadConfig()

	// 初始化数据库
	if err := database.InitPostgres(cfg); err != nil {
		log.Fatalf("Failed to initialize PostgreSQL: %v", err)
	}

	// 初始化Redis
	if err := database.InitRedis(cfg); err != nil {
		log.Fatalf("Failed to initialize Redis: %v", err)
	}

	// 初始化Kafka生产者
	kafkapkg.InitKafkaProducer(cfg)

	// 初始化Kafka消费者
	kafkapkg.InitKafkaConsumer(cfg)

	// 创建控制器
	userController := controllers.NewUserController()
	blogController := controllers.NewBlogController()

	// 创建Gin路由
	r := gin.Default()

	// 用户路由
	userGroup := r.Group("/user")
	{
		userGroup.POST("/register", userController.Register)
		userGroup.POST("/login", userController.Login)
		userGroup.POST("/logout", userController.Logout)
	}

	// 博客路由(需要认证)
	blogGroup := r.Group("/blog")
	blogGroup.Use(middleware.AuthMiddleware())
	{
		blogGroup.POST("/publishBlog", blogController.PublishBlog)
		blogGroup.POST("/getBlogList", blogController.GetBlogList)
		blogGroup.POST("/getBlogDetail", blogController.GetBlogDetail)
	}

	// 优雅关闭
	setupGracefulShutdown()

	// 启动服务器
	log.Println("Server starting on :8080")
	if err := r.Run(":8080"); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

func setupGracefulShutdown() {
	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		<-sigchan
		log.Println("Shutting down gracefully...")
		
		// 关闭Kafka生产者
		kafkapkg.CloseProducer()
		
		os.Exit(0)
	}()
}

更新go.mod依赖

go 复制代码
module blog-project

go 1.20

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/go-redis/redis/v8 v8.11.5
    gorm.io/driver/postgres v1.5.2
    gorm.io/gorm v1.25.4
    golang.org/x/crypto v0.14.0
    github.com/segmentio/kafka-go v0.4.42 // 新增Kafka依赖
)

// 其他依赖保持不变...

环境变量配置示例

bash 复制代码
# Kafka配置
KAFKA_ENABLED=true
KAFKA_BROKERS=localhost:9092
KAFKA_TOPIC=publish-blog-events
KAFKA_GROUP_ID=blog-service-group

# 如果Kafka未启动,可以设置为false
# KAFKA_ENABLED=false

运行说明

  1. Kafka服务器启动时:系统会正常发送和消费消息,发送邮件通知
  2. Kafka服务器未启动时:
    · 生产者会记录日志并继续执行主业务流程
    · 消费者不会启动
    · 所有博客发布功能正常工作,只是没有邮件通知
    · 通过设置 KAFKA_ENABLED=false 可以完全禁用Kafka功能

这样的设计确保了系统的健壮性,即使消息队列服务不可用,核心业务功能也不会受到影响。

相关推荐
小白学大数据4 天前
Scrapy 框架实战:构建高效的快看漫画分布式爬虫
开发语言·分布式·爬虫·python·scrapy
猫头虎19 天前
用 Python 写你的第一个爬虫:小白也能轻松搞定数据抓取(超详细包含最新所有Python爬虫库的教程)
爬虫·python·opencv·scrapy·beautifulsoup·numpy·scipy
5171 个月前
Scrapy爬虫集成MongoDB存储
爬虫·scrapy·mongodb
万粉变现经纪人1 个月前
如何解决pip安装报错ModuleNotFoundError: No module named ‘keras’问题
人工智能·python·深度学习·scrapy·pycharm·keras·pip
一勺菠萝丶1 个月前
零基础掌握 Scrapy 和 Scrapy-Redis:爬虫分布式部署深度解析
redis·爬虫·scrapy
万粉变现经纪人1 个月前
如何解决pip安装报错ModuleNotFoundError: No module named ‘dash’问题
python·scrapy·pycharm·flask·pip·策略模式·dash
万粉变现经纪人1 个月前
如何解决pip安装报错ModuleNotFoundError: No module named ‘plotly’问题
python·scrapy·plotly·pycharm·flask·pandas·pip
t_hj1 个月前
Scrapy
前端·数据库·scrapy
陌上倾城落蝶雨1 个月前
python爬虫
python·scrapy·pycharm